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;