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
1 change: 1 addition & 0 deletions packages/fetchye/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
],
"dependencies": {
"@babel/runtime": "^7.11.2",
"@keyv/redis": "^5.1.6",
"cacheable": "^2.5.0",
"fetchye-core": "^1.9.0",
"object-hash": "^2.0.3",
Expand Down
3 changes: 2 additions & 1 deletion packages/fetchye/src/react-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
import makeServerFetchye from './makeServerFetchye';
import { ignoreHeadersByKey } from '../mapOptionsToKeyHelpers';
import { computeKey } from '../computeKey';
import { invalidateFetchyeTags } from './persistentCache';
import { invalidateFetchyeTags, connectToRedis } from './persistentCache';

export {
makeServerFetchye,
ignoreHeadersByKey,
computeKey,
invalidateFetchyeTags,
connectToRedis,
};
6 changes: 3 additions & 3 deletions packages/fetchye/src/react-server/makeServerFetchye.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { runAsync } from './runAsync';
import { computeKey } from '../computeKey';
import { coerceSsrField } from '../queryHelpers';
import {
persistentCacheStore,
getPersistentCacheStore,
validatePersistentCache,
getPersistentCacheKey,
} from './persistentCache';
Expand Down Expand Up @@ -62,7 +62,7 @@ const makeServerFetchye = ({
if (persistentCache) {
const { isolationKey } = persistentCache;
const persistentCacheKey = getPersistentCacheKey(computedKey, isolationKey);
const cachedResponse = await persistentCacheStore.get(persistentCacheKey);
const cachedResponse = await getPersistentCacheStore().get(persistentCacheKey);

if (cachedResponse) {
return {
Expand Down Expand Up @@ -94,7 +94,7 @@ const makeServerFetchye = ({
const storeOptions = {
ttl, tags,
};
await persistentCacheStore.set(persistentCacheKey, res, storeOptions);
await getPersistentCacheStore().set(persistentCacheKey, res, storeOptions);
}
return {
data: coerceSsrField(res.data),
Expand Down
57 changes: 49 additions & 8 deletions packages/fetchye/src/react-server/persistentCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,72 @@
*/

import { Cacheable } from 'cacheable';
import KeyvRedis from '@keyv/redis';
import computeHash from 'object-hash';

// Applied to newly created entries when a persistentCache config does not provide
// its own ttl. A ttl is always required by the underlying cache.
export const DEFAULT_PERSISTENT_CACHE_TTL = 5 * 60 * 1000; // 5 minutes

/**
* A single, module-level cache shared by every call made with a `persistentCache`
* option. Unlike the promiseStore (which is only used to dedupe in-flight requests for
* the lifetime of a single request/render), this cache is intended to live across
* requests, which is what makes it "persistent".
* Returns the single, process-wide cache shared by every call made with a
* `persistentCache` option. Unlike the promiseStore (which is only used to dedupe
* in-flight requests for the lifetime of a single request/render), this cache is
* intended to live across requests, which is what makes it "persistent".
*
* This is a `cacheable` `Cacheable` instance. Its default (and only, unless a
* `secondary` store is explicitly configured) `primary` store is an in-memory TTL cache
* This is backed by `cacheable`'s shared static instance (`Cacheable.getStaticInstance`),
* so it is safe to call this multiple times (or from multiple files) and always get the
* same underlying cache. Its default (and only, unless a `secondary` store has been
* configured via `connectToRedis`) `primary` store is an in-memory TTL cache
* (`CacheableMemory`, via `createKeyv()`), so entries always expire on their own.
*
* All reads/writes go through cacheable's native, promise-based `get`/`set` API.
*
* Tag-based invalidation is enabled to support invalidating groups of cache entries by tags.
*
* This is a function rather than a direct reference to a `Cacheable` instance so that,
* if `connectToRedis` later swaps in a new static instance (backed by Redis) via
* `Cacheable.setStaticInstance`, every caller transparently starts using the new
* instance on their next call, without needing to re-import anything.
*/
export const persistentCacheStore = new Cacheable({
export const getPersistentCacheStore = () => Cacheable.getStaticInstance({
ttl: DEFAULT_PERSISTENT_CACHE_TTL,
tags: true,
});

/**
* Connects the shared persistent cache to a Redis server, using it as a `secondary`
* store alongside the existing in-memory `primary` store. This allows the persistent
* cache to be shared across multiple server instances/processes.
*
* If the connection attempt fails, the existing (in-memory only) static instance is
* left untouched, and `false` is returned so the caller can decide how to handle the
* failure (eg. logging, retrying, or simply continuing with an in-memory-only cache).
*
* @param {string} connectionString - A Redis connection string (eg. redis://localhost:6379)
* @returns {Promise<boolean>} Whether the connection succeeded and the static instance
* was updated to use Redis as its secondary store.
*/
export const connectToRedis = async (connectionString) => {
const keyvRedis = new KeyvRedis(connectionString);

try {
// getClient() establishes (and awaits) the underlying Redis connection, throwing
// if it fails to connect (KeyvRedis defaults to throwOnConnectError: true).
await keyvRedis.getClient();
} catch (error) {
return false;
}

Cacheable.setStaticInstance(new Cacheable({
ttl: DEFAULT_PERSISTENT_CACHE_TTL,
tags: true,
secondary: keyvRedis,
}));

return true;
};

/**
* Validates the shape of a `persistentCache` option. Throws when invalid.
*/
Expand Down Expand Up @@ -85,5 +126,5 @@ export const invalidateFetchyeTags = async (tags) => {
throw new Error('invalidateFetchyeTags requires at least one tag');
}

await persistentCacheStore.tags.invalidateTags(tags);
await getPersistentCacheStore().tags.invalidateTags(tags);
};
23 changes: 22 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1843,6 +1843,15 @@
hashery "^1.4.0"
hookified "^1.15.0"

"@keyv/redis@^5.1.6":
version "5.1.6"
resolved "https://registry.yarnpkg.com/@keyv/redis/-/redis-5.1.6.tgz#f56030cc2883fa30401da814761d350aad87adee"
integrity sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ==
dependencies:
"@redis/client" "^5.10.0"
cluster-key-slot "^1.1.2"
hookified "^1.13.0"

"@keyv/serialize@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@keyv/serialize/-/serialize-1.1.1.tgz"
Expand Down Expand Up @@ -2277,6 +2286,13 @@
dependencies:
"@octokit/openapi-types" "^24.2.0"

"@redis/client@^5.10.0":
version "5.12.1"
resolved "https://registry.yarnpkg.com/@redis/client/-/client-5.12.1.tgz#a35a2bac546c727d7915d2d91b63a77111e51ebd"
integrity sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==
dependencies:
cluster-key-slot "1.1.2"

"@sigstore/bundle@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-4.0.0.tgz"
Expand Down Expand Up @@ -3582,6 +3598,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=

cluster-key-slot@1.1.2, cluster-key-slot@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==

cmd-shim@6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.3.tgz"
Expand Down Expand Up @@ -5764,7 +5785,7 @@ hoist-non-react-statics@^3.3.2:
dependencies:
react-is "^16.7.0"

hookified@^1.15.0, hookified@^1.15.1:
hookified@^1.13.0, hookified@^1.15.0, hookified@^1.15.1:
version "1.15.1"
resolved "https://registry.yarnpkg.com/hookified/-/hookified-1.15.1.tgz"
integrity sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==
Expand Down
Loading