Skip to content

Commit

Permalink
refactor: switch to useSyncExternalStoreWithSelector (vercel#1953)
Browse files Browse the repository at this point in the history
* type: extends useConfig cache interface (vercel#1938)

* remove mount check for react18 (vercel#1927)

reactwg/react-18#82

related pr vercel#787 vercel#433

* lint: dont check unused vars with underscore prefix (vercel#1939)

* test: upgrade to jest 28 (vercel#1942)

* Upgrade to jest 28

* Upgrade to jest 28

* feat: useSyncExternalStoreWithSelector

* refactor: remove stateUpdate and boardcast

state update should be handled by uSESW

* type: fix test type error

* remove pnpm.lock

* fix: import cjs for codesanbox

* refactor: add selector

* refactor: add cachestate interface and try fix custom cache

* fix: custom cache init

* refactor: remove useless flag

* chore: codesanbox ci

* refactor: remove force render in infinite

* build: add _internal

* chore: mark warning test

* fix: dts generation

* codesanbox ci

* chore: rename swr folder to core

Co-authored-by: Jiachi Liu <inbox@huozhi.im>
  • Loading branch information
promer94 and huozhi committed May 13, 2022
1 parent e03e1e2 commit 892b7fa
Show file tree
Hide file tree
Showing 54 changed files with 534 additions and 7,555 deletions.
File renamed without changes.
24 changes: 24 additions & 0 deletions _internal/index.ts
@@ -0,0 +1,24 @@
import SWRConfig from './utils/config-context'
import * as revalidateEvents from './constants'

export { SWRConfig, revalidateEvents }

export { initCache } from './utils/cache'
export { defaultConfig, cache, mutate, compare } from './utils/config'
export * from './utils/env'
export { SWRGlobalState } from './utils/global-state'
export { stableHash } from './utils/hash'
export * from './utils/helper'
export { mergeConfigs } from './utils/merge-config'
export { internalMutate } from './utils/mutate'
export { normalize } from './utils/normalize-args'
export { withArgs } from './utils/resolve-args'
export { serialize } from './utils/serialize'
export { useStateWithDeps } from './utils/state'
export { subscribeCallback } from './utils/subscribe-key'
export { getTimestamp } from './utils/timestamp'
export { useSWRConfig } from './utils/use-swr-config'
export { preset, defaultConfigOptions } from './utils/web-preset'
export { withMiddleware } from './utils/with-middleware'

export * from './types'
11 changes: 11 additions & 0 deletions _internal/package.json
@@ -0,0 +1,11 @@
{
"name": "swr-internal",
"version": "0.0.1",
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"types": "./dist/_internal",
"exports": "./dist/index.mjs",
"peerDependencies": {
"react": "*"
}
}
8 changes: 8 additions & 0 deletions _internal/tsconfig.json
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"outDir": "./dist"
},
"include": ["./**/*.ts"]
}
36 changes: 28 additions & 8 deletions src/types.ts → _internal/types.ts
@@ -1,6 +1,14 @@
import * as revalidateEvents from './constants'
import { defaultConfig } from './utils/config'

export type GlobalState = [
Record<string, RevalidateCallback[]>, // EVENT_REVALIDATORS
Record<string, [number, number]>, // MUTATION: [ts, end_ts]
Record<string, [any, number]>, // FETCH: [data, ts]
ScopedMutator, // Mutator
(key: string, value: any, prev: any) => void, // Setter
(key: string, callback: (current: any, prev: any) => void) => () => void // Subscriber
]
export type FetcherResponse<Data = unknown> = Data | Promise<Data>
export type BareFetcher<Data = unknown> = (
...args: any[]
Expand Down Expand Up @@ -144,7 +152,9 @@ export type MutatorCallback<Data = any> = (

export type MutatorOptions<Data = any> = {
revalidate?: boolean
populateCache?: boolean | ((result: any, currentData?: Data) => Data)
populateCache?:
| boolean
| ((result: any, currentData: Data | undefined) => Data)
optimisticData?: Data | ((currentData?: Data) => Data)
rollbackOnError?: boolean
}
Expand Down Expand Up @@ -255,12 +265,22 @@ export type RevalidateCallback = <K extends RevalidateEvent>(
type: K
) => RevalidateCallbackReturnType[K]

export type StateUpdateCallback<Data = any, Error = any> = (
state: State<Data, Error>
) => void

export interface Cache<Data = any, Error = any> {
get(key: Key): State<Data, Error> | undefined
set(key: Key, value: State<Data, Error>): void
export interface Cache<Data = any> {
get(key: Key): Data | null | undefined
set(key: Key, value: Data): void
delete(key: Key): void
}

export interface SWRCacheResult<Data = any, Error = any> {
data?: Data
error?: Error
isValidating?: boolean
isLoading?: boolean
}

export interface StateDependencies {
data?: boolean
error?: boolean
isValidating?: boolean
isLoading?: boolean
}
127 changes: 56 additions & 71 deletions src/utils/cache.ts → _internal/utils/cache.ts
@@ -1,20 +1,19 @@
import { defaultConfigOptions } from './web-preset'
import { IS_SERVER } from './env'
import { UNDEFINED, mergeObjects, noop, isUndefined } from './helper'
import { UNDEFINED, mergeObjects, noop } from './helper'
import { internalMutate } from './mutate'
import { GlobalState, SWRGlobalState } from './global-state'
import { SWRGlobalState } from './global-state'
import * as revalidateEvents from '../constants'

import {
Key,
Cache,
State,
ScopedMutator,
RevalidateEvent,
RevalidateCallback,
ProviderConfiguration
ProviderConfiguration,
GlobalState
} from '../types'
import { compare } from './config'

const revalidateAllKeys = (
revalidators: Record<string, RevalidateCallback[]>,
Expand All @@ -29,7 +28,7 @@ export const initCache = <Data = any>(
provider: Cache<Data>,
options?: Partial<ProviderConfiguration>
):
| [Cache<Data>, ScopedMutator<Data>, () => void]
| [Cache<Data>, ScopedMutator<Data>, () => void, () => void]
| [Cache<Data>, ScopedMutator<Data>]
| undefined => {
// The global state for a specific provider will be used to deduplicate
Expand Down Expand Up @@ -66,7 +65,6 @@ export const initCache = <Data = any>(
}
const setter = (key: string, value: any, prev: any) => {
provider.set(key, value)

const subs = subscriptions[key]
if (subs) {
for (let i = subs.length; i--; ) {
Expand All @@ -75,78 +73,65 @@ export const initCache = <Data = any>(
}
}

// Update the state if it's new, or the provider has been extended.
SWRGlobalState.set(provider, [
EVENT_REVALIDATORS,
{},
{},
{},
mutate,
setter,
subscribe
])

// This is a new provider, we need to initialize it and setup DOM events
// listeners for `focus` and `reconnect` actions.
if (!IS_SERVER) {
// When listening to the native events for auto revalidations,
// we intentionally put a delay (setTimeout) here to make sure they are
// fired after immediate JavaScript executions, which can possibly be
// React's state updates.
// This avoids some unnecessary revalidations such as
// https://github.com/vercel/swr/issues/1680.
const releaseFocus = opts.initFocus(
setTimeout.bind(
UNDEFINED,
revalidateAllKeys.bind(
UNDEFINED,
EVENT_REVALIDATORS,
revalidateEvents.FOCUS_EVENT
const initProvider = () => {
if (!SWRGlobalState.has(provider)) {
// Update the state if it's new, or the provider has been extended.
SWRGlobalState.set(provider, [
EVENT_REVALIDATORS,
{},
{},
mutate,
setter,
subscribe
])
if (!IS_SERVER) {
// When listening to the native events for auto revalidations,
// we intentionally put a delay (setTimeout) here to make sure they are
// fired after immediate JavaScript executions, which can possibly be
// React's state updates.
// This avoids some unnecessary revalidations such as
// https://github.com/vercel/swr/issues/1680.
const releaseFocus = opts.initFocus(
setTimeout.bind(
UNDEFINED,
revalidateAllKeys.bind(
UNDEFINED,
EVENT_REVALIDATORS,
revalidateEvents.FOCUS_EVENT
)
)
)
)
)
const releaseReconnect = opts.initReconnect(
setTimeout.bind(
UNDEFINED,
revalidateAllKeys.bind(
UNDEFINED,
EVENT_REVALIDATORS,
revalidateEvents.RECONNECT_EVENT
const releaseReconnect = opts.initReconnect(
setTimeout.bind(
UNDEFINED,
revalidateAllKeys.bind(
UNDEFINED,
EVENT_REVALIDATORS,
revalidateEvents.RECONNECT_EVENT
)
)
)
)
)
unmount = () => {
releaseFocus && releaseFocus()
releaseReconnect && releaseReconnect()

// When un-mounting, we need to remove the cache provider from the state
// storage too because it's a side-effect. Otherwise when re-mounting we
// will not re-register those event listeners.
SWRGlobalState.delete(provider)
unmount = () => {
releaseFocus && releaseFocus()
releaseReconnect && releaseReconnect()
// When un-mounting, we need to remove the cache provider from the state
// storage too because it's a side-effect. Otherwise when re-mounting we
// will not re-register those event listeners.
SWRGlobalState.delete(provider)
}
}
}
}
initProvider()

// This is a new provider, we need to initialize it and setup DOM events
// listeners for `focus` and `reconnect` actions.

// 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, mutate, unmount]
return [provider, mutate, initProvider, unmount]
}

return [provider, (SWRGlobalState.get(provider) as GlobalState)[4]]
}

const EMPTY_CACHE = {}
export const createCacheHelper = <Data = any, ExtendedInfo = {}>(
cache: Cache,
key: Key
) => {
const state = SWRGlobalState.get(cache) as GlobalState
return [
// Getter
() => (cache.get(key) || {}) as State<Data, any> & Partial<ExtendedInfo>,
// Setter
(info: Partial<State<Data, any> | ExtendedInfo>) => {
cache.set(key, mergeObjects(cache.get(key), info))
}
] as const
return [provider, (SWRGlobalState.get(provider) as GlobalState)[3]]
}
Expand Up @@ -50,10 +50,12 @@ const SWRConfig: FC<
}

// Unsubscribe events.
useIsomorphicLayoutEffect(
() => (cacheContext ? cacheContext[2] : UNDEFINED),
[]
)
useIsomorphicLayoutEffect(() => {
if (cacheContext) {
cacheContext[2] && cacheContext[2]()
return cacheContext[3]
}
}, [])

return createElement(
SWRConfigContext.Provider,
Expand Down
6 changes: 1 addition & 5 deletions src/utils/config.ts → _internal/utils/config.ts
Expand Up @@ -41,11 +41,7 @@ const compare = (currentData: any, newData: any) =>
stableHash(currentData) == stableHash(newData)

// Default cache provider
const [cache, mutate] = initCache(new Map()) as [
Cache<any>,
ScopedMutator<any>,
() => {}
]
const [cache, mutate] = initCache(new Map()) as [Cache<any>, ScopedMutator<any>]
export { cache, mutate, compare }

// Default config
Expand Down
2 changes: 1 addition & 1 deletion src/utils/env.ts → _internal/utils/env.ts
@@ -1,7 +1,7 @@
import React, { useEffect, useLayoutEffect } from 'react'
import { hasRequestAnimationFrame, isWindowDefined } from './helper'

// @ts-expect-error TODO: should remove this when the default react version is 18

export const IS_REACT_LEGACY = !React.useId

export const IS_SERVER = !isWindowDefined || 'Deno' in window
Expand Down
4 changes: 4 additions & 0 deletions _internal/utils/global-state.ts
@@ -0,0 +1,4 @@
import { Cache, GlobalState } from '../types'

// Global state used to deduplicate requests and store listeners
export const SWRGlobalState = new WeakMap<Cache, GlobalState>()
File renamed without changes.
22 changes: 22 additions & 0 deletions src/utils/helper.ts → _internal/utils/helper.ts
@@ -1,3 +1,5 @@
import { SWRGlobalState } from './global-state'
import { Key, Cache, SWRCacheResult, GlobalState } from '../types'
export const noop = () => {}

// Using noop() as the undefined value as undefined can possibly be replaced
Expand All @@ -10,6 +12,7 @@ export const OBJECT = Object

export const isUndefined = (v: any): v is undefined => v === UNDEFINED
export const isFunction = (v: any): v is Function => typeof v == 'function'
export const isEmptyCache = (v: any): boolean => v === EMPTY_CACHE
export const mergeObjects = (a: any, b: any) => OBJECT.assign({}, a, b)

const STR_UNDEFINED = 'undefined'
Expand All @@ -19,3 +22,22 @@ export const isWindowDefined = typeof window != STR_UNDEFINED
export const isDocumentDefined = typeof document != STR_UNDEFINED
export const hasRequestAnimationFrame = () =>
isWindowDefined && typeof window['requestAnimationFrame'] != STR_UNDEFINED

const EMPTY_CACHE = {}
export const createCacheHelper = <Data = any, T = SWRCacheResult<Data, any>>(
cache: Cache,
key: Key
) => {
const state = SWRGlobalState.get(cache) as GlobalState
return [
// Getter
() => (cache.get(key) || EMPTY_CACHE) as T,
// Setter
(info: T) => {
const prev = cache.get(key)
state[4](key as string, mergeObjects(prev, info), prev || EMPTY_CACHE)
},
// Subscriber
state[5]
] as const
}
File renamed without changes.

0 comments on commit 892b7fa

Please sign in to comment.