diff --git a/.changeset/sour-bears-develop.md b/.changeset/sour-bears-develop.md
new file mode 100644
index 00000000..d18b3df5
--- /dev/null
+++ b/.changeset/sour-bears-develop.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/core": minor
+---
+
+Added key modification parameter `modifyKey` to operator `cache`
diff --git a/apps/website/docs/api/operators/cache.md b/apps/website/docs/api/operators/cache.md
index cec01c52..76107892 100644
--- a/apps/website/docs/api/operators/cache.md
+++ b/apps/website/docs/api/operators/cache.md
@@ -20,6 +20,7 @@ Config fields:
- `staleAfter?`: [_Time_](/api/primitives/time) after which the data is considered stale and will be re-fetched immediately
- `purge?`: [_Event_](https://effector.dev/en/api/effector/event/) after calling which all records will be deleted from the cache
- `humanReadableKeys?`: _boolean_ whether to use human-readable keys in the cache. Default is `false` and the key is not human-readable.
+- `modifyKey?`: [_Effect_](https://effector.dev/en.effector/Effect/) keys modification (important: don't use for keys replacement).
## Adapters
diff --git a/apps/website/docs/recipes/cache.md b/apps/website/docs/recipes/cache.md
index 757a62b8..a8f2828c 100644
--- a/apps/website/docs/recipes/cache.md
+++ b/apps/website/docs/recipes/cache.md
@@ -104,6 +104,33 @@ To get short and unique key, we stringify all data, concatenate it and then hash
It is a cryptographically broken, but we use it for key generation only, so it is safe to use it in this case.
:::
+### Custom key modification
+
+Key modification based on already generated key. It can be useful for multiple cases: SSR, multiple account, localizations, environment variable or something else.
+For example, we can use localization with known store value: we know that cache must be related with your localization, but we don't need use localization key as query `params`, and we don't need purge already loaded (previous) locale.
+```ts
+import { attach } from "effector";
+import { createQuery, localStorageCache } from "@farfetched/core";
+
+const $locale = createStore("en");
+
+const modifyKeyFx = attach({
+ source: { locale: $locale },
+ effect: ({locale}, key: string) => {
+ return `${key}:lang_${locale}`;
+ },
+});
+
+const query = createQuery({
+ effect: createEffect(async () => (await fetch("/api/posts")).json()),
+});
+
+cache(query, {
+ adapter: localStorageCache,
+ modifyKey: modifyKeyFx,
+});
+```
+
## Adapter replacement
Sometimes it's necessary to replace current cache adapter with a different one. E.g. it's impossible to use `localStorage` on server-side during SSR, so you have to replace it with some in-memory adapter. To do this Farfetched provides a special property in every adapter `.__.$instance` that can be replaced via Fork API.
diff --git a/packages/core/src/cache/__test__/cache.test.ts b/packages/core/src/cache/__test__/cache.test.ts
index b861a092..38f9861c 100644
--- a/packages/core/src/cache/__test__/cache.test.ts
+++ b/packages/core/src/cache/__test__/cache.test.ts
@@ -1,4 +1,4 @@
-import { allSettled, createEffect, createEvent, fork } from 'effector';
+import { allSettled, createEffect, createEvent, createStore, attach, fork } from 'effector';
import { setTimeout } from 'timers/promises';
import { describe, vi, expect, test, beforeAll, afterAll } from 'vitest';
@@ -441,4 +441,84 @@ describe('cache', () => {
]
`);
});
+
+ test('`modifyKey` method get/set correct key with dynamic external store', async () => {
+ const q = withFactory({
+ sid: 'test',
+ fn: () => createQuery({ handler: async (id: number) => id }),
+ });
+
+ const get = vi.fn();
+ const set = vi.fn();
+ const unset = vi.fn();
+
+ const myAdapter = createCacheAdapter({
+ get: createEffect(get),
+ set: createEffect(set),
+ purge: createEvent(),
+ unset: createEffect(unset),
+ });
+
+ const $dynamicKey = createStore(1)
+ const modifyKeyFx = attach({
+ source: { dynamicKey: $dynamicKey },
+ effect: ({dynamicKey}, key) => {
+ return `${key}:custom-postfix_${dynamicKey}`
+ },
+ });
+
+ cache(q, { humanReadableKeys: true, adapter: myAdapter, modifyKey: modifyKeyFx });
+
+ const scope = fork();
+
+ await allSettled($dynamicKey, { scope, params: 1 });
+ await allSettled(q.start, { scope, params: 1 });
+ await allSettled($dynamicKey, { scope, params: 2 });
+ await allSettled(q.start, { scope, params: 1 });
+ await allSettled($dynamicKey, { scope, params: 1 });
+ await allSettled(q.start, { scope, params: 1 });
+
+ expect(set.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ {
+ "key": "{"params":1,"sid":"test|dummy","sources":[]}:custom-postfix_1",
+ "value": 1,
+ },
+ ],
+ [
+ {
+ "key": "{"params":1,"sid":"test|dummy","sources":[]}:custom-postfix_2",
+ "value": 1,
+ },
+ ],
+ [
+ {
+ "key": "{"params":1,"sid":"test|dummy","sources":[]}:custom-postfix_1",
+ "value": 1,
+ },
+ ],
+ ]
+ `);
+
+ expect(get.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ {
+ "key": "{"params":1,"sid":"test|dummy","sources":[]}:custom-postfix_1",
+ },
+ ],
+ [
+ {
+ "key": "{"params":1,"sid":"test|dummy","sources":[]}:custom-postfix_2",
+ },
+ ],
+ [
+ {
+ "key": "{"params":1,"sid":"test|dummy","sources":[]}:custom-postfix_1",
+ },
+ ],
+ ]
+ `);
+ });
});
diff --git a/packages/core/src/cache/cache.ts b/packages/core/src/cache/cache.ts
index cbc792fe..45f02adf 100644
--- a/packages/core/src/cache/cache.ts
+++ b/packages/core/src/cache/cache.ts
@@ -1,4 +1,4 @@
-import { attach, createEffect, Event, sample } from 'effector';
+import { attach, createEffect, Effect, Event, sample } from 'effector';
import { parseTime, type Time } from '../libs/date-nfs';
import { type Query } from '../query/type';
@@ -12,6 +12,7 @@ interface CacheParameters {
staleAfter?: Time;
purge?: Event;
humanReadableKeys?: boolean;
+ modifyKey?: Effect;
}
interface CacheParametersDefaulted {
@@ -19,6 +20,7 @@ interface CacheParametersDefaulted {
staleAfter?: Time;
purge?: Event;
humanReadableKeys: boolean;
+ modifyKey?: Effect;
}
export function cache>(
@@ -30,6 +32,7 @@ export function cache>(
staleAfter,
purge,
humanReadableKeys,
+ modifyKey,
}: CacheParametersDefaulted = {
adapter: rawParams?.adapter ?? inMemoryCache(),
humanReadableKeys: false,
@@ -45,6 +48,8 @@ export function cache>(
return Promise.all(sourcedReaders.map((readerFx) => readerFx(params)));
});
+ const modifyKeyFx = createEffect(modifyKey ?? ((key) => key));
+
const unsetFx = createEffect<
{
params: unknown;
@@ -64,7 +69,9 @@ export function cache>(
return;
}
- await instance.unset({ key });
+ const modifiedKey = await modifyKeyFx(key);
+
+ await instance.unset({ key: modifiedKey });
});
const setFx = createEffect<
@@ -88,7 +95,9 @@ export function cache>(
return;
}
- await instance.set({ key, value: result });
+ const modifiedKey = await modifyKeyFx(key);
+
+ await instance.set({ key: modifiedKey, value: result });
});
const getFx = createEffect<
@@ -107,8 +116,9 @@ export function cache>(
if (!key) {
return null;
}
+ const modifiedKey = await modifyKeyFx(key);
- const result = await instance.get({ key });
+ const result = await instance.get({ key: modifiedKey });
if (!result) {
return null;