Skip to content

Custom cache provider #1017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 2, 2021
Merged
Show file tree
Hide file tree
Changes from 13 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
100 changes: 6 additions & 94 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,8 @@
import { Cache as CacheType, Key, CacheListener } from './types'
import hash from './libs/hash'
import { Cache } from './types'

export default class Cache implements CacheType {
private cache: Map<string, any>
private subs: CacheListener[]

constructor(initialData: any = {}) {
this.cache = new Map(Object.entries(initialData))
this.subs = []
}

get(key: Key): any {
const [_key] = this.serializeKey(key)
return this.cache.get(_key)
}

set(key: Key, value: any): any {
const [_key] = this.serializeKey(key)
this.cache.set(_key, value)
this.notify()
}

keys() {
return Array.from(this.cache.keys())
}

has(key: Key) {
const [_key] = this.serializeKey(key)
return this.cache.has(_key)
}

clear() {
this.cache.clear()
this.notify()
}

delete(key: Key) {
const [_key] = this.serializeKey(key)
this.cache.delete(_key)
this.notify()
}

// TODO: introduce namespace for the cache
serializeKey(key: Key): [string, any, string, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}

if (Array.isArray(key)) {
// args array
args = key
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}

const errorKey = key ? 'err@' + key : ''
const isValidatingKey = key ? 'validating@' + key : ''

return [key, args, errorKey, isValidatingKey]
}

subscribe(listener: CacheListener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}

let isSubscribed = true
this.subs.push(listener)

return () => {
if (!isSubscribed) return
isSubscribed = false
const index = this.subs.indexOf(listener)
if (index > -1) {
this.subs[index] = this.subs[this.subs.length - 1]
this.subs.length--
}
}
}

// Notify Cache subscribers about a change in the cache
private notify() {
for (let listener of this.subs) {
listener()
}
}
export function wrapCache<Data = any>(provider: Cache<Data>): Cache {
// We might want to inject an extra layer on top of `provider` in the future,
// such as key serialization, auto GC, etc.
// For now, it's just a `Map` interface without any modifications.
return provider
}
2 changes: 1 addition & 1 deletion src/config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { createContext } from 'react'
import { SWRConfiguration } from './types'

const SWRConfigContext = createContext<SWRConfiguration>({})
SWRConfigContext.displayName = 'SWRConfigContext'
SWRConfigContext.displayName = 'SWRConfig'

export default SWRConfigContext
14 changes: 7 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { dequal } from 'dequal/lite'
import { Configuration, RevalidatorOptions, Revalidator } from './types'
import Cache from './cache'

import { wrapCache } from './cache'
import webPreset from './libs/web-preset'
import { Configuration, RevalidatorOptions, Revalidator } from './types'

// cache
const cache = new Cache()
const fetcher = (url: string) => fetch(url).then(res => res.json())

// error retry
function onErrorRetry(
Expand All @@ -14,7 +14,7 @@ function onErrorRetry(
revalidate: Revalidator,
opts: Required<RevalidatorOptions>
): void {
if (!config.isDocumentVisible()) {
if (!webPreset.isDocumentVisible()) {
// if it's hidden, stop
// it will auto revalidate when focus
return
Expand Down Expand Up @@ -67,10 +67,10 @@ const defaultConfig = {
shouldRetryOnError: true,
suspense: false,
compare: dequal,
fetcher,

isPaused: () => false,
...webPreset
cache: wrapCache(new Map())
} as const

export { cache }
export default defaultConfig
6 changes: 1 addition & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ export * from './use-swr'
// `useSWRInfinite`
export { useSWRInfinite } from './use-swr-infinite'

// Cache related, to be replaced by the new APIs
export { cache } from './config'

// Types
export {
SWRConfiguration,
Expand All @@ -27,6 +24,5 @@ export {
revalidateType,
RevalidateOptionInterface,
keyInterface,
responseInterface,
CacheInterface
responseInterface
} from './types'
28 changes: 28 additions & 0 deletions src/libs/serialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import hash from './hash'
import { Key } from '../types'

export function serialize(key: Key): [string, any, string, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}

if (Array.isArray(key)) {
// args array
args = key
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}

const errorKey = key ? 'err@' + key : ''
const isValidatingKey = key ? 'req@' + key : ''

return [key, args, errorKey, isValidatingKey]
}
3 changes: 0 additions & 3 deletions src/libs/web-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ const isDocumentVisible = () => {
return true
}

const fetcher = (url: string) => fetch(url).then(res => res.json())

const registerOnFocus = (cb: () => void) => {
if (
typeof window !== 'undefined' &&
Expand Down Expand Up @@ -54,7 +52,6 @@ const registerOnReconnect = (cb: () => void) => {
export default {
isOnline,
isDocumentVisible,
fetcher,
registerOnFocus,
registerOnReconnect
}
4 changes: 2 additions & 2 deletions src/resolve-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function useArgs<KeyType, ConfigType, Data>(
| readonly [KeyType, Fetcher<Data> | null]
| readonly [KeyType, ConfigType | undefined]
| readonly [KeyType, Fetcher<Data> | null, ConfigType | undefined]
): [KeyType, (typeof defaultConfig) & ConfigType, Fetcher<Data> | null] {
): [KeyType, Fetcher<Data> | null, (typeof defaultConfig) & ConfigType] {
const config = Object.assign(
{},
defaultConfig,
Expand All @@ -37,5 +37,5 @@ export default function useArgs<KeyType, ConfigType, Data>(
? args[1]
: config.fetcher) as Fetcher<Data> | null

return [args[0], config, fn]
return [args[0], fn, config]
}
71 changes: 34 additions & 37 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ export interface Configuration<
suspense: boolean
fetcher: Fn
initialData?: Data
cache: Cache

isOnline: () => boolean
isDocumentVisible: () => boolean
isPaused: () => boolean
onLoadingSlow: (
key: string,
Expand All @@ -44,12 +43,17 @@ export interface Configuration<
revalidate: Revalidator,
revalidateOpts: Required<RevalidatorOptions>
) => void
registerOnFocus?: (cb: () => void) => void
registerOnReconnect?: (cb: () => void) => void

compare: (a: Data | undefined, b: Data | undefined) => boolean
}

export interface Preset {
isOnline: () => boolean
isDocumentVisible: () => boolean
registerOnFocus?: (cb: () => void) => void
registerOnReconnect?: (cb: () => void) => void
}

export type ValueKey = string | any[] | null

export type Updater<Data = any, Error = any> = (
Expand All @@ -59,26 +63,43 @@ export type Updater<Data = any, Error = any> = (
shouldDedupe?: boolean,
dedupe?: boolean
) => boolean | Promise<boolean>
export type Trigger = (key: Key, shouldRevalidate?: boolean) => Promise<any>

export type MutatorCallback<Data = any> = (
currentValue: undefined | Data
) => Promise<undefined | Data> | undefined | Data

export type Broadcaster<Data = any, Error = any> = (
cache: Cache,
key: string,
data: Data,
error?: Error,
isValidating?: boolean
) => void
isValidating?: boolean,
shouldRevalidate?: boolean
) => Promise<Data>

export type State<Data, Error> = {
data?: Data
error?: Error
isValidating?: boolean
}

export type CacheListener = () => void
export type Mutator<Data = any> = (
cache: Cache,
key: Key,
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
) => Promise<Data | undefined>

export type ScopedMutator<Data> = (
key: Key,
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
) => Promise<Data | undefined>

export type KeyedMutator<Data> = (
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
) => Promise<Data | undefined>

// Public types

Expand Down Expand Up @@ -109,10 +130,7 @@ export type responseInterface<Data, Error> = {
data?: Data
error?: Error
revalidate: () => Promise<boolean>
mutate: (
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
) => Promise<Data | undefined>
mutate: KeyedMutator<Data>
isValidating: boolean
}
export interface SWRResponse<Data, Error> {
Expand All @@ -122,10 +140,7 @@ export interface SWRResponse<Data, Error> {
* @deprecated `revalidate` is deprecated, please use `mutate()` for the same purpose.
*/
revalidate: () => Promise<boolean>
mutate: (
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
) => Promise<Data | undefined>
mutate: KeyedMutator<Data>
isValidating: boolean
}

Expand Down Expand Up @@ -195,26 +210,8 @@ export type Revalidator = (
revalidateOpts: RevalidatorOptions
) => Promise<boolean>

/**
* @deprecated `CacheInterface` will be renamed to `Cache`.
*/
export interface CacheInterface {
get(key: Key): any
set(key: Key, value: any): any
keys(): string[]
has(key: Key): boolean
delete(key: Key): void
clear(): void
serializeKey(key: Key): [string, any, string, string]
subscribe(listener: CacheListener): () => void
}
export interface Cache {
get(key: Key): any
set(key: Key, value: any): any
keys(): string[]
has(key: Key): boolean
export interface Cache<Data = any> {
get(key: Key): Data | null | undefined
set(key: Key, value: Data): void
delete(key: Key): void
clear(): void
serializeKey(key: Key): [string, any, string, string]
subscribe(listener: CacheListener): () => void
}
Loading