From faa19ec570833d322f7e2869dcb004a7a78af32f Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Tue, 18 Apr 2023 20:01:46 +0700 Subject: [PATCH] Mark read-only _Events_ and _Stores_ with `readonly` --- .changeset/shy-avocados-love.md | 5 + apps/website/docs/api/primitives/mutation.md | 78 +++++++++--- apps/website/docs/api/primitives/query.md | 116 +++++++++++++----- apps/website/docs/releases/0-9.md | 6 + packages/core/src/cache/key/key.ts | 8 +- packages/core/src/libs/patronus/index.ts | 1 + packages/core/src/libs/patronus/readonly.ts | 10 ++ .../src/mutation/create_headless_mutation.ts | 31 +++-- .../core/src/query/create_headless_query.ts | 60 +++++++-- packages/core/src/query/type.ts | 1 + .../create_remote_operation.ts | 7 +- packages/core/src/remote_operation/type.ts | 2 + packages/core/src/update/update.ts | 4 +- 13 files changed, 249 insertions(+), 80 deletions(-) create mode 100644 .changeset/shy-avocados-love.md create mode 100644 packages/core/src/libs/patronus/readonly.ts diff --git a/.changeset/shy-avocados-love.md b/.changeset/shy-avocados-love.md new file mode 100644 index 000000000..c60484dba --- /dev/null +++ b/.changeset/shy-avocados-love.md @@ -0,0 +1,5 @@ +--- +'@farfetched/core': minor +--- + +Mark read-only _Events_ and _Stores_ with `readonly` diff --git a/apps/website/docs/api/primitives/mutation.md b/apps/website/docs/api/primitives/mutation.md index f22d93d82..eec0d9dac 100644 --- a/apps/website/docs/api/primitives/mutation.md +++ b/apps/website/docs/api/primitives/mutation.md @@ -1,30 +1,68 @@ +--- +outline: [2, 3] +--- + # Mutation Representation of a mutation of remote data. -## API reference +## Commands + +This section describes the [_Event_](https://effector.dev/docs/api/effector/event) that can be used to perform actions on the _Mutation_. Commands should be called in application code. + +### `start` + +Unconditionally starts the _Mutation_ with the given parameters. + +## Stores + +This section describes the [_Stores_](https://effector.dev/docs/api/effector/store) that can be used to read the _Mutation_ state. + +### `$status` + +[_Store_](https://effector.dev/docs/api/effector/store) with the current status of the _Mutation_. It must not be changed directly. Can be one of the following values: `"initial"`, `"pending"`, `"done"`, `"fail"`. + +For convenience, there are also the following [_Stores_](https://effector.dev/docs/api/effector/store): + +- `$idle` — `true` if the _Mutation_ is in the `"initial"` state, `false` otherwise. +- `$pending` — `true` if the _Mutation_ is in the `"pending"` state, `false` otherwise. +- `$failed` — `true` if the _Mutation_ is in the `"fail"` state, `false` otherwise. +- `$succeeded` — `true` if the _Mutation_ is in the `"done"` state, `false` otherwise. + +### `$enabled` + +[_Store_](https://effector.dev/docs/api/effector/store) with the current enabled state of the _Mutation_. Disabled _Mutations_ will not be executed, instead, they will be treated as skipped. It must not be changed directly. Can be `true` or `false`. + +## Events + +This section describes the [_Event_](https://effector.dev/docs/api/effector/event) that can be used to listen to the _Mutation_ state changes. Events must not be called in application code. + +### `finished.success` + +[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is finished with success. Payload will contain the object with the following fields: + +- `params` with the parameters that were used to start the _Mutation_ +- `result` with the result of the _Mutation_ +- `meta` with the execution metadata + +### `finished.failure` + +[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is finished with failure. Payload will contain the object with the following fields: + +- `params` with the parameters that were used to start the _Mutation_ +- `error` with the error of the _Mutation_ +- `meta` with the execution metadata -```ts -const mutation: Mutation; +### `finished.skip` -// Stores -mutation.$status; // Store<'initial' | 'pending' | 'done' | 'fail'> -mutation.$idle; // Store, since v0.8.0 -mutation.$pending; // Store -mutation.$failed; // Store -mutation.$succeeded; // Store -mutation.$enabled; // Store +[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is skipped. Payload will contain the object with the following fields: -// Commands -mutation.start; // Event; +- `params` with the parameters that were used to start the _Mutation_ +- `meta` with the execution metadata -// Events -mutation.finished.success; // Event; -mutation.finished.failure; // Event; -mutation.finished.skip; // Event; -mutation.finished.finally; // Event; +### `finished.finally` -// Note: Store and Event are imported from 'effector' package -``` +[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is finished with success, failure or skip. Payload will contain the object with the following fields: -More information about API can be found in [the source code](https://github.com/igorkamyshev/farfetched/blob/master/packages/core/src/mutation/type.ts). +- `params` with the parameters that were used to start the _Mutation_ +- `meta` with the execution metadata diff --git a/apps/website/docs/api/primitives/query.md b/apps/website/docs/api/primitives/query.md index 6a1559a92..1b1c4a1ef 100644 --- a/apps/website/docs/api/primitives/query.md +++ b/apps/website/docs/api/primitives/query.md @@ -1,36 +1,88 @@ +--- +outline: [2, 3] +--- + # Query Representation of a piece of remote data. -## API reference - -```ts -const query: Query; -const query: Query; // InitialData is allowed since v0.3.0 - -// Stores -query.$data; // Store -query.$error; // Store -query.$status; // Store<'initial' | 'pending' | 'done' | 'fail'> -query.$idle; // Store, since v0.8.0 -query.$pending; // Store -query.$failed; // Store, since v0.2.0 -query.$succeeded; // Store, since v0.2.0 -query.$enabled; // Store -query.$stale; // Store - -// Commands -query.start; // Event -query.reset; // Event, since v0.2.0 -query.refresh; // Event. since v0.8.0 - -// Events -query.finished.success; // Event<{ result: Data, params: Params }> -query.finished.failure; // Event<{ error: Error, params: Params }> -query.finished.skip; // Event<{ params: Params }> -query.finished.finally; // Event<{ params: Params }> - -// Note: Store and Event are imported from 'effector' package -``` - -More information about API can be found in [the source code](https://github.com/igorkamyshev/farfetched/blob/master/packages/core/src/query/type.ts). +## Commands + +This section describes the [_Event_](https://effector.dev/docs/api/effector/event) that can be used to perform actions on the _Query_. Commands should be called in application code. + +### `start` + +Unconditionally starts the _Query_ with the given parameters. + +### `refresh` + +Starts the _Query_ with the given parameters if it is `$stale`. Otherwise, it will be treated as skipped. + +### `reset` + +Resets the _Query_ to the initial state. + +## Stores + +This section describes the [_Stores_](https://effector.dev/docs/api/effector/store) that can be used to read the _Query_ state. + +### `$data` + +[_Store_](https://effector.dev/docs/api/effector/store) with the latest data. It must not be changed directly. In case of error, it will contain the initial data. + +### `$error` + +[_Store_](https://effector.dev/docs/api/effector/store) with the latest error. It must not be changed directly. In case of success, it will contain `null`. + +### `$status` + +[_Store_](https://effector.dev/docs/api/effector/store) with the current status of the _Query_. It must not be changed directly. Can be one of the following values: `"initial"`, `"pending"`, `"done"`, `"fail"`. + +For convenience, there are also the following [_Stores_](https://effector.dev/docs/api/effector/store): + +- `$idle` — `true` if the _Query_ is in the `"initial"` state, `false` otherwise. +- `$pending` — `true` if the _Query_ is in the `"pending"` state, `false` otherwise. +- `$failed` — `true` if the _Query_ is in the `"fail"` state, `false` otherwise. +- `$succeeded` — `true` if the _Query_ is in the `"done"` state, `false` otherwise. + +### `$enabled` + +[_Store_](https://effector.dev/docs/api/effector/store) with the current enabled state of the _Query_. Disabled queries will not be executed, instead, they will be treated as skipped. It must not be changed directly. Can be `true` or `false`. + +### `$stale` + +[_Store_](https://effector.dev/docs/api/effector/store) with the current stale state of the _Query_. Stale queries will be executed on the next call to `refresh` [_Event_](https://effector.dev/docs/api/effector/event). It must not be changed directly. Can be `true` or `false`. + +## Events + +This section describes the [_Event_](https://effector.dev/docs/api/effector/event) that can be used to listen to the _Query_ state changes. Events must not be called in application code. + +### `finished.success` + +[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is finished with success. Payload will contain the object with the following fields: + +- `params` with the parameters that were used to start the _Query_ +- `result` with the result of the _Query_ +- `meta` with the execution metadata + +### `finished.failure` + +[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is finished with failure. Payload will contain the object with the following fields: + +- `params` with the parameters that were used to start the _Query_ +- `error` with the error of the _Query_ +- `meta` with the execution metadata + +### `finished.skip` + +[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is skipped. Payload will contain the object with the following fields: + +- `params` with the parameters that were used to start the _Query_ +- `meta` with the execution metadata + +### `finished.finally` + +[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is finished with success, failure or skip. Payload will contain the object with the following fields: + +- `params` with the parameters that were used to start the _Query_ +- `meta` with the execution metadata diff --git a/apps/website/docs/releases/0-9.md b/apps/website/docs/releases/0-9.md index 30f6d97cf..b4981c414 100644 --- a/apps/website/docs/releases/0-9.md +++ b/apps/website/docs/releases/0-9.md @@ -4,4 +4,10 @@ `externalCache` adapter was deprecated in [0.8](/releases/0-8), write your own adapter instead [by recipe](/recipes/server_cache). +### Read-only [_Stores_](https://effector.dev/docs/api/effector/store) and [_Events_](https://effector.dev/docs/api/effector/event) + +[_Events_](https://effector.dev/docs/api/effector/event) `finished.*` have never been supposed to be called in application code. Now they are read-only. In case you call them, you will get a warning in console in Effector 22 and exception in Effector 23. + +[_Stores_](https://effector.dev/docs/api/effector/store) `$data`, `$error`, `$status`, `$idle`, `$pending`, `$succeeded`, `$failed`, `$enabled` have never been supposed to be changed in application code directly. Now they are read-only. In case you change them, you will get a warning in console in Effector 22 and exception in Effector 23. + diff --git a/packages/core/src/cache/key/key.ts b/packages/core/src/cache/key/key.ts index 6ae36e0fb..615650d9d 100644 --- a/packages/core/src/cache/key/key.ts +++ b/packages/core/src/cache/key/key.ts @@ -39,13 +39,7 @@ export function queryUniqId(query: Query) { } function querySid(query: Query): string | null { - const sid = query.$data.sid; - - if (!sid?.includes('|')) { - return null; - } - - return sid; + return query.__.meta.sid ?? null; } const prevNames = new Set(); diff --git a/packages/core/src/libs/patronus/index.ts b/packages/core/src/libs/patronus/index.ts index 287f7cdae..64de679da 100644 --- a/packages/core/src/libs/patronus/index.ts +++ b/packages/core/src/libs/patronus/index.ts @@ -18,3 +18,4 @@ export { export { type FetchingStatus } from './status'; export { time } from './time'; export { and } from './and'; +export { readonly } from './readonly'; diff --git a/packages/core/src/libs/patronus/readonly.ts b/packages/core/src/libs/patronus/readonly.ts new file mode 100644 index 000000000..26efce605 --- /dev/null +++ b/packages/core/src/libs/patronus/readonly.ts @@ -0,0 +1,10 @@ +import { Event, Store } from 'effector'; + +export function readonly(store: Store): Store; +export function readonly(event: Event): Event; + +export function readonly( + storeOrEvent: Store | Event +): Store | Event { + return storeOrEvent.map((v) => v); +} diff --git a/packages/core/src/mutation/create_headless_mutation.ts b/packages/core/src/mutation/create_headless_mutation.ts index 440cc8f4b..e354deb24 100644 --- a/packages/core/src/mutation/create_headless_mutation.ts +++ b/packages/core/src/mutation/create_headless_mutation.ts @@ -1,10 +1,15 @@ +import { attach, type Store } from 'effector'; + import { createRemoteOperation } from '../remote_operation/create_remote_operation'; -import { DynamicallySourcedField, StaticOrReactive } from '../libs/patronus'; -import { Mutation, MutationSymbol } from './type'; -import { Contract } from '../contract/type'; -import { InvalidDataError } from '../errors/type'; -import { Validator } from '../validation/type'; -import { attach, Store } from 'effector'; +import { + type DynamicallySourcedField, + readonly, + type StaticOrReactive, +} from '../libs/patronus'; +import { type Mutation, MutationSymbol } from './type'; +import { type Contract } from '../contract/type'; +import { type InvalidDataError } from '../errors/type'; +import { type Validator } from '../validation/type'; export interface SharedMutationFactoryConfig { name?: string; @@ -100,7 +105,19 @@ export function createHeadlessMutation< // -- Public API -- return { - ...operation, + start: operation.start, + $status: readonly(operation.$status), + $idle: readonly(operation.$idle), + $pending: readonly(operation.$pending), + $succeeded: readonly(operation.$succeeded), + $failed: readonly(operation.$failed), + $enabled: readonly(operation.$enabled), + finished: { + success: readonly(operation.finished.success), + failure: readonly(operation.finished.failure), + finally: readonly(operation.finished.finally), + skip: readonly(operation.finished.skip), + }, __: { ...operation.__, experimentalAPI: { attach: attachProtocol } }, '@@unitShape': unitShapeProtocol, }; diff --git a/packages/core/src/query/create_headless_query.ts b/packages/core/src/query/create_headless_query.ts index 203a28af9..c16e2e7e6 100644 --- a/packages/core/src/query/create_headless_query.ts +++ b/packages/core/src/query/create_headless_query.ts @@ -1,7 +1,8 @@ -import { createStore, sample, createEvent, Store, attach } from 'effector'; +import { createStore, sample, createEvent, type Store, attach } from 'effector'; +import { type Event } from 'effector'; -import { Contract } from '../contract/type'; -import { InvalidDataError } from '../errors/type'; +import { type Contract } from '../contract/type'; +import { type InvalidDataError } from '../errors/type'; import { createRemoteOperation } from '../remote_operation/create_remote_operation'; import { postpone, @@ -11,10 +12,11 @@ import { type DynamicallySourcedField, SourcedField, } from '../libs/patronus'; -import { Validator } from '../validation/type'; -import { Query, QueryMeta, QuerySymbol } from './type'; -import { Event } from 'effector'; +import { type Validator } from '../validation/type'; +import { type Query, type QueryMeta, QuerySymbol } from './type'; + import { isEqual } from '../libs/lohyphen'; +import { readonly } from '../libs/patronus'; export interface SharedQueryFactoryConfig { name?: string; @@ -80,7 +82,11 @@ export function createHeadlessQuery< kind: QuerySymbol, serialize: serializationForSideStore(serialize), enabled, - meta: { serialize, initialData }, + meta: { + serialize, + initialData, + sid: querySid(createStore(null, { sid: 'dummy' })), + }, contract, validate, mapData, @@ -130,6 +136,16 @@ export function createHeadlessQuery< target: $stale, }); + sample({ + clock: operation.__.lowLevelAPI.pushData, + target: [$data, $error.reinit!], + }); + + sample({ + clock: operation.__.lowLevelAPI.pushError, + target: [$error, $data.reinit!], + }); + // -- Trigger API const postponedRefresh: Event = postpone({ @@ -210,12 +226,24 @@ export function createHeadlessQuery< // -- Public API -- return { - $data, - $error, - $stale, reset, refresh, - ...operation, + start: operation.start, + $data: readonly($data), + $error: readonly($error), + $status: readonly(operation.$status), + $idle: readonly(operation.$idle), + $pending: readonly(operation.$pending), + $succeeded: readonly(operation.$succeeded), + $failed: readonly(operation.$failed), + $enabled: readonly(operation.$enabled), + $stale, + finished: { + success: readonly(operation.finished.success), + failure: readonly(operation.finished.failure), + finally: readonly(operation.finished.finally), + skip: readonly(operation.finished.skip), + }, __: { ...operation.__, experimentalAPI: { attach: attachProtocol }, @@ -223,3 +251,13 @@ export function createHeadlessQuery< '@@unitShape': unitShapeProtocol, }; } + +function querySid($data: Store): string | null { + const sid = $data.sid; + + if (!sid?.includes('|')) { + return null; + } + + return sid; +} diff --git a/packages/core/src/query/type.ts b/packages/core/src/query/type.ts index 325c95a69..6f8e87638 100644 --- a/packages/core/src/query/type.ts +++ b/packages/core/src/query/type.ts @@ -13,6 +13,7 @@ export interface QueryMeta { */ serialize: Serialize; initialData: InitialData; + sid: string | null; } export interface Query diff --git a/packages/core/src/remote_operation/create_remote_operation.ts b/packages/core/src/remote_operation/create_remote_operation.ts index f06a9b198..7dc32dd24 100644 --- a/packages/core/src/remote_operation/create_remote_operation.ts +++ b/packages/core/src/remote_operation/create_remote_operation.ts @@ -14,6 +14,7 @@ import { type StaticOrReactive, type FetchingStatus, SourcedField, + readonly, } from '../libs/patronus'; import { createContractApplier } from '../contract/apply_contract'; import { Contract } from '../contract/type'; @@ -64,6 +65,8 @@ export function createRemoteOperation< paramsAreMeaningless?: boolean; }): RemoteOperation { const revalidate = createEvent<{ params: Params; refresh: boolean }>(); + const pushData = createEvent(); + const pushError = createEvent(); const applyContractFx = createContractApplier( contract @@ -327,13 +330,15 @@ export function createRemoteOperation< executeFx, meta: { ...meta, name }, kind, - $latestParams, + $latestParams: readonly($latestParams), lowLevelAPI: { dataSources, dataSourceRetrieverFx: retrieveDataFx, sourced: sourced ?? [], paramsAreMeaningless: paramsAreMeaningless ?? false, revalidate, + pushError, + pushData, }, }, }; diff --git a/packages/core/src/remote_operation/type.ts b/packages/core/src/remote_operation/type.ts index 39efdbe95..b598ba3ec 100644 --- a/packages/core/src/remote_operation/type.ts +++ b/packages/core/src/remote_operation/type.ts @@ -90,6 +90,8 @@ export interface RemoteOperation { sourced: SourcedField[]; paramsAreMeaningless: boolean; revalidate: Event<{ params: Params; refresh: boolean }>; + pushData: Event; + pushError: Event; }; experimentalAPI?: { attach: (config: { diff --git a/packages/core/src/update/update.ts b/packages/core/src/update/update.ts index 615e0563d..446089961 100644 --- a/packages/core/src/update/update.ts +++ b/packages/core/src/update/update.ts @@ -131,13 +131,13 @@ export function update< sample({ clock: fillQueryData, fn: ({ result }) => result, - target: [query.$data, query.$error.reinit!], + target: query.__.lowLevelAPI.pushData, }); sample({ clock: fillQueryError, fn: ({ error }) => error, - target: [query.$error, query.$data.reinit!], + target: query.__.lowLevelAPI.pushError, }); // -- Refetching