Skip to content

Commit

Permalink
Share inMemoryCache between scopes, do not block allSettled in it
Browse files Browse the repository at this point in the history
  • Loading branch information
igorkamyshev committed Jan 28, 2023
1 parent 6883193 commit 7fd0c57
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-tips-mix.md
@@ -0,0 +1,5 @@
---
'@farfetched/core': minor
---

Share `inMemoryCache` between scopes, do not block `allSettled` in it
41 changes: 40 additions & 1 deletion packages/core/src/cache/adapters/__test__/in_memory.test.ts
@@ -1,4 +1,5 @@
import { createEvent, fork, scopeBind } from 'effector';
import { allSettled, createEvent, fork, scopeBind } from 'effector';
import { parseTime } from 'packages/core/src/libs/date-nfs';
import { describe, test, expect, vi } from 'vitest';

import { inMemoryCache } from '../in_memory';
Expand Down Expand Up @@ -39,4 +40,42 @@ describe('inMemoryCache', () => {
expect(listener).toBeCalledTimes(1);
expect(listener).toBeCalledWith({ key: 'key' });
});

test('share across scopes', async () => {
const cache = inMemoryCache({
maxAge: '1sec',
});

const scopeOne = fork();
const scopeTwo = fork();

// Save on scopeOne
await scopeBind(cache.set, {
scope: scopeOne,
})({ key: 'key', value: 'test-value' });

// Get on scopeTwo
const resultOne = await scopeBind(cache.get, {
scope: scopeTwo,
})({ key: 'key' });

expect(resultOne?.value).toEqual('test-value');
});

test('do not block allSettled', async () => {
const cache = inMemoryCache({
maxAge: '1sec',
});

const scope = fork();

const before = Date.now();
await allSettled(cache.set, {
scope,
params: { key: 'key', value: 'test-value' },
});
const after = Date.now();

expect(after - before).toBeLessThan(parseTime('1sec'));
});
});
84 changes: 33 additions & 51 deletions packages/core/src/cache/adapters/in_memory.ts
@@ -1,12 +1,6 @@
import {
attach,
createEffect,
createEvent,
createStore,
sample,
} from 'effector';

import { time, delay } from '../../libs/patronus';
import { createEffect, createEvent, sample, scopeBind } from 'effector';

import { time } from '../../libs/patronus';
import { parseTime } from '../../libs/date-nfs';
import { createAdapter } from './instance';
import { attachObservability } from './observability';
Expand All @@ -15,16 +9,10 @@ import { CacheAdapter, CacheAdapterOptions } from './type';
type Entry = { value: unknown; cachedAt: number };
type Storage = Record<string, Entry>;

// TODO: save time to prevent stale data because of throttling
export function inMemoryCache(config?: CacheAdapterOptions): CacheAdapter {
const { maxEntries, maxAge, observability } = config ?? {};

const $storage = createStore<Storage>(
{},
{
serialize: 'ignore',
}
);
let storage: Storage = {};

const saveValue = createEvent<{ key: string; value: unknown }>();
const removeValue = createEvent<{ key: string }>();
Expand All @@ -34,25 +22,25 @@ export function inMemoryCache(config?: CacheAdapterOptions): CacheAdapter {

const purge = createEvent();

$storage.reset(purge);
purge.watch(() => {
storage = {};
});

const $now = time({ clock: saveValue });

const maxEntriesApplied = sample({
clock: saveValue,
source: { storage: $storage, now: $now },
fn: ({ storage, now }, { key, value }) =>
source: { now: $now },
fn: ({ now }, { key, value }) =>
applyMaxEntries(
storage,
{ key, entry: { value, cachedAt: now } },
maxEntries
),
});

sample({
source: maxEntriesApplied,
fn: ({ next }) => next,
target: $storage,
maxEntriesApplied.watch(({ next }) => {
storage = next;
});

sample({
Expand All @@ -62,22 +50,19 @@ export function inMemoryCache(config?: CacheAdapterOptions): CacheAdapter {
target: itemEvicted,
});

sample({
clock: removeValue,
source: $storage,
fn: (storage, { key }) => {
const { [key]: _, ...rest } = storage;

return rest;
},
target: $storage,
removeValue.watch(({ key }) => {
const { [key]: _, ...rest } = storage;

storage = rest;
});

if (maxAge) {
delay({
clock: saveValue,
timeout: parseTime(maxAge),
target: itemExpired,
const timeout = parseTime(maxAge);

saveValue.watch((payload) => {
const boundItemExpired = scopeBind(itemExpired, { safe: true });

setTimeout(() => boundItemExpired(payload), timeout);
});

sample({
Expand All @@ -88,26 +73,23 @@ export function inMemoryCache(config?: CacheAdapterOptions): CacheAdapter {
}

const adapter = {
get: attach({
source: $storage,
effect: (storage, { key }: { key: string }): Entry | null => {
const saved = storage[key] ?? null;
get: createEffect(({ key }: { key: string }): Entry | null => {
const saved = storage[key] ?? null;

if (!saved) {
return null;
}
if (!saved) {
return null;
}

if (maxAge) {
const expiredAt = saved?.cachedAt + parseTime(maxAge);
if (maxAge) {
const expiredAt = saved?.cachedAt + parseTime(maxAge);

if (Date.now() >= expiredAt) {
removeValue({ key });
return null;
}
if (Date.now() >= expiredAt) {
removeValue({ key });
return null;
}
}

return saved;
},
return saved;
}),
set: createEffect<
{
Expand Down

0 comments on commit 7fd0c57

Please sign in to comment.