Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sour-bears-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@farfetched/core": minor
---

Added key modification parameter `modifyKey` to operator `cache`
1 change: 1 addition & 0 deletions apps/website/docs/api/operators/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?`: <Badge type="tip" text="since v0.12" /> _boolean_ whether to use human-readable keys in the cache. Default is `false` and the key is not human-readable.
- `modifyKey?`: [_Effect<string, string>_](https://effector.dev/en.effector/Effect/) keys modification (important: don't use for keys replacement).

## Adapters

Expand Down
27 changes: 27 additions & 0 deletions apps/website/docs/recipes/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
82 changes: 81 additions & 1 deletion packages/core/src/cache/__test__/cache.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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",
},
],
]
`);
});
});
18 changes: 14 additions & 4 deletions packages/core/src/cache/cache.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,13 +12,15 @@ interface CacheParameters {
staleAfter?: Time;
purge?: Event<void>;
humanReadableKeys?: boolean;
modifyKey?: Effect<string, string>;
}

interface CacheParametersDefaulted {
adapter: CacheAdapter;
staleAfter?: Time;
purge?: Event<void>;
humanReadableKeys: boolean;
modifyKey?: Effect<string, string>;
}

export function cache<Q extends Query<any, any, any, any>>(
Expand All @@ -30,6 +32,7 @@ export function cache<Q extends Query<any, any, any, any>>(
staleAfter,
purge,
humanReadableKeys,
modifyKey,
}: CacheParametersDefaulted = {
adapter: rawParams?.adapter ?? inMemoryCache(),
humanReadableKeys: false,
Expand All @@ -45,6 +48,8 @@ export function cache<Q extends Query<any, any, any, any>>(
return Promise.all(sourcedReaders.map((readerFx) => readerFx(params)));
});

const modifyKeyFx = createEffect<string, string>(modifyKey ?? ((key) => key));

const unsetFx = createEffect<
{
params: unknown;
Expand All @@ -64,7 +69,9 @@ export function cache<Q extends Query<any, any, any, any>>(
return;
}

await instance.unset({ key });
const modifiedKey = await modifyKeyFx(key);

await instance.unset({ key: modifiedKey });
});

const setFx = createEffect<
Expand All @@ -88,7 +95,9 @@ export function cache<Q extends Query<any, any, any, any>>(
return;
}

await instance.set({ key, value: result });
const modifiedKey = await modifyKeyFx(key);

await instance.set({ key: modifiedKey, value: result });
});

const getFx = createEffect<
Expand All @@ -107,8 +116,9 @@ export function cache<Q extends Query<any, any, any, any>>(
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;
Expand Down