@nanostores/query
Advanced tools
Comparing version 0.2.10 to 0.3.0
@@ -1,2 +0,3 @@ | ||
import { MapStore, ReadableAtom } from "nanostores"; | ||
import { MapStore, ReadableAtom } from 'nanostores'; | ||
type NoKey = null | undefined | void | false; | ||
@@ -9,10 +10,17 @@ type SomeKey = string | number | true; | ||
export type Fetcher<T> = (...args: KeyParts) => Promise<T>; | ||
export type OnErrorRetry = (opts: { | ||
error: unknown; | ||
key: Key; | ||
retryCount: number; | ||
}) => number | void | false | null | undefined; | ||
type EventTypes = { | ||
onError?: (error: any) => unknown; | ||
onError?: (error: unknown) => void; | ||
}; | ||
type RefetchSettings = { | ||
dedupeTime?: number; | ||
refetchOnFocus?: boolean; | ||
refetchOnReconnect?: boolean; | ||
refetchInterval?: number; | ||
revalidateOnFocus?: boolean; | ||
revalidateOnReconnect?: boolean; | ||
revalidateInterval?: number; | ||
cacheLifetime?: number; | ||
onErrorRetry?: OnErrorRetry | null | false; | ||
}; | ||
@@ -23,3 +31,9 @@ export type CommonSettings<T = unknown> = { | ||
export type NanoqueryArgs = { | ||
cache?: Map<Key, any>; | ||
cache?: Map<Key, { | ||
data?: unknown; | ||
error?: unknown; | ||
retryCount?: number; | ||
created?: number; | ||
expires?: number; | ||
}>; | ||
} & CommonSettings; | ||
@@ -34,4 +48,5 @@ export type FetcherValue<T = any, E = Error> = { | ||
_: Symbol; | ||
key?: string; | ||
key?: Key; | ||
invalidate: (...args: any[]) => void; | ||
revalidate: (...args: any[]) => void; | ||
mutate: (data?: T) => void; | ||
@@ -43,2 +58,3 @@ }; | ||
invalidate: (key: KeySelector) => void; | ||
revalidate: (key: KeySelector) => void; | ||
getCacheUpdater: <T = unknown>(key: Key, shouldRevalidate?: boolean) => [(newValue?: T) => void, T | undefined]; | ||
@@ -55,7 +71,11 @@ }) => Promise<Result>; | ||
}; | ||
export declare const nanoquery: ({ cache, fetcher: globalFetcher, ...globalSettings }?: NanoqueryArgs) => readonly [<T = unknown, E = any>(keyInput: KeyInput, { fetcher, ...fetcherSettings }?: CommonSettings<T>) => FetcherStore<T, E>, <Data = void, Result = unknown, E_1 = any>(mutator: ManualMutator<Data, Result>) => MutatorStore<Data, Result, E_1>, { | ||
export declare const nanoquery: ({ cache, fetcher: globalFetcher, ...globalSettings }?: NanoqueryArgs) => readonly [<T = unknown, E = any>(keyInput: KeyInput, { fetcher, ...fetcherSettings }?: CommonSettings<T>) => FetcherStore<T, E>, <Data = void, Result = unknown, E_1 = any>(mutator: ManualMutator<Data, Result>, opts?: { | ||
throttleCalls?: boolean; | ||
onError?: EventTypes["onError"]; | ||
}) => MutatorStore<Data, Result, E_1>, { | ||
readonly __unsafeOverruleSettings: (data: CommonSettings) => void; | ||
readonly invalidateKeys: (keySelector: KeySelector) => void; | ||
readonly revalidateKeys: (keySelector: KeySelector) => void; | ||
readonly mutateCache: (keySelector: KeySelector, data?: unknown) => void; | ||
}]; | ||
export {}; |
@@ -1,3 +0,4 @@ | ||
import { map, onStart, onStop, atom, startTask } from "nanostores"; | ||
import { createNanoEvents } from "nanoevents"; | ||
import { map, onStart, onStop, atom, batched, startTask } from 'nanostores'; | ||
import { createNanoEvents } from 'nanoevents'; | ||
const nanoquery = ({ | ||
@@ -15,6 +16,12 @@ cache = /* @__PURE__ */ new Map(), | ||
subscribe("online", () => events.emit(RECONNECT)); | ||
const _refetchOnInterval = /* @__PURE__ */ new Map(), _lastFetch = /* @__PURE__ */ new Map(), _runningFetches = /* @__PURE__ */ new Map(); | ||
const _revalidateOnInterval = /* @__PURE__ */ new Map(), _errorInvalidateTimeouts = /* @__PURE__ */ new Map(), _runningFetches = /* @__PURE__ */ new Map(); | ||
let rewrittenSettings = {}; | ||
const runFetcher = async ([key, keyParts], store, settings, force) => { | ||
var _a; | ||
const getCachedValueByKey = (key) => { | ||
const fromCache = cache.get(key); | ||
if (!fromCache) | ||
return []; | ||
const cacheHit = (fromCache.expires || 0) > (/* @__PURE__ */ new Date()).getTime(); | ||
return cacheHit ? [fromCache.data, fromCache.error] : []; | ||
}; | ||
const runFetcher = async ([key, keyParts], store, settings) => { | ||
if (!focus) | ||
@@ -28,5 +35,6 @@ return; | ||
}; | ||
const setAsLoading = () => { | ||
const setAsLoading = (prev) => { | ||
const toSet = prev === void 0 ? {} : { data: prev }; | ||
set({ | ||
...store.value, | ||
...toSet, | ||
...loading, | ||
@@ -36,18 +44,27 @@ promise: _runningFetches.get(key) | ||
}; | ||
const { dedupeTime = 4e3, fetcher } = { | ||
let { | ||
dedupeTime = 4e3, | ||
cacheLifetime = Infinity, | ||
fetcher, | ||
onErrorRetry = defaultOnErrorRetry | ||
} = { | ||
...settings, | ||
...rewrittenSettings | ||
}; | ||
const now = getNow(); | ||
if (cacheLifetime < dedupeTime) | ||
cacheLifetime = dedupeTime; | ||
const now = (/* @__PURE__ */ new Date()).getTime(); | ||
if (_runningFetches.has(key)) { | ||
if (!store.value.loading) | ||
setAsLoading(); | ||
setAsLoading(getCachedValueByKey(key)[0]); | ||
return; | ||
} | ||
if (!force) { | ||
const cached = cache.get(key); | ||
if (cached && store.value.data !== cached) | ||
set({ data: cached, ...notLoading }); | ||
const last = _lastFetch.get(key); | ||
if (last && last + dedupeTime > now) { | ||
let cachedValue, cachedError; | ||
const fromCache = cache.get(key); | ||
if (fromCache?.data !== void 0 || fromCache?.error) { | ||
[cachedValue, cachedError] = getCachedValueByKey(key); | ||
if ((fromCache.created || 0) + dedupeTime > now) { | ||
if (store.value.data != cachedValue || store.value.error != cachedError) { | ||
set({ ...notLoading, data: cachedValue, error: cachedError }); | ||
} | ||
return; | ||
@@ -58,12 +75,34 @@ } | ||
try { | ||
clearTimeout(_errorInvalidateTimeouts.get(key)); | ||
const promise = fetcher(...keyParts); | ||
_lastFetch.set(key, now); | ||
_runningFetches.set(key, promise); | ||
setAsLoading(); | ||
setAsLoading(cachedValue); | ||
const res = await promise; | ||
cache.set(key, res); | ||
cache.set(key, { | ||
data: res, | ||
created: (/* @__PURE__ */ new Date()).getTime(), | ||
expires: (/* @__PURE__ */ new Date()).getTime() + cacheLifetime | ||
}); | ||
set({ data: res, ...notLoading }); | ||
_lastFetch.set(key, getNow()); | ||
} catch (error) { | ||
(_a = settings.onError) == null ? void 0 : _a.call(settings, error); | ||
settings.onError?.(error); | ||
const retryCount = (cache.get(key)?.retryCount || 0) + 1; | ||
cache.set(key, { | ||
error, | ||
created: (/* @__PURE__ */ new Date()).getTime(), | ||
expires: (/* @__PURE__ */ new Date()).getTime() + cacheLifetime, | ||
retryCount | ||
}); | ||
if (onErrorRetry) { | ||
const timer = onErrorRetry({ | ||
error, | ||
key, | ||
retryCount | ||
}); | ||
if (timer) | ||
_errorInvalidateTimeouts.set( | ||
key, | ||
setTimeout(() => invalidateKeys(key), timer) | ||
); | ||
} | ||
set({ data: store.value.data, error, ...notLoading }); | ||
@@ -94,2 +133,8 @@ } finally { | ||
}; | ||
fetcherStore.revalidate = () => { | ||
const { key } = fetcherStore; | ||
if (key) { | ||
revalidateKeys(key); | ||
} | ||
}; | ||
fetcherStore.mutate = (data) => { | ||
@@ -125,5 +170,5 @@ const { key } = fetcherStore; | ||
const { | ||
refetchInterval = 0, | ||
refetchOnFocus, | ||
refetchOnReconnect | ||
revalidateInterval = 0, | ||
revalidateOnFocus, | ||
revalidateOnReconnect | ||
} = settings; | ||
@@ -134,18 +179,20 @@ const runRefetcher = () => { | ||
}; | ||
if (refetchInterval > 0) { | ||
_refetchOnInterval.set( | ||
if (revalidateInterval > 0) { | ||
_revalidateOnInterval.set( | ||
keyInput, | ||
setInterval(runRefetcher, refetchInterval) | ||
setInterval(runRefetcher, revalidateInterval) | ||
); | ||
} | ||
if (refetchOnFocus) | ||
if (revalidateOnFocus) | ||
evtUnsubs.push(events.on(FOCUS, runRefetcher)); | ||
if (refetchOnReconnect) | ||
if (revalidateOnReconnect) | ||
evtUnsubs.push(events.on(RECONNECT, runRefetcher)); | ||
const cacheKeyChangeHandler = (keySelector) => { | ||
if (prevKey && testKeyAgainstSelector(prevKey, keySelector)) { | ||
runFetcher([prevKey, prevKeyParts], fetcherStore, settings); | ||
} | ||
}; | ||
evtUnsubs.push( | ||
events.on(INVALIDATE_KEYS, (keySelector) => { | ||
if (prevKey && testKeyAgainstSelector(prevKey, keySelector)) { | ||
runFetcher([prevKey, prevKeyParts], fetcherStore, settings, true); | ||
} | ||
}), | ||
events.on(INVALIDATE_KEYS, cacheKeyChangeHandler), | ||
events.on(REVALIDATE_KEYS, cacheKeyChangeHandler), | ||
events.on(SET_CACHE, (keySelector, data, full) => { | ||
@@ -173,14 +220,10 @@ if (prevKey && testKeyAgainstSelector(prevKey, keySelector) && fetcherStore.value !== data && fetcherStore.value.data !== data) { | ||
fetcherStore.value = { ...notLoading }; | ||
keysInternalUnsub == null ? void 0 : keysInternalUnsub(); | ||
keysInternalUnsub?.(); | ||
evtUnsubs.forEach((fn) => fn()); | ||
evtUnsubs = []; | ||
keyUnsub == null ? void 0 : keyUnsub(); | ||
clearInterval(_refetchOnInterval.get(keyInput)); | ||
keyUnsub?.(); | ||
clearInterval(_revalidateOnInterval.get(keyInput)); | ||
}); | ||
return fetcherStore; | ||
}; | ||
const nukeKey = (key) => { | ||
cache.delete(key); | ||
_lastFetch.delete(key); | ||
}; | ||
const iterOverCache = (keySelector, cb) => { | ||
@@ -193,19 +236,45 @@ for (const key of cache.keys()) { | ||
const invalidateKeys = (keySelector) => { | ||
iterOverCache(keySelector, nukeKey); | ||
iterOverCache(keySelector, (key) => { | ||
cache.delete(key); | ||
}); | ||
events.emit(INVALIDATE_KEYS, keySelector); | ||
}; | ||
const revalidateKeys = (keySelector) => { | ||
iterOverCache(keySelector, (key) => { | ||
const cached = cache.get(key); | ||
if (cached) { | ||
cache.set(key, { ...cached, created: -Infinity }); | ||
} | ||
}); | ||
events.emit(REVALIDATE_KEYS, keySelector); | ||
}; | ||
const mutateCache = (keySelector, data) => { | ||
iterOverCache(keySelector, (key) => { | ||
if (data === void 0) | ||
nukeKey(key); | ||
else | ||
cache.set(key, data); | ||
cache.delete(key); | ||
else { | ||
cache.set(key, { | ||
data, | ||
created: (/* @__PURE__ */ new Date()).getTime(), | ||
expires: (/* @__PURE__ */ new Date()).getTime() + (globalSettings.cacheLifetime ?? 8e3) | ||
}); | ||
} | ||
}); | ||
events.emit(SET_CACHE, keySelector, data); | ||
}; | ||
function createMutatorStore(mutator) { | ||
function createMutatorStore(mutator, opts) { | ||
const { throttleCalls, onError } = opts ?? { | ||
throttleCalls: true, | ||
onError: globalSettings?.onError | ||
}; | ||
const mutate = async (data) => { | ||
var _a; | ||
if (throttleCalls && store.value?.loading) | ||
return; | ||
const newMutator = rewrittenSettings.fetcher ?? mutator; | ||
const keysToInvalidate = []; | ||
const keysToInvalidate = [], keysToRevalidate = []; | ||
const safeKeySet = (k, v) => { | ||
if (store.lc) { | ||
store.setKey(k, v); | ||
} | ||
}; | ||
try { | ||
@@ -223,20 +292,25 @@ store.set({ | ||
}, | ||
getCacheUpdater: (key, shouldInvalidate = true) => [ | ||
revalidate: (key) => { | ||
keysToRevalidate.push(key); | ||
}, | ||
getCacheUpdater: (key, shouldRevalidate = true) => [ | ||
(newVal) => { | ||
mutateCache(key, newVal); | ||
if (shouldInvalidate) { | ||
keysToInvalidate.push(key); | ||
if (shouldRevalidate) { | ||
keysToRevalidate.push(key); | ||
} | ||
}, | ||
cache.get(key) | ||
cache.get(key)?.data | ||
] | ||
}); | ||
store.setKey("data", result); | ||
safeKeySet("data", result); | ||
return result; | ||
} catch (error) { | ||
(_a = globalSettings == null ? void 0 : globalSettings.onError) == null ? void 0 : _a.call(globalSettings, error); | ||
onError?.(error); | ||
safeKeySet("error", error); | ||
store.setKey("error", error); | ||
} finally { | ||
store.setKey("loading", false); | ||
safeKeySet("loading", false); | ||
keysToInvalidate.forEach(invalidateKeys); | ||
keysToRevalidate.forEach(revalidateKeys); | ||
} | ||
@@ -248,2 +322,6 @@ }; | ||
}); | ||
onStop( | ||
store, | ||
() => store.set({ mutate, ...notLoading }) | ||
); | ||
store.mutate = mutate; | ||
@@ -263,3 +341,3 @@ return store; | ||
createMutatorStore, | ||
{ __unsafeOverruleSettings, invalidateKeys, mutateCache } | ||
{ __unsafeOverruleSettings, invalidateKeys, revalidateKeys, mutateCache } | ||
]; | ||
@@ -274,11 +352,12 @@ }; | ||
}]; | ||
let keyStore = atom(null), keyParts = []; | ||
const keyParts = []; | ||
const $key = atom(null); | ||
const keysAsStoresToIndexes = /* @__PURE__ */ new Map(); | ||
const setKeyStoreValue = () => { | ||
if (keyParts.some((v) => v === null || v === void 0 || v === false)) { | ||
keyStore.set(null); | ||
$key.set(null); | ||
} else { | ||
keyStore.set([keyParts.join(""), keyParts]); | ||
$key.set([keyParts.join(""), keyParts]); | ||
} | ||
}; | ||
const unsubs = []; | ||
for (let i = 0; i < keys.length; i++) { | ||
@@ -289,17 +368,23 @@ const keyOrStore = keys[i]; | ||
} else { | ||
unsubs.push( | ||
keyOrStore.subscribe((newValue) => { | ||
keyParts[i] = isFetcherStore(keyOrStore) ? keyOrStore.value && "data" in keyOrStore.value ? keyOrStore.key : null : newValue; | ||
setKeyStoreValue(); | ||
}) | ||
); | ||
keyParts.push(null); | ||
keysAsStoresToIndexes.set(keyOrStore, i); | ||
} | ||
} | ||
const storesAsArray = [...keysAsStoresToIndexes.keys()]; | ||
const $storeKeys = batched(storesAsArray, (...storeValues) => { | ||
for (let i = 0; i < storeValues.length; i++) { | ||
const store = storesAsArray[i], partIndex = keysAsStoresToIndexes.get(store); | ||
keyParts[partIndex] = store._ === fetcherSymbol ? store.value && "data" in store.value ? store.key : null : storeValues[i]; | ||
} | ||
setKeyStoreValue(); | ||
}); | ||
setKeyStoreValue(); | ||
return [keyStore, () => unsubs.forEach((fn) => fn())]; | ||
return [$key, $storeKeys.subscribe(noop)]; | ||
}; | ||
function isFetcherStore(v) { | ||
return v._ === fetcherSymbol; | ||
function defaultOnErrorRetry({ retryCount }) { | ||
return ~~((Math.random() + 0.5) * (1 << (retryCount < 8 ? retryCount : 8))) * 2e3; | ||
} | ||
const FOCUS = 1, RECONNECT = 2, INVALIDATE_KEYS = 3, SET_CACHE = 4; | ||
function noop() { | ||
} | ||
const FOCUS = 1, RECONNECT = 2, INVALIDATE_KEYS = 3, REVALIDATE_KEYS = 4, SET_CACHE = 5; | ||
const subscribe = (name, fn) => { | ||
@@ -319,7 +404,5 @@ const isServer = typeof window === "undefined"; | ||
}; | ||
const getNow = () => (/* @__PURE__ */ new Date()).getTime(); | ||
const fetcherSymbol = Symbol(); | ||
const loading = { loading: true }, notLoading = { loading: false }; | ||
export { | ||
nanoquery | ||
}; | ||
export { nanoquery }; |
{ | ||
"name": "@nanostores/query", | ||
"version": "0.2.10", | ||
"version": "0.3.0", | ||
"description": "Tiny remote data fetching library for Nano Stores", | ||
"scripts": { | ||
"pub": "pnpm build && npm publish && git push && git push --tags", | ||
"prepublish": "pnpm build", | ||
"pub": "pnpm test && pnpm build && npm publish && git push && git push --tags", | ||
"build": "vite build", | ||
"test:types": "tsc --noEmit && vitest typecheck --run", | ||
"test:unit": "vitest run", | ||
"test:unit": "vitest run --typecheck", | ||
"test:size": "size-limit --silent", | ||
@@ -44,24 +42,25 @@ "test": "pnpm run /^test:/", | ||
"dependencies": { | ||
"nanoevents": "^8.0.0" | ||
"nanoevents": "^9.0.0" | ||
}, | ||
"devDependencies": { | ||
"@evilmartians/lefthook": "^1.4.8", | ||
"@nanostores/react": "^0.7.1", | ||
"@rollup/plugin-strip": "^3.0.2", | ||
"@size-limit/preset-small-lib": "^8.2.6", | ||
"@testing-library/react": "^14.0.0", | ||
"@types/react": "^18.2.20", | ||
"@types/react-dom": "^18.2.7", | ||
"happy-dom": "^10.9.0", | ||
"nanostores": "^0.9.3", | ||
"@evilmartians/lefthook": "^1.6.9", | ||
"@nanostores/react": "^0.7.2", | ||
"@rollup/plugin-strip": "^3.0.4", | ||
"@size-limit/preset-small-lib": "^11.1.2", | ||
"@testing-library/react": "^14.3.0", | ||
"@types/node": "^20.12.6", | ||
"@types/react": "^18.2.75", | ||
"@types/react-dom": "^18.2.24", | ||
"happy-dom": "^14.7.1", | ||
"nanostores": "^0.10.3", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"size-limit": "^8.2.6", | ||
"typescript": "^5.1.6", | ||
"vite": "^4.4.9", | ||
"vite-plugin-dts": "^3.5.1", | ||
"vitest": "^0.34.1" | ||
"size-limit": "^11.1.2", | ||
"typescript": "^5.4.4", | ||
"vite": "^5.2.8", | ||
"vite-plugin-dts": "^3.8.1", | ||
"vitest": "^1.4.0" | ||
}, | ||
"peerDependencies": { | ||
"nanostores": ">0.7" | ||
"nanostores": ">0.10" | ||
}, | ||
@@ -80,3 +79,3 @@ "engines": { | ||
}, | ||
"limit": "1626 B" | ||
"limit": "1811 B" | ||
} | ||
@@ -83,0 +82,0 @@ ], |
132
README.md
@@ -8,17 +8,8 @@ # Nano Stores Query | ||
- **Small**. 1.62 Kb (minified and gzipped). | ||
- **Familiar DX**. If you've used [`swr`](https://swr.vercel.app/) or | ||
[`react-query`](https://react-query-v3.tanstack.com/), you'll get the same treatment, | ||
but for 10-20% of the size. | ||
- **Built-in cache**. `stale-while-revalidate` caching from | ||
[HTTP RFC 5861](https://tools.ietf.org/html/rfc5861). User rarely sees unnecessary | ||
loaders or stale data. | ||
- **Revalidate cache**. Automaticallty revalidate on interval, refocus, network | ||
recovery. Or just revalidate it manually. | ||
- **Nano Stores first**. Finally, fetching logic *outside* of components. Plays nicely | ||
with [store events](https://github.com/nanostores/nanostores#store-events), | ||
[computed stores](https://github.com/nanostores/nanostores#computed-stores), | ||
[router](https://github.com/nanostores/router), and the rest. | ||
- **Transport agnostic**. Use GraphQL, REST codegen, plain fetch or anything, | ||
that returns Promises. | ||
- **Small**. 1.8 Kb (minified and gzipped). | ||
- **Familiar DX**. If you've used [`swr`](https://swr.vercel.app/) or [`react-query`](https://react-query-v3.tanstack.com/), you'll get the same treatment, but for 10-20% of the size. | ||
- **Built-in cache**. `stale-while-revalidate` caching from [HTTP RFC 5861](https://tools.ietf.org/html/rfc5861). User rarely sees unnecessary loaders or stale data. | ||
- **Revalidate cache**. Automaticallty revalidate on interval, refocus, network recovery. Or just revalidate it manually. | ||
- **Nano Stores first**. Finally, fetching logic *outside* of components. Plays nicely with [store events](https://github.com/nanostores/nanostores#store-events), [computed stores](https://github.com/nanostores/nanostores#computed-stores), [router](https://github.com/nanostores/router), and the rest. | ||
- **Transport agnostic**. Use GraphQL, REST codegen, plain fetch or anything, that returns Promises (Web Workers, SubtleCrypto, calls to WASM, etc.). | ||
@@ -38,10 +29,7 @@ <a href="https://evilmartians.com/?utm_source=nanostores-query"> | ||
See [Nano Stores docs](https://github.com/nanostores/nanostores#guide) | ||
about using the store and subscribing to store’s changes in UI frameworks. | ||
See [Nano Stores docs](https://github.com/nanostores/nanostores#guide) about using the store and subscribing to store’s changes in UI frameworks. | ||
### Context | ||
First, we define the context. It allows us to share the default fetcher | ||
implementation between all fetcher stores, refetching settings, and allows for | ||
simple mocking in tests and stories. | ||
First, we define the context. It allows us to share the default fetcher implementation and general settings between all fetcher stores, and allows for simple mocking in tests and stories. | ||
@@ -73,6 +61,7 @@ ```ts | ||
const { data, loading } = useStore($currentPost); | ||
if (data) return <div>{data.content}</div>; | ||
if (loading) return <>Loading...</>; | ||
if (!data) return <>Error!</>; | ||
return <div>{data.content}</div>; | ||
return <>Error!</>; | ||
}; | ||
@@ -108,13 +97,22 @@ | ||
// How much time should pass between running fetcher for the exact same key parts | ||
// default = 4s | ||
// default = 4000 (=4 seconds; provide all time in milliseconds) | ||
dedupeTime?: number; | ||
// Lifetime for the stale cache. It present stale cache will be shown to a user. | ||
// Cannot be less than `dedupeTime`. | ||
// default = Infinity | ||
cacheLifetime?: number; | ||
// If we should revalidate the data when the window focuses | ||
// default = false | ||
refetchOnFocus?: boolean; | ||
revalidateOnFocus?: boolean; | ||
// If we should revalidate the data when network connection restores | ||
// default = false | ||
refetchOnReconnect?: boolean; | ||
// If we should run revalidation on an interval, in ms | ||
revalidateOnReconnect?: boolean; | ||
// If we should run revalidation on an interval | ||
// default = 0, no interval | ||
refetchInterval?: number; | ||
revalidateInterval?: number; | ||
// Error handling for specific fetcher store. Will get whatever fetcher function threw | ||
onError?: (error: any) => void; | ||
// A function that defines a timeout for automatic invalidation in case of an error | ||
// default — set to exponential backoff strategy | ||
onErrorRetry?: OnErrorRetry | null; | ||
} | ||
@@ -133,11 +131,11 @@ ``` | ||
- `data` is the data you pass to the `mutate` function; | ||
- `invalidate` allows you to mark other keys as stale so they are refetched next time; | ||
- `invalidate` and `revalidate`; more on them in section [How cache works](#how-cache-works) | ||
- `getCacheUpdater` allows you to get current cache value by key and update it with | ||
a new value. The key is also invalidated by default. | ||
a new value. The key is also revalidated by default. | ||
```ts | ||
export const $addComment = createMutatorStore<Comment>( | ||
async ({ data: comment, invalidate, getCacheUpdater }) => { | ||
// You can either invalidate the author… | ||
invalidate(`/api/users/${comment.authorId}`); | ||
async ({ data: comment, revalidate, getCacheUpdater }) => { | ||
// You can either revalidate the author… | ||
revalidate(`/api/users/${comment.authorId}`); | ||
@@ -175,2 +173,14 @@ // …or you can optimistically update current cache. | ||
`createMutatorStore` accepts an optional second argument with settings: | ||
```ts | ||
type MutationOptions = { | ||
// Error handling for specific fetcher store. Will get whatever mutation function threw | ||
onError?: (error: any) => void; | ||
// Throttles all subsequent calls to `mutate` function until the first call finishes. | ||
// default: true | ||
throttleCalls?: boolean; | ||
} | ||
``` | ||
You can also access the mutator function via `$addComment.mutate`—the function is the same. | ||
@@ -188,12 +198,7 @@ | ||
export const [,, { invalidateKeys, mutateCache }] = nanoquery(); | ||
export const [,, { invalidateKeys, revalidateKeys, mutateCache }] = nanoquery(); | ||
``` | ||
`invalidateKeys` does 2 things: | ||
Both `invalidateKeys` and `revalidateKeys` accept one argument—the keys—in 3 different forms, that we call _key selector_. More on them in section [How cache works](#how-cache-works) | ||
1. nukes all cache for the specified keys; | ||
2. asks all the fetcher stores that used those keys to refresh data immediately, if they have active subscribers. | ||
It accepts one argument—the keys—in 3 different forms, that we call _key selector_. | ||
```ts | ||
@@ -224,2 +229,33 @@ // Single key | ||
### How cache works | ||
All of this is based on [`stale-while-revalidate`](https://tools.ietf.org/html/rfc5861) methodology. The goal is simple: | ||
1. user visits `page 1` that fetches `/api/data/1`; | ||
2. user visits `page 2` that fetches `/api/data/2`; | ||
3. almost immediately user goes back to `page 1`. Instead of showing a spinner and loading data once again, we fetch it from cache. | ||
So, using this example, let's try to explain different cache-related settings the library has: | ||
- `dedupeTime` is the time that user needs to spend on `page 2` before going back for the library to trigger fetch function once again. | ||
- `cacheLifetime` is the maximum possible time between first visit and second visit to `page 1` after which we will stop serving stale cache to user (so they will immediately see a spinner). | ||
- `revalidate` forces the `dedupeTime` for this key to be 0, meaning, the very next time anything can trigger fetch (e.g., `refetchOnInterval`), it will call fetch function. If you were on the page during revalidation, you'd see cached value during loading. | ||
- `invalidate` kills this cache value entirely—it's as if you never were on this page. If you were on the page during invalidation, you'd see a spinner immediately. | ||
So, the best UI, we think, comes from this snippet: | ||
```tsx | ||
// components/Post.tsx | ||
const Post = () => { | ||
const { data, loading } = useStore($currentPost); | ||
if (data) return <div>{data.content}</div>; | ||
if (loading) return <>Loading...</>; | ||
return <>Error!</>; | ||
}; | ||
``` | ||
This way you actually embrace the stale-while-revalidate concept and only show spinners when there's no cache, but other than that you always fall back to cached state. | ||
### Local state and Pagination | ||
@@ -255,3 +291,3 @@ | ||
1. `fetcherStore.invalidate`. It's a function that invalidates current key for the fetcher. Doesn't accept any arguments. | ||
1. `fetcherStore.invalidate` and `fetcherStore.revalidate` | ||
2. `fetcherStore.mutate`. It's a function that mutates current key for the fetcher. Accepts the new value. | ||
@@ -264,3 +300,3 @@ 3. `fetcherStore.key`. Well, it holds current key in serialized form (as a string). | ||
Let's say, you have a dependency for your fetcher, but you don't wish for it to be in your fetcher keys. For example, this could be your `refreshToken`—that would be a hassle to put it _everywhere_, but you need it, because once you change your user, you don't want to have stale cache from the previous user. | ||
Let's say, you have a dependency for your fetcher, but you don't want it to be in your fetcher keys. For example, this could be your `userId`—that would be a hassle to put it _everywhere_, but you need it, because once you change your user, you don't want to have stale cache from the previous user. | ||
@@ -273,3 +309,3 @@ The idea here is to wipe the cache manually. For something as big as a new refresh token you can go and do a simple "wipe everything you find": | ||
But if your store is somehow dependant on other store, but it shouldn't be reflected in the key, you should do the same, but more targetly: | ||
If your store is somehow dependant on other store, but it shouldn't be reflected in the key, you should do the same, but more targetly: | ||
@@ -279,1 +315,11 @@ ```ts | ||
``` | ||
### Error handling | ||
`nanoquery`, `createFetcherStore` and `createMutationStore` all accept an optional setting called `onError`. Global `onError` handler is called for all errors thrown from fetcher and mutation calls unless you set a local `onError` handler for a specific store (then it "overwrites" the global one). | ||
`nanoquery` and `createFetcherStore` both accept and argument `onErrorRetry`. It also cascades down from context to each fetcher and can be rewritten by a fetcher. By default it implements an exponential backoff strategy with an element of randomness, but you can set your own according to `OnErrorRetry` signature. If you want to disable automatic revalidation for error responses, set this value to `null`. | ||
This feature is particularly handy for stuff like showing flash notifications for all errors. | ||
`onError` gets a single argument of whatever the fetch or mutate functions threw. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
45560
863
314
17
+ Addednanoevents@9.1.0(transitive)
- Removednanoevents@8.0.0(transitive)
Updatednanoevents@^9.0.0