diff --git a/.changeset/curly-spiders-smash.md b/.changeset/curly-spiders-smash.md
new file mode 100644
index 000000000..516609daa
--- /dev/null
+++ b/.changeset/curly-spiders-smash.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/core": minor
+---
+
+Delete `concurrency` field in favour of `concurrency` operator
diff --git a/.changeset/dry-comics-shop.md b/.changeset/dry-comics-shop.md
new file mode 100644
index 000000000..4416388e7
--- /dev/null
+++ b/.changeset/dry-comics-shop.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/core": patch
+---
+
+Get rid off `response.clone()` to support streaming in edge runtimes
diff --git a/.changeset/famous-hornets-hammer.md b/.changeset/famous-hornets-hammer.md
new file mode 100644
index 000000000..5f2246e0c
--- /dev/null
+++ b/.changeset/famous-hornets-hammer.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/core": minor
+---
+
+Delete `attachOperation` operator
diff --git a/.changeset/metal-dragons-teach.md b/.changeset/metal-dragons-teach.md
new file mode 100644
index 000000000..32e8c178c
--- /dev/null
+++ b/.changeset/metal-dragons-teach.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/dev-tools": minor
+---
+
+Add option `logErrorsToConsole`
diff --git a/.changeset/old-onions-juggle.md b/.changeset/old-onions-juggle.md
new file mode 100644
index 000000000..133392aa5
--- /dev/null
+++ b/.changeset/old-onions-juggle.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/core": patch
+---
+
+Export `ExecutionMeta`
diff --git a/.changeset/strange-apes-heal.md b/.changeset/strange-apes-heal.md
new file mode 100644
index 000000000..740b1ec37
--- /dev/null
+++ b/.changeset/strange-apes-heal.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/core": patch
+---
+
+Export `attachObservability`
diff --git a/apps/showcase-solid-real-world-rick-morty/src/main.tsx b/apps/showcase-solid-real-world-rick-morty/src/main.tsx
index e1e788688..4aa993a00 100644
--- a/apps/showcase-solid-real-world-rick-morty/src/main.tsx
+++ b/apps/showcase-solid-real-world-rick-morty/src/main.tsx
@@ -5,4 +5,4 @@ import { App } from './app';
render(() => , document.getElementById('root') as HTMLElement);
-attachFarfetchedDevTools();
+attachFarfetchedDevTools({ logErrorsToConsole: true });
diff --git a/apps/website/docs/.vitepress/config.js b/apps/website/docs/.vitepress/config.js
index 34eeac1d1..799ff3d8a 100644
--- a/apps/website/docs/.vitepress/config.js
+++ b/apps/website/docs/.vitepress/config.js
@@ -209,10 +209,6 @@ export default withMermaid(
{ text: 'concurrency', link: '/api/operators/concurrency' },
{ text: 'applyBarrier', link: '/api/operators/apply_barrier' },
{ text: 'update', link: '/api/operators/update' },
- {
- text: 'attachOperation',
- link: '/api/operators/attach_operation',
- },
{ text: 'connectQuery', link: '/api/operators/connect_query' },
],
},
@@ -407,6 +403,7 @@ export default withMermaid(
{
text: 'Releases',
items: [
+ { text: 'v0.14', link: '/releases/0-14' },
{ text: 'v0.13 Naiharn', link: '/releases/0-13' },
{ text: 'v0.12 Talat Noi', link: '/releases/0-12' },
{ text: 'v0.11 Namtok Ngao', link: '/releases/0-11' },
diff --git a/apps/website/docs/api/factories/create_json_mutation.md b/apps/website/docs/api/factories/create_json_mutation.md
index 766d0f695..e70b53289 100644
--- a/apps/website/docs/api/factories/create_json_mutation.md
+++ b/apps/website/docs/api/factories/create_json_mutation.md
@@ -39,14 +39,3 @@ Config fields:
- `headers`: raw response headers
- `status.expected`: `number` or `Array` of expected HTTP status codes, if the response status code is not in the list, the mutation will be treated as failed
-
-- `concurrency?`: concurrency settings for the [_Mutation_](/api/primitives/mutation)
- ::: danger Deprecation warning
-
- This field is deprecated since [v0.12](/releases/0-12) and will be removed in v0.14. Use [`concurrency` operator](/api/operators/concurrency) instead.
-
- Please read [this ADR](/adr/concurrency) for more information and migration guide.
-
- :::
-
- - `abort?`: [_Event_](https://effector.dev/en/api/effector/event/) after calling which all in-flight requests will be aborted
diff --git a/apps/website/docs/api/factories/create_json_query.md b/apps/website/docs/api/factories/create_json_query.md
index f50165af2..7c2b4a807 100644
--- a/apps/website/docs/api/factories/create_json_query.md
+++ b/apps/website/docs/api/factories/create_json_query.md
@@ -40,21 +40,6 @@ Config fields:
- `params`: params which were passed to the [_Query_](/api/primitives/query)
- `headers`: raw response headers
-- `concurrency?`: concurrency settings for the [_Query_](/api/primitives/query)
- ::: danger Deprecation warning
-
- This field is deprecated since [v0.12](/releases/0-12) and will be removed in v0.14. Use [`concurrency` operator](/api/operators/concurrency) instead.
-
- Please read [this ADR](/adr/concurrency) for more information and migration guide.
-
- :::
-
- - `strategy?`: available values:
- - `TAKE_EVERY` execute every request
- - `TAKE_FIRST` skip all requests if there is a pending one
- - `TAKE_LATEST` (**default value**) cancel all pending requests and execute the latest one
- - `abort?`: [_Event_](https://effector.dev/en/api/effector/event/) after calling which all in-flight requests will be aborted
-
## Showcases
- [Real-world showcase with SolidJS around JSON API](https://github.com/igorkamyshev/farfetched/tree/master/apps/showcase-solid-real-world-rick-morty/)
diff --git a/apps/website/docs/api/operators/attach_operation.md b/apps/website/docs/api/operators/attach_operation.md
index e59d26c0c..5c22fd111 100644
--- a/apps/website/docs/api/operators/attach_operation.md
+++ b/apps/website/docs/api/operators/attach_operation.md
@@ -2,115 +2,9 @@
outline: [2, 3]
---
-# `attachOperation`
+# `attachOperation`
::: danger
-This operator is deprecated since [v0.12](/releases/0-12) and will be removed in v0.14. Please read [this ADR](/adr/attach_operation_deprecation) for more information and migration guide.
+This operator is removed in [v0.14](/releases/0-14). Please read [this ADR](/adr/attach_operation_deprecation) for more information and migration guide.
:::
-
-Creates new [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) on top of the existing one.
-
-::: tip
-It is analog of [attach](https://effector.dev/en/api/effector/attach/) from Effector for [_Queries_](/api/primitives/query) or [_Mutations_](/api/primitives/mutation).
-:::
-
-## Formulae
-
-### `attachOperation(query)`
-
-Creates new [_Query_](/api/primitives/query) on top of the existing one.
-
-```ts
-import { attachOperation, createQuery } from '@farfetched/core';
-
-const originalQuery = createQuery({ handler: async () => 'some data' });
-const attachedQuery = attachOperation(originalQuery);
-```
-
-### `attachOperation(query, { mapParams })`
-
-Creates new [_Query_](/api/primitives/query) on top of the existing one, transforming its parameters through `mapParams` function.
-
-```ts
-import { attachOperation, createQuery } from '@farfetched/core';
-
-const originalQuery = createQuery({
- handler: async (params: string) => 'some data',
-});
-
-const attachedQuery = attachOperation(originalQuery, {
- mapParams: (params: number) => params.toString(),
-});
-```
-
-### `attachOperation(query, { source, mapParams })`
-
-Creates new [_Query_](/api/primitives/query) on top of the existing one, transforming its parameters through `mapParams` function with accept a value from `source` [_Store_](https://effector.dev/en/api/effector/store/) as a second argument.
-
-```ts
-import { createStore } from 'effector';
-import { attachOperation, createQuery } from '@farfetched/core';
-
-const $externalStore = createStore(12);
-
-const originalQuery = createQuery({
- handler: async (params: string) => 'some data',
-});
-
-const attachedQuery = attachOperation(originalQuery, {
- source: $externalStore,
- mapParams: (params: number, externalSource) => (params + externalSource).toString(),
-});
-```
-
-### `attachOperation(mutation)`
-
-Creates new [_Mutation_](/api/primitives/mutation) on top of the existing one.
-
-```ts
-import { attachOperation, createMutation } from '@farfetched/core';
-
-const originalMutation = createMutation({ handler: async () => 'some data' });
-const attachedMutation = attachOperation(originalMutation);
-```
-
-### `attachOperation(mutation, { mapParams })`
-
-Creates new [_Mutation_](/api/primitives/mutation) on top of the existing one, transforming its parameters through `mapParams` function.
-
-```ts
-import { attachOperation, createMutation } from '@farfetched/core';
-
-const originaMutation = createMutation({
- handler: async (params: string) => 'some data',
-});
-
-const attachedMutation = attachOperation(originaMutation, {
- mapParams: (params: number) => params.toString(),
-});
-```
-
-### `attachOperation(mutation, { source, mapParams })`
-
-Creates new [_Mutation_](/api/primitives/mutation) on top of the existing one, transforming its parameters through `mapParams` function with accept a value from `source` [_Store_](https://effector.dev/en/api/effector/store/) as a second argument.
-
-```ts
-import { createStore } from 'effector';
-import { attachOperation, createMutation } from '@farfetched/core';
-
-const $externalStore = createStore(12);
-
-const originalMutation = createMutation({
- handler: async (params: string) => 'some data',
-});
-
-const attachedMutation = attachOperation(originalMutation, {
- source: $externalStore,
- mapParams: (params: number, externalSource) => (params + externalSource).toString(),
-});
-```
-
-## Showcases
-
-- [Real-world showcase with SolidJS around JSON API](https://github.com/igorkamyshev/farfetched/tree/master/apps/showcase-solid-real-world-rick-morty/)
diff --git a/apps/website/docs/releases/0-14.md b/apps/website/docs/releases/0-14.md
new file mode 100644
index 000000000..6c4cdc16d
--- /dev/null
+++ b/apps/website/docs/releases/0-14.md
@@ -0,0 +1,15 @@
+# v0.14
+
+Mostly about improving and cleaning the APIs of Farfetched. We are preparing for the big release v1.0, so [as promised](/roadmap), all 0.X releases will be about improving the existing features and cleaning the APIs.
+
+## Migration guide
+
+### `attachOperation` operator
+
+This operator is deprecated since [v0.12](/releases/0-12) and removed in v0.14. Please read [this ADR](/adr/attach_operation_deprecation) for more information and migration guide.
+
+### `concurrency` operator
+
+Field `concurrency` in `createJsonQuery` and `createJsonMutation` is deprecated since [v0.12](/releases/0-12) and removed in v0.14. It has to be replaced by the [`concurrency` operator](/api/operators/concurrency). Please read [this ADR](/adr/concurrency) for more information and migration guide.
+
+
diff --git a/apps/website/docs/tutorial/built_in_query_factories.md b/apps/website/docs/tutorial/built_in_query_factories.md
index 998062116..e4c80ddef 100644
--- a/apps/website/docs/tutorial/built_in_query_factories.md
+++ b/apps/website/docs/tutorial/built_in_query_factories.md
@@ -110,7 +110,6 @@ Built-in factories consider any response as `unknown` by default, so you have to
`createJsonQuery` does some additional job to make your life easier. It does the following:
- Add `Content-Type: application/json` header to the request
-- Apply `TAKE_LATEST` strategy and cancel all previous requests, you can override this behavior by passing `concurrency.strategy` option to the factory
- Parse the response as JSON
## Custom factories
diff --git a/apps/website/docs/tutorial/devtools.md b/apps/website/docs/tutorial/devtools.md
index de3d8b9ae..cde66f20c 100644
--- a/apps/website/docs/tutorial/devtools.md
+++ b/apps/website/docs/tutorial/devtools.md
@@ -56,6 +56,22 @@ if (process.env.NODE_ENV === 'development') {
That is it, now you can open Farfetched Dev Tools in your browser and see all your [_Queries_](/api/primitives/query) and its states.
+## Log all errors
+
+You can enable logging of all failed [_Queries_](/api/primitives/query) to console by passing `logErrorsToConsole` field to `attachFarfetchedDevTools` function.
+
+```ts
+// main.ts
+
+if (process.env.NODE_ENV === 'development') {
+ const { attachFarfetchedDevTools } = await import('@farfetched/dev-tools');
+
+ attachFarfetchedDevTools({
+ logErrorsToConsole: true, // [!code focus]
+ });
+}
+```
+
## Roadmap
::: tip
diff --git a/apps/website/docs/tutorial/operation_copying.md b/apps/website/docs/tutorial/operation_copying.md
index 40b6e7e80..39ffeae1e 100644
--- a/apps/website/docs/tutorial/operation_copying.md
+++ b/apps/website/docs/tutorial/operation_copying.md
@@ -4,105 +4,3 @@
This topic is considered obsolete since [v0.12](/releases/0-12) and will be removed in v0.14. Please read [this ADR](/adr/attach_operation_deprecation) for more information and migration guide.
:::
-
-> It is advanced topic, so you can write an application without it. But it is useful when you have a lot of similar _Queries_ and _Mutations_ and want to simplify your code.
-
-:::tip You will learn:
-
-- When you need to copy _Queries_ and _Mutations_
-- How to copy _Queries_ and _Mutations_
-- How to simplify your code with _Queries_ and _Mutations_ copying
-
-:::
-
-## Use-case for copying _Queries_ and _Mutations_
-
-Let's revise the example from the [previous chapters](/tutorial/dependent_queries). We have two [_Queries_](/api/primitives/query): `characterQuery` and `originQuery`. Let's imagine, we want to add a new _Query_ `currentLocationQuery` that will be similar to `originQuery`, but will fetch the current location of the character.
-
-```ts{9-15}
-const characterQuery = createQuery(/* ... */);
-
-const originQuery = createQuery({
- handler: async ({ originUrl }) => {
- const response = await fetch(originUrl);
- return response.json();
- },
-});
-
-const currentLocationQuery = createQuery({
- handler: async ({ currentLocationUrl }) => {
- const response = await fetch(currentLocationUrl);
- return response.json();
- },
-});
-```
-
-As you can see, the only difference between `originQuery` and `currentLocationQuery` is parameters, so it would be nice to have a way to copy the _Query_ and change only the parameters.
-
-## Extract base _Query_
-
-To copy the [_Query_](/api/primitives/query), we need to extract the base [_Query_](/api/primitives/query) that will be used as a template for the new ones:
-
-```ts
-const locationQuery = createQuery({
- handler: async ({ locationUrl }) => {
- const response = await fetch(locationUrl);
- return response.json();
- },
-});
-```
-
-Now, we can copy the `locationQuery` and change only the parameters:
-
-```ts{3-7,9-13}
-import { attachOperation } from '@farfetched/core'
-
-const originQuery = attachOperation(locationQuery, {
- mapParams: ({ originUrl }) => ({
- locationUrl: originUrl
- }),
-});
-
-const currentLocationQuery = attachOperation(locationQuery, {
- mapParams: ({ currentLocationUrl }) => ({
- locationUrl: currentLocationUrl
- }),
-});
-```
-
-So, `originQuery` and `currentLocationQuery` are copies of `locationQuery`, they have separate states and can be used independently.
-
-## Additional parameters from external source
-
-If you want to use additional parameters from external source, you can use the `source` field of `attachOperation` config:
-
-```ts{7}
-import { createStore } from 'effector';
-import { attachOperation } from '@farfetched/core';
-
-const $someExtarnalSource = createStore({});
-
-const currentLocationQuery = attachOperation(locationQuery, {
- source: $someExternalSource,
- mapParams: ({ currentLocationUrl }, valueOfExternalSource) => ({
- locationUrl: currentLocationUrl,
- }),
-});
-```
-
-In this case, the `valueOfExternalSource` will be equal to the current value of the `$someExternalSource` [_Store_](https://effector.dev/en/api/effector/store/).
-
-## Using `attachOperation` with _Mutations_
-
-The `attachOperation` operator can be used with [_Mutations_](/api/primitives/mutation) as well:
-
-```ts
-import { attachOperation } from '@farfetched/core';
-
-const baseMutation = createMutation(/* ... */);
-const newMutation = attachOperation(baseMutation);
-```
-
-## API reference
-
-You can find the full API reference for the `attachOperation` operator in the [API reference](/api/operators/attach_operation).
diff --git a/packages/core/package.json b/packages/core/package.json
index 681030320..77c489a7f 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -42,7 +42,7 @@
"size-limit": [
{
"path": "./dist/core.js",
- "limit": "16 kB"
+ "limit": "15.5 kB"
}
]
}
diff --git a/packages/core/src/__tests__/keep_fresh_attached.test.ts b/packages/core/src/__tests__/keep_fresh_attached.test.ts
deleted file mode 100644
index d9d0cd708..000000000
--- a/packages/core/src/__tests__/keep_fresh_attached.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { describe, test, expect, vi } from 'vitest';
-
-import { createJsonQuery } from '../query/create_json_query';
-import { keepFresh } from '../trigger_api/keep_fresh';
-import { attachOperation } from '../attach/attach';
-import { allSettled, createStore, createWatch, fork } from 'effector';
-import { unknownContract } from '../contract/unknown_contract';
-
-describe('combination of keepFresh and attachOperation', () => {
- test('re-execute attached when changes source in original', async () => {
- const handler = vi.fn().mockResolvedValue('ok');
-
- const $url = createStore('https://api.salo.com/');
-
- const originalQuery = createJsonQuery({
- request: { url: $url, method: 'GET' },
- response: { contract: unknownContract },
- });
-
- const attachedQuery = attachOperation(originalQuery);
-
- keepFresh(attachedQuery, { automatically: true });
-
- const scope = fork({ handlers: [[originalQuery.__.executeFx, handler]] });
-
- const attachedFinallyListener = vi.fn();
-
- createWatch({
- unit: attachedQuery.finished.finally,
- fn: attachedFinallyListener,
- scope,
- });
-
- await allSettled(attachedQuery.refresh, { scope });
-
- expect(attachedFinallyListener).toBeCalledTimes(1);
-
- await allSettled($url, { scope, params: 'https://api.salo.com/v2/' });
-
- expect(attachedFinallyListener).toBeCalledTimes(2);
- });
-});
diff --git a/packages/core/src/__tests__/share_cache_between_attached.test.ts b/packages/core/src/__tests__/share_cache_between_attached.test.ts
deleted file mode 100644
index dce1a59c1..000000000
--- a/packages/core/src/__tests__/share_cache_between_attached.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { allSettled, fork } from 'effector';
-import { describe, test, expect, vi } from 'vitest';
-
-import { createQuery } from '../query/create_query';
-import { attachOperation } from '../attach/attach';
-import { cache } from '../cache/cache';
-import { withFactory } from '../libs/patronus';
-
-describe('shared cache between base Query and attached one', async () => {
- test('attached query uses cache from base query', async () => {
- const baseHandler = vi.fn().mockResolvedValue('base result');
-
- const baseQuery = withFactory({
- fn: () => createQuery({ handler: baseHandler }),
- sid: '1',
- });
- cache(baseQuery, { staleAfter: '1s' });
- const attachedQuery = attachOperation(baseQuery);
-
- const scope = fork();
-
- await allSettled(attachedQuery.refresh, { scope });
- await allSettled(attachedQuery.refresh, { scope });
-
- expect(baseHandler).toBeCalledTimes(1);
- });
-});
diff --git a/packages/core/src/attach/__tests__/attach.mutation.test.ts b/packages/core/src/attach/__tests__/attach.mutation.test.ts
deleted file mode 100644
index 96b827377..000000000
--- a/packages/core/src/attach/__tests__/attach.mutation.test.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { allSettled, createStore, fork } from 'effector';
-import { describe, test, expect, vi } from 'vitest';
-
-import { watchRemoteOperation } from '../../test_utils/watch_query';
-import { unknownContract } from '../../contract/unknown_contract';
-import { fetchFx } from '../../fetch/fetch';
-import { createJsonMutation } from '../../mutation/create_json_mutation';
-
-import { createMutation } from '../../mutation/create_mutation';
-import { attachOperation } from '../attach';
-
-describe('attach for mutation', () => {
- test('execute original handler as handler', async () => {
- const originalHandler = vi
- .fn()
- .mockResolvedValue('data from original mutation');
-
- const originalMutation = createMutation({ handler: originalHandler });
- const attachedMutation = attachOperation(originalMutation);
-
- const scope = fork();
-
- const { listeners: originalListeners } = watchRemoteOperation(
- originalMutation,
- scope
- );
- const { listeners: attachedListeners } = watchRemoteOperation(
- attachedMutation,
- scope
- );
-
- await allSettled(attachedMutation.start, { scope });
-
- expect(originalHandler).toBeCalledTimes(1);
- expect(originalListeners.onSuccess).toHaveBeenCalledWith(
- expect.objectContaining({ result: 'data from original mutation' })
- );
- expect(attachedListeners.onSuccess).toHaveBeenCalledWith(
- expect.objectContaining({ result: 'data from original mutation' })
- );
- });
-
- test('attached queries do not overlap', async () => {
- const originalHandler = vi
- .fn()
- .mockResolvedValueOnce('first response')
- .mockResolvedValueOnce('second response');
-
- const originalMutation = createMutation({ handler: originalHandler });
-
- const firstMutation = attachOperation(originalMutation);
- const secondMutation = attachOperation(originalMutation);
-
- const scope = fork();
-
- const { listeners: firstListeners } = watchRemoteOperation(
- firstMutation,
- scope
- );
- const { listeners: secondListeners } = watchRemoteOperation(
- secondMutation,
- scope
- );
-
- await allSettled(firstMutation.start, { scope });
- await allSettled(secondMutation.start, { scope });
-
- expect(firstListeners.onSuccess).toHaveBeenCalledWith(
- expect.objectContaining({ result: 'first response' })
- );
- expect(secondListeners.onSuccess).toHaveBeenCalledWith(
- expect.objectContaining({ result: 'second response' })
- );
- });
-
- test('pass params from mapParams to original handler', async () => {
- const originalHandler = vi
- .fn()
- .mockResolvedValue('data from original mutation');
-
- const originalMutation = createMutation({ handler: originalHandler });
- const attachedMutation = attachOperation(originalMutation, {
- mapParams: (v: number) => v * 2,
- });
-
- const scope = fork();
-
- await allSettled(attachedMutation.start, { scope, params: 1 });
- await allSettled(attachedMutation.start, { scope, params: 2 });
-
- expect(originalHandler).toBeCalledTimes(2);
-
- expect(originalHandler).toBeCalledWith(2);
- expect(originalHandler).toBeCalledWith(4);
- });
-
- test('uses source as second arguments in mapParams', async () => {
- const originalHandler = vi
- .fn()
- .mockResolvedValue('data from original mutation');
-
- const $source = createStore(1);
-
- const originalMutation = createMutation({ handler: originalHandler });
- const attachedMutation = attachOperation(originalMutation, {
- source: $source,
- mapParams: (v: number, s) => s,
- });
-
- const scope = fork();
-
- await allSettled($source, { scope, params: 11 });
- await allSettled(attachedMutation.start, { scope, params: 1 });
-
- await allSettled($source, { scope, params: 55 });
- await allSettled(attachedMutation.start, { scope, params: 2 });
-
- expect(originalHandler).toBeCalledTimes(2);
-
- expect(originalHandler).toBeCalledWith(11);
- expect(originalHandler).toBeCalledWith(55);
- });
-
- test('supports special factories', async () => {
- const originalMutation = createJsonMutation({
- request: { method: 'GET', url: 'https://api.salo.com' },
- response: { contract: unknownContract },
- });
-
- const attachedMutation = attachOperation(originalMutation);
-
- const fetchMock = vi.fn().mockRejectedValue(new Error('cannot'));
-
- const scope = fork({ handlers: [[fetchFx, fetchMock]] });
-
- await allSettled(attachedMutation.start, { scope });
-
- expect(fetchMock).toBeCalledTimes(1);
- expect(await fetchMock.mock.calls[0][0].url).toBe('https://api.salo.com/');
- });
-});
diff --git a/packages/core/src/attach/__tests__/attach.query.test.ts b/packages/core/src/attach/__tests__/attach.query.test.ts
deleted file mode 100644
index 9cf319652..000000000
--- a/packages/core/src/attach/__tests__/attach.query.test.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-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';
-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', () => {
- test('execute original handler as handler and fill $data', async () => {
- const originalHandler = vi
- .fn()
- .mockResolvedValue('data from original query');
-
- const originalQuery = withFactory({
- fn: () => createQuery({ handler: originalHandler }),
- sid: 'base',
- });
- const attachedQuery = withFactory({
- fn: () => attachOperation(originalQuery),
- sid: 'attached',
- });
-
- const scope = fork();
-
- await allSettled(attachedQuery.start, { scope });
-
- expect(originalHandler).toBeCalledTimes(1);
- expect(scope.getState(attachedQuery.$data)).toBe(
- 'data from original query'
- );
- expect(scope.getState(originalQuery.$data)).toBe(
- 'data from original query'
- );
- });
-
- test('execute original handler as handler and fill $error', async () => {
- const originalHandler = vi
- .fn()
- .mockRejectedValue('data from original query');
-
- const originalQuery = withFactory({
- fn: () => createQuery({ handler: originalHandler }),
- sid: 'base',
- });
- const attachedQuery = withFactory({
- fn: () => attachOperation(originalQuery),
- sid: 'attached',
- });
-
- const scope = fork();
-
- await allSettled(attachedQuery.start, { scope });
-
- expect(originalHandler).toBeCalledTimes(1);
- expect(scope.getState(attachedQuery.$error)).toBe(
- 'data from original query'
- );
- expect(scope.getState(originalQuery.$error)).toBe(
- 'data from original query'
- );
- });
-
- test('attached queries do not overlap', async () => {
- const originalHandler = vi
- .fn()
- .mockResolvedValueOnce('first response')
- .mockResolvedValueOnce('second response');
-
- const originalQuery = createQuery({ handler: originalHandler });
-
- const firstQuery = attachOperation(originalQuery);
- const secondQuery = attachOperation(originalQuery);
-
- const scope = fork();
-
- await allSettled(firstQuery.start, { scope });
- await allSettled(secondQuery.start, { scope });
-
- expect(scope.getState(firstQuery.$data)).toBe('first response');
- expect(scope.getState(secondQuery.$data)).toBe('second response');
- });
-
- test('attached queries do not overlap with error', async () => {
- const err1 = new Error('cannot');
- const err2 = new Error('can not');
-
- const originalHandler = vi
- .fn()
- .mockResolvedValueOnce('first response')
- .mockImplementationOnce(() => {
- throw err1;
- })
- .mockImplementationOnce(() => {
- throw err2;
- });
-
- const originalQuery = createQuery({ handler: originalHandler });
-
- const firstQuery = attachOperation(originalQuery);
- const secondQuery = attachOperation(originalQuery);
- const thirdQuery = attachOperation(originalQuery);
-
- const scope = fork();
-
- await allSettled(firstQuery.start, { scope });
- await allSettled(secondQuery.start, { scope });
- await allSettled(thirdQuery.start, { scope });
-
- expect(scope.getState(firstQuery.$data)).toBe('first response');
- expect(scope.getState(firstQuery.$error)).toBe(null);
- expect(scope.getState(firstQuery.$failed)).toBe(false);
-
- expect(scope.getState(secondQuery.$error)).toBe(err1);
- expect(scope.getState(secondQuery.$data)).toBe(null);
- expect(scope.getState(secondQuery.$failed)).toBe(true);
-
- expect(scope.getState(thirdQuery.$error)).toBe(err2);
- expect(scope.getState(thirdQuery.$data)).toBe(null);
- expect(scope.getState(thirdQuery.$failed)).toBe(true);
- });
-
- test('pass params from mapParams to original handler', async () => {
- const originalHandler = vi
- .fn()
- .mockResolvedValue('data from original query');
-
- const originalQuery = createQuery({ handler: originalHandler });
- const attachedQuery = attachOperation(originalQuery, {
- mapParams: (v: number) => v * 2,
- });
-
- const scope = fork();
-
- await allSettled(attachedQuery.start, { scope, params: 1 });
- await allSettled(attachedQuery.start, { scope, params: 2 });
-
- expect(originalHandler).toBeCalledTimes(2);
-
- expect(originalHandler).toBeCalledWith(2);
- expect(originalHandler).toBeCalledWith(4);
- });
-
- test('uses source as second arguments in mapParams', async () => {
- const originalHandler = vi
- .fn()
- .mockResolvedValue('data from original query');
-
- const $source = createStore(1);
-
- const originalQuery = createQuery({ handler: originalHandler });
- const attachedQuery = attachOperation(originalQuery, {
- source: $source,
- mapParams: (v: number, s) => s,
- });
-
- const scope = fork();
-
- await allSettled($source, { scope, params: 11 });
- await allSettled(attachedQuery.start, { scope, params: 1 });
-
- await allSettled($source, { scope, params: 55 });
- await allSettled(attachedQuery.start, { scope, params: 2 });
-
- expect(originalHandler).toBeCalledTimes(2);
-
- expect(originalHandler).toBeCalledWith(11);
- expect(originalHandler).toBeCalledWith(55);
- });
-
- test('supports special factories', async () => {
- const originalQuery = createJsonQuery({
- request: { method: 'GET', url: 'https://api.salo.com' },
- response: { contract: unknownContract },
- });
-
- const attachedQuery = attachOperation(originalQuery);
-
- const fetchMock = vi.fn().mockRejectedValue(new Error('cannot'));
-
- const scope = fork({ handlers: [[fetchFx, fetchMock]] });
-
- await allSettled(attachedQuery.start, { scope });
-
- 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/2');
- expect(originalHandler.mock.calls[1][0].url).toBe('https://api.salo.com/1');
- });
-});
diff --git a/packages/core/src/attach/__tests__/attach.test-d.ts b/packages/core/src/attach/__tests__/attach.test-d.ts
deleted file mode 100644
index 9ca1982a9..000000000
--- a/packages/core/src/attach/__tests__/attach.test-d.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Store } from 'effector';
-import { describe, expectTypeOf, test } from 'vitest';
-
-import { Query } from '../../query/type';
-import { attachOperation } from '../attach';
-
-describe('attachOperation', () => {
- test('Query + source/mapParams, issue #280', () => {
- const query: Query = {} as any;
-
- const $source: Store = {} as any;
-
- const result = attachOperation(query, {
- source: $source,
- mapParams: (_: void, params) => params + 1,
- });
-
- expectTypeOf(result).toEqualTypeOf>();
- });
-});
diff --git a/packages/core/src/attach/attach.ts b/packages/core/src/attach/attach.ts
deleted file mode 100644
index 7a59a1e9f..000000000
--- a/packages/core/src/attach/attach.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { createStore, type Store } from 'effector';
-
-import { Mutation } from '../mutation/type';
-import { Query, QueryInitialData } from '../query/type';
-import {
- RemoteOperation,
- RemoteOperationError,
- RemoteOperationParams,
- RemoteOperationResult,
-} from '../remote_operation/type';
-
-// -- Query overloads
-
-/**
- * @deprecated Deprecated since 0.12
- * @see {@link https://ff.effector.dev/adr/attach_operation_deprecation.html#how-to-migrate}
- */
-export function attachOperation<
- NewParams,
- Q extends Query,
- Source,
->(
- operation: Q,
- config: {
- source: Store;
- mapParams: (params: NewParams, source: Source) => RemoteOperationParams;
- }
-): Query<
- NewParams,
- RemoteOperationResult,
- RemoteOperationError,
- QueryInitialData
->;
-
-/**
- * @deprecated Deprecated since 0.12
- * @see {@link https://ff.effector.dev/adr/attach_operation_deprecation.html#how-to-migrate}
- */
-export function attachOperation>(
- operation: Q,
- config: { mapParams: (params: NewParams) => RemoteOperationParams }
-): Query<
- NewParams,
- RemoteOperationResult,
- RemoteOperationError,
- QueryInitialData
->;
-
-/**
- * @deprecated Deprecated since 0.12
- * @see {@link https://ff.effector.dev/adr/attach_operation_deprecation.html#how-to-migrate}
- */
-export function attachOperation>(
- operation: Q
-): Q;
-
-// -- Mutation overloads
-
-/**
- * @deprecated Deprecated since 0.12
- * @see {@link https://ff.effector.dev/adr/attach_operation_deprecation.html#how-to-migrate}
- */
-export function attachOperation<
- NewParams,
- M extends Mutation,
- Source,
->(
- operation: M,
- config: {
- source: Store;
- mapParams: (params: NewParams, source: Source) => RemoteOperationParams;
- }
-): Mutation, RemoteOperationError>;
-
-/**
- * @deprecated Deprecated since 0.12
- * @see {@link https://ff.effector.dev/adr/attach_operation_deprecation.html#how-to-migrate}
- */
-export function attachOperation>(
- operation: M,
- config: { mapParams: (params: NewParams) => RemoteOperationParams }
-): Mutation, RemoteOperationError>;
-
-/**
- * @deprecated Deprecated since 0.12
- * @see {@link https://ff.effector.dev/adr/attach_operation_deprecation.html#how-to-migrate}
- */
-export function attachOperation>(
- operation: M
-): M;
-
-// -- Implementation
-
-/**
- * @deprecated Deprecated since 0.12
- * @see {@link https://ff.effector.dev/adr/attach_operation_deprecation.html#how-to-migrate}
- */
-export function attachOperation<
- NewParams,
- OriginalParams,
- O extends RemoteOperation,
->(
- operation: O,
- config?: {
- source?: Store;
- mapParams?: (params: NewParams, source?: any) => OriginalParams;
- }
-) {
- console.error(
- 'attachOperation is deprecated since 0.12, please read the migration guide: https://ff.effector.dev/adr/attach_operation_deprecation.html'
- );
-
- const { source, mapParams } = config ?? {};
-
- return operation.__.experimentalAPI?.attach({
- source:
- source ??
- createStore(null, {
- serialize: 'ignore',
- }),
- mapParams: mapParams ?? ((v: NewParams) => v as unknown as OriginalParams),
- });
-}
diff --git a/packages/core/src/concurrency/concurrency.ts b/packages/core/src/concurrency/concurrency.ts
index 07ead18f4..539755560 100644
--- a/packages/core/src/concurrency/concurrency.ts
+++ b/packages/core/src/concurrency/concurrency.ts
@@ -29,14 +29,6 @@ export function concurrency(
abortAll?: Event;
}
) {
- if (op.__.meta.flags.concurrencyFieldUsed) {
- console.error(
- `Both concurrency-operator and concurrency-field are used on operation ${op.__.meta.name}.`,
- `Please use only concurrency-operator, because field concurrency-field in createJsonQuery and createJsonMutation is deprecated and will be deleted soon.`
- );
- }
- op.__.meta.flags.concurrencyOperatorUsed = true;
-
const $callObjects = callObejcts(op);
if (strategy) {
diff --git a/packages/core/src/fetch/__tests__/api.response.extract.test.ts b/packages/core/src/fetch/__tests__/api.response.extract.test.ts
index ddb742bc3..b0859fd85 100644
--- a/packages/core/src/fetch/__tests__/api.response.extract.test.ts
+++ b/packages/core/src/fetch/__tests__/api.response.extract.test.ts
@@ -15,8 +15,8 @@ describe('fetch/api.response.prepare', () => {
mapBody: () => 'some_body',
};
- test('pass oriiginal response to preparation', async () => {
- const extractMock = vi.fn().mockImplementation((t) => t);
+ test('pass original response to preparation', async () => {
+ const extractMock = vi.fn().mockImplementation((t) => t.text());
const apiCallFx = createApiRequest({
request,
@@ -34,7 +34,7 @@ describe('fetch/api.response.prepare', () => {
params: {},
});
- expect(extractMock).toHaveBeenCalledWith(ORIGINAL_RESPONSE);
+ expect(extractMock).toReturnWith('ok');
});
});
diff --git a/packages/core/src/fetch/api.ts b/packages/core/src/fetch/api.ts
index 26d8f57a6..5a69e38e8 100644
--- a/packages/core/src/fetch/api.ts
+++ b/packages/core/src/fetch/api.ts
@@ -1,7 +1,7 @@
import { attach, createEffect } from 'effector';
import { normalizeStaticOrReactive, StaticOrReactive } from '../libs/patronus';
-import { NonOptionalKeys } from '../libs/lohyphen';
+import { drain, NonOptionalKeys } from '../libs/lohyphen';
import {
ConfigurationError,
HttpError,
@@ -153,15 +153,21 @@ export function createApiRequest<
throw cause;
});
- // We cannot read body of the response twice (prepareFx and throw preparationError)
- const clonedResponse = response.clone();
+ const [forPrepare, forError] = response.body?.tee() ?? [null, null];
- const prepared = await prepareFx(response).catch(async (cause) => {
- throw preparationError({
- response: await clonedResponse.text(),
- reason: cause?.message ?? null,
- });
- });
+ const prepared = await prepareFx(new Response(forPrepare, response)).then(
+ async (result) => {
+ await drain(forError);
+
+ return result;
+ },
+ async (cause) => {
+ throw preparationError({
+ response: await new Response(forError).text(),
+ reason: cause?.message ?? null,
+ });
+ }
+ );
if (config.response.status) {
const expected = Array.isArray(config.response.status.expected)
diff --git a/packages/core/src/fetch/json.ts b/packages/core/src/fetch/json.ts
index f8692c355..7967a3830 100644
--- a/packages/core/src/fetch/json.ts
+++ b/packages/core/src/fetch/json.ts
@@ -12,6 +12,7 @@ import {
StaticOnlyRequestConfig,
} from './api';
import { mergeRecords } from './lib';
+import { drain } from 'libs/lohyphen';
export type JsonObject = Record;
@@ -66,13 +67,14 @@ export function createJsonApiRequest(
},
response: {
extract: async (response) => {
- const emptyContent = await isEmptyResponse(response);
+ const [emptyContent, nonEmptyResponse] =
+ await checkEmptyResponse(response);
if (emptyContent) {
return null;
}
- return response.json();
+ return nonEmptyResponse.json();
},
transformError: (error) => {
if (!isHttpError({ error })) {
@@ -104,23 +106,26 @@ export function createJsonApiRequest(
return jsonApiCallFx;
}
-async function isEmptyResponse(response: Response): Promise {
+async function checkEmptyResponse(
+ response: Response
+): Promise<[true, null] | [false, Response]> {
if (!response.body) {
- return true;
+ return [true, null];
}
const headerAsEmpty = response.headers.get('Content-Length') === '0';
if (headerAsEmpty) {
- return true;
+ return [true, null];
}
- // Clone response to read it
- // because response can be read only once
- const clonnedResponse = response.clone();
- const bodyAsText = await clonnedResponse.text();
+ const [originalBody, clonedBody] = response.body.tee();
+
+ const bodyAsText = await new Response(clonedBody).text();
if (bodyAsText.length === 0) {
- return true;
+ await drain(originalBody);
+
+ return [true, null];
}
- return false;
+ return [false, new Response(originalBody, response)];
}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 5eb6a15d1..021865719 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -25,9 +25,6 @@ export { timeout } from './timeout/timeout';
// Update public API
export { update } from './update/update';
-// Attach public API
-export { attachOperation } from './attach/attach';
-
// Cache public API
export { cache } from './cache/cache';
export {
@@ -39,6 +36,7 @@ export { localStorageCache } from './cache/adapters/local_storage';
export { sessionStorageCache } from './cache/adapters/session_storage';
export { voidCache } from './cache/adapters/void';
export { createCacheAdapter } from './cache/adapters/instance';
+export { attachObservability } from './cache/adapters/observability';
// Exposed libs
export {
@@ -58,6 +56,7 @@ export {
type RemoteOperationResult,
type RemoteOperationError,
type RemoteOperationParams,
+ type ExecutionMeta,
} from './remote_operation/type';
export { onAbort } from './remote_operation/on_abort';
export { Meta, Result } from './remote_operation/store_meta';
diff --git a/packages/core/src/libs/lohyphen/drain.ts b/packages/core/src/libs/lohyphen/drain.ts
new file mode 100644
index 000000000..6322e6ce6
--- /dev/null
+++ b/packages/core/src/libs/lohyphen/drain.ts
@@ -0,0 +1,3 @@
+export function drain(stream: ReadableStream | null) {
+ return stream?.pipeTo(new WritableStream({ write() {} })).catch(() => {});
+}
diff --git a/packages/core/src/libs/lohyphen/index.ts b/packages/core/src/libs/lohyphen/index.ts
index 467a35f07..e5e81d176 100644
--- a/packages/core/src/libs/lohyphen/index.ts
+++ b/packages/core/src/libs/lohyphen/index.ts
@@ -9,3 +9,4 @@ export { divide } from './divide';
export { get } from './field';
export { uniq } from './uniq';
export { Mutex } from './mutex';
+export { drain } from './drain';
diff --git a/packages/core/src/mutation/create_headless_mutation.ts b/packages/core/src/mutation/create_headless_mutation.ts
index cd843ed16..868ece8cf 100644
--- a/packages/core/src/mutation/create_headless_mutation.ts
+++ b/packages/core/src/mutation/create_headless_mutation.ts
@@ -1,5 +1,3 @@
-import { attach, type Store } from 'effector';
-
import { createRemoteOperation } from '../remote_operation/create_remote_operation';
import {
readonly,
@@ -11,7 +9,6 @@ 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 {
name?: string;
@@ -68,43 +65,6 @@ export function createHeadlessMutation<
};
const unitShapeProtocol = () => unitShape;
- // Experimental API, won't be exposed as protocol for now
- const attachProtocol = ({
- source,
- mapParams,
- }: {
- source: Store;
- mapParams: (params: NewParams, source: Source) => Params;
- }) => {
- const attachedMutation = createHeadlessMutation<
- NewParams,
- Data,
- ContractData,
- MappedData,
- unknown,
- MapDataSource,
- ValidationSource
- >(config as any);
-
- attachedMutation.__.lowLevelAPI.dataSourceRetrieverFx.use(
- attach({
- source,
- mapParams: (
- { params, ...rest }: { params: NewParams; meta: ExecutionMeta },
- sourceValue
- ): { params: Params; meta: ExecutionMeta } => ({
- params: (mapParams
- ? mapParams(params, sourceValue)
- : params) as Params,
- ...rest,
- }),
- effect: operation.__.lowLevelAPI.dataSourceRetrieverFx,
- })
- );
-
- return attachedMutation;
- };
-
// -- Public API --
return {
@@ -125,7 +85,7 @@ export function createHeadlessMutation<
finally: readonly(operation.finished.finally),
skip: readonly(operation.finished.skip),
},
- __: { ...operation.__, experimentalAPI: { attach: attachProtocol } },
+ __: operation.__,
'@@unitShape': unitShapeProtocol,
};
}
diff --git a/packages/core/src/mutation/create_json_mutation.ts b/packages/core/src/mutation/create_json_mutation.ts
index 58e823479..702fbf262 100644
--- a/packages/core/src/mutation/create_json_mutation.ts
+++ b/packages/core/src/mutation/create_json_mutation.ts
@@ -1,4 +1,4 @@
-import { attach, type Json, type Event, createEffect } from 'effector';
+import { attach, type Json, createEffect } from 'effector';
import { type Contract } from '../contract/type';
import { unknownContract } from '../contract/unknown_contract';
@@ -17,16 +17,11 @@ import {
type SharedMutationFactoryConfig,
} from './create_headless_mutation';
import { type Mutation } from './type';
-import { concurrency } from '../concurrency/concurrency';
import { onAbort } from '../remote_operation/on_abort';
import { Meta, Result } from '../remote_operation/store_meta';
// -- Shared --
-type ConcurrencyConfig = {
- abort?: Event;
-};
-
type RequestConfig =
{
url: SourcedField;
@@ -59,11 +54,6 @@ interface BaseJsonMutationConfigNoParams<
HeadersSource,
UrlSource
>;
- /**
- * @deprecated Deprecated since 0.12, use `concurrency` operator instead
- * @see {@link https://ff.effector.dev/adr/concurrency}
- */
- concurrency?: ConcurrencyConfig;
}
interface BaseJsonMutationConfigWithParams<
@@ -82,11 +72,6 @@ interface BaseJsonMutationConfigWithParams<
HeadersSource,
UrlSource
>;
- /**
- * @deprecated Deprecated since 0.12, use `concurrency` operator instead
- * @see {@link https://ff.effector.dev/adr/concurrency}
- */
- concurrency?: ConcurrencyConfig;
}
// -- Overloads
@@ -263,30 +248,8 @@ export function createJsonMutation(config: any): Mutation {
})
);
- const op = {
+ return {
...headlessMutation,
__: { ...headlessMutation.__, executeFx },
};
-
- /* TODO: in future releases we will remove this code and make concurrency a separate function */
- if (config.concurrency) {
- console.error(
- 'concurrency field in createJsonMutation is deprecated, please use concurrency operator instead: https://ff.effector.dev/adr/concurrency.html'
- );
-
- op.__.meta.flags.concurrencyFieldUsed = true;
- }
-
- if (config.concurrency) {
- setTimeout(() => {
- if (!op.__.meta.flags.concurrencyOperatorUsed) {
- concurrency(op, {
- strategy: config.concurrency?.strategy ?? 'TAKE_EVERY',
- abortAll: config.concurrency?.abort,
- });
- }
- });
- }
-
- return op;
}
diff --git a/packages/core/src/query/__tests__/query.started.test.ts b/packages/core/src/query/__tests__/query.started.test.ts
index 840354da1..8ddac6cb7 100644
--- a/packages/core/src/query/__tests__/query.started.test.ts
+++ b/packages/core/src/query/__tests__/query.started.test.ts
@@ -1,11 +1,9 @@
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 () => {
@@ -34,28 +32,4 @@ describe('Query#started', () => {
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/query/create_headless_query.ts b/packages/core/src/query/create_headless_query.ts
index f6ed74580..83ba34817 100644
--- a/packages/core/src/query/create_headless_query.ts
+++ b/packages/core/src/query/create_headless_query.ts
@@ -4,7 +4,6 @@ import {
createStore,
sample,
createEvent,
- attach,
split,
withRegion,
} from 'effector';
@@ -23,7 +22,6 @@ import {
} from '../libs/patronus';
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 { createMetaNode } from '../inspect';
@@ -213,44 +211,6 @@ export function createHeadlessQuery<
};
const unitShapeProtocol = () => unitShape;
- // Experimental API, won't be exposed as protocol for now
- const attachProtocol = ({
- source,
- mapParams,
- }: {
- source: Store;
- mapParams: (params: NewParams, source: Source) => Params;
- }) => {
- const attachedQuery = createHeadlessQuery<
- NewParams,
- Response,
- unknown,
- ContractData,
- MappedData,
- MapDataSource,
- ValidationSource,
- Initial
- >(config as any);
-
- attachedQuery.__.lowLevelAPI.dataSourceRetrieverFx.use(
- attach({
- source,
- mapParams: (
- { params, ...rest }: { params: NewParams; meta: ExecutionMeta },
- sourceValue
- ): { params: Params; meta: ExecutionMeta } => ({
- params: (mapParams
- ? mapParams(params, sourceValue)
- : params) as Params,
- ...rest,
- }),
- effect: operation.__.lowLevelAPI.dataSourceRetrieverFx,
- })
- );
-
- return attachedQuery;
- };
-
// -- Public API --
const metaNode = createMetaNode(
@@ -284,7 +244,6 @@ export function createHeadlessQuery<
__: {
...operation.__,
lowLevelAPI: { ...operation.__.lowLevelAPI, refreshSkipDueToFreshness },
- experimentalAPI: { attach: attachProtocol },
},
'@@unitShape': unitShapeProtocol,
};
diff --git a/packages/core/src/query/create_json_query.ts b/packages/core/src/query/create_json_query.ts
index 65c897bfd..013380b14 100644
--- a/packages/core/src/query/create_json_query.ts
+++ b/packages/core/src/query/create_json_query.ts
@@ -1,4 +1,4 @@
-import { attach, createEffect, type Event, type Json } from 'effector';
+import { attach, createEffect, type Json } from 'effector';
import { type Contract } from '../contract/type';
import { createJsonApiRequest } from '../fetch/json';
@@ -17,17 +17,11 @@ import {
} from './create_headless_query';
import { unknownContract } from '../contract/unknown_contract';
import { type Validator } from '../validation/type';
-import { concurrency } from '../concurrency/concurrency';
import { onAbort } from '../remote_operation/on_abort';
import { Result, Meta } from '../remote_operation/store_meta';
// -- Shared
-type ConcurrencyConfig = {
- strategy?: 'TAKE_EVERY' | 'TAKE_FIRST' | 'TAKE_LATEST';
- abort?: Event;
-};
-
type RequestConfig =
{
url: SourcedField;
@@ -60,11 +54,6 @@ interface BaseJsonQueryConfigNoParams<
HeadersSource,
UrlSource
>;
- /**
- * @deprecated Deprecated since 0.12, use `concurrency` operator instead
- * @see {@link https://ff.effector.dev/adr/concurrency}
- */
- concurrency?: ConcurrencyConfig;
}
interface BaseJsonQueryConfigWithParams<
@@ -83,11 +72,6 @@ interface BaseJsonQueryConfigWithParams<
HeadersSource,
UrlSource
>;
- /**
- * @deprecated Deprecated since 0.12, use `concurrency` operator instead
- * @see {@link https://ff.effector.dev/adr/concurrency}
- */
- concurrency?: ConcurrencyConfig;
}
// -- Overloads
@@ -391,32 +375,8 @@ export function createJsonQuery(config: any) {
})
);
- const op = {
+ return {
...headlessQuery,
__: { ...headlessQuery.__, executeFx },
};
-
- /* TODO: in future releases we will remove this code and make concurrency a separate function */
- if (config.concurrency) {
- console.error(
- 'concurrency field in createJsonQuery is deprecated, please use concurrency operator instead: https://ff.effector.dev/adr/concurrency.html'
- );
-
- op.__.meta.flags.concurrencyFieldUsed = true;
- }
-
- setTimeout(() => {
- if (!op.__.meta.flags.concurrencyOperatorUsed) {
- console.error(
- 'Please apply concurrency operator to the query, read more: https://ff.effector.dev/adr/concurrency.html'
- );
-
- concurrency(op, {
- strategy: config.concurrency?.strategy ?? 'TAKE_LATEST',
- abortAll: config.concurrency?.abort,
- });
- }
- });
-
- return op;
}
diff --git a/packages/core/src/remote_operation/type.ts b/packages/core/src/remote_operation/type.ts
index 1e0898f1a..eea0c578c 100644
--- a/packages/core/src/remote_operation/type.ts
+++ b/packages/core/src/remote_operation/type.ts
@@ -129,12 +129,6 @@ export interface RemoteOperation<
meta: ExecutionMeta;
}>;
} & ExtraLowLevelAPI;
- experimentalAPI?: {
- attach: (config: {
- source: Store;
- mapParams: (params: NewParams, source: Source) => Params;
- }) => any;
- };
};
}
diff --git a/packages/dev-tools/src/App.vue b/packages/dev-tools/src/App.vue
index e26dbe278..5b3581f5a 100644
--- a/packages/dev-tools/src/App.vue
+++ b/packages/dev-tools/src/App.vue
@@ -4,6 +4,7 @@ import { useUnit, useVModel } from 'effector-vue/composition';
import { $visible, show, hide } from './view-model/visibility';
import { operationHeaders, $operationsList } from './view-model/list';
import { $search } from './view-model/search';
+import './view-model/console';
import FloatingButton from './ui/FloatingButton.vue';
import Modal from './ui/Modal.vue';
import Table from './ui/Table.vue';
diff --git a/packages/dev-tools/src/index.ts b/packages/dev-tools/src/index.ts
index 1238c50a2..644a8cb30 100644
--- a/packages/dev-tools/src/index.ts
+++ b/packages/dev-tools/src/index.ts
@@ -8,6 +8,7 @@ import { appStarted } from './model/init';
export function attachFarfetchedDevTools(config?: {
scope?: Scope;
element?: Element;
+ logErrorsToConsole?: boolean;
}) {
let root: Element;
if (config?.element) {
@@ -17,7 +18,10 @@ export function attachFarfetchedDevTools(config?: {
document.body.appendChild(root);
}
- appStarted({ scope: config?.scope });
+ appStarted({
+ scope: config?.scope,
+ config: { logErrorsToConsole: config?.logErrorsToConsole ?? false },
+ });
createApp(App).mount(root);
}
diff --git a/packages/dev-tools/src/model/init.ts b/packages/dev-tools/src/model/init.ts
index 846e26269..bc4743e9e 100644
--- a/packages/dev-tools/src/model/init.ts
+++ b/packages/dev-tools/src/model/init.ts
@@ -1,3 +1,6 @@
import { type Scope, createEvent } from 'effector';
-export const appStarted = createEvent<{ scope?: Scope }>();
+export const appStarted = createEvent<{
+ scope?: Scope;
+ config: { logErrorsToConsole: boolean };
+}>();
diff --git a/packages/dev-tools/src/view-model/console.ts b/packages/dev-tools/src/view-model/console.ts
new file mode 100644
index 000000000..9be34ea61
--- /dev/null
+++ b/packages/dev-tools/src/view-model/console.ts
@@ -0,0 +1,60 @@
+import { createEffect, createStore, sample } from 'effector';
+
+import { appStarted } from '../model/init';
+import { $operations } from '../model/operations';
+import { $errors, $data, $stauses, newError } from '../model/states';
+import { createOperationViewModel } from './operation';
+
+const $logErrorsToConsole = createStore(false).on(
+ appStarted,
+ (_, { config }) => config.logErrorsToConsole
+);
+
+const logged = new Set();
+
+const logErrorFx = createEffect(
+ (err: { text: string; content?: unknown } | null) => {
+ if (!err || !err.content) {
+ return;
+ }
+
+ if (logged.has(err.text)) {
+ return;
+ }
+
+ logged.add(err.text);
+
+ console.error(err.text, err.content);
+ }
+);
+
+sample({
+ clock: newError,
+ filter: $logErrorsToConsole,
+ source: {
+ operations: $operations,
+ statuses: $stauses,
+ data: $data,
+ errors: $errors,
+ },
+ fn: ({ operations, statuses, data, errors }, err) => {
+ const operation = operations.find((operation) => operation.id === err.key);
+
+ if (!operation) {
+ return null;
+ }
+
+ const operationViewModel = createOperationViewModel({
+ operation,
+ statuses,
+ data,
+ errors,
+ });
+
+ return {
+ text: `[${operationViewModel.type}] ${operationViewModel.name}`,
+ content: operationViewModel.error,
+ };
+ },
+ target: logErrorFx,
+});
diff --git a/packages/dev-tools/src/view-model/list.ts b/packages/dev-tools/src/view-model/list.ts
index 1853a026b..158285783 100644
--- a/packages/dev-tools/src/view-model/list.ts
+++ b/packages/dev-tools/src/view-model/list.ts
@@ -1,8 +1,9 @@
import { combine } from 'effector';
-import { $operations, getFarfetchedMeta } from '../model/operations';
+import { $operations } from '../model/operations';
import { $stauses, $data, $errors } from '../model/states';
import { $search } from './search';
+import { createOperationViewModel, overlap } from './operation';
export const operationHeaders = ['Type', 'Name', 'Status', 'Data', 'Error'];
@@ -16,20 +17,14 @@ export const $operationsList = combine(
},
({ operations, statuses, data, errors, search }) =>
operations
- .map((operation) => {
- const meta = getFarfetchedMeta(operation);
- const status = statuses[operation.id] ?? 'unknown';
- const dataItem = data[operation.id] ?? null;
- const errorItem = errors[operation.id] ?? null;
-
- return {
- type: meta.type,
- name: meta.name ?? getFactoryName(operation) ?? 'unnamed',
- status,
- data: dataItem,
- error: errorItem,
- };
- })
+ .map((operation) =>
+ createOperationViewModel({
+ operation,
+ statuses,
+ data,
+ errors,
+ })
+ )
.filter((item) => overlap(search, item.name))
.map((item) => {
return [
@@ -41,18 +36,3 @@ export const $operationsList = combine(
];
})
);
-
-function getFactoryName(node: any) {
- return node?.region?.region?.meta?.name;
-}
-
-function overlap(search?: string, name?: string): boolean {
- if (!search || !name) return true;
-
- if (search.length === 0) return true;
-
- const normalSearch = search.toLowerCase();
- const normalName = name.toLowerCase();
-
- return normalSearch.includes(normalName) || normalName.includes(normalSearch);
-}
diff --git a/packages/dev-tools/src/view-model/operation.ts b/packages/dev-tools/src/view-model/operation.ts
new file mode 100644
index 000000000..9f14f0656
--- /dev/null
+++ b/packages/dev-tools/src/view-model/operation.ts
@@ -0,0 +1,41 @@
+import { FarfetchedDeclaration, getFarfetchedMeta } from '../model/operations';
+
+export function createOperationViewModel({
+ operation,
+ statuses,
+ data,
+ errors,
+}: {
+ operation: FarfetchedDeclaration;
+ statuses: Record;
+ data: Record;
+ errors: Record;
+}) {
+ const meta = getFarfetchedMeta(operation);
+ const status = statuses[operation.id] ?? 'unknown';
+ const dataItem = data[operation.id] ?? null;
+ const errorItem = errors[operation.id] ?? null;
+
+ return {
+ type: meta.type,
+ name: meta.name ?? getFactoryName(operation) ?? 'unnamed',
+ status,
+ data: dataItem,
+ error: errorItem,
+ };
+}
+
+export function getFactoryName(node: any) {
+ return node?.region?.region?.meta?.name;
+}
+
+export function overlap(search?: string, name?: string): boolean {
+ if (!search || !name) return true;
+
+ if (search.length === 0) return true;
+
+ const normalSearch = search.toLowerCase();
+ const normalName = name.toLowerCase();
+
+ return normalSearch.includes(normalName) || normalName.includes(normalSearch);
+}