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); +}