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: