Skip to content

Commit

Permalink
feat: determine staleness locally instead of globally (#933)
Browse files Browse the repository at this point in the history
  • Loading branch information
boschni authored Aug 31, 2020
1 parent d85f79b commit f3b2fe0
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 331 deletions.
1 change: 1 addition & 0 deletions docs/src/pages/docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Feature/Capability Key:
| Supported Query Keys | JSON | JSON | GraphQL Query |
| Query Key Change Detection | Deep Compare (Serialization) | Referential Equality (===) | Deep Compare (Serialization) |
| Query Data Memoization Level | Query + Structural Sharing | Query | Query + Entity + Structural Sharing |
| Stale While Revalidate | Server-Side + Client-Side | Server-Side | None |
| Bundle Size | [![][bp-react-query]][bpl-react-query] | [![][bp-swr]][bpl-swr] | [![][bp-apollo]][bpl-apollo] |
| Queries ||||
| Caching ||||
Expand Down
147 changes: 31 additions & 116 deletions src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export interface QueryState<TResult, TError> {
isFetchingMore: IsFetchingMoreValue
isIdle: boolean
isLoading: boolean
isStale: boolean
isSuccess: boolean
status: QueryStatus
throwInErrorBoundary?: boolean
Expand All @@ -66,7 +65,6 @@ export interface RefetchOptions {

export enum ActionType {
Failed = 'Failed',
MarkStale = 'MarkStale',
Fetch = 'Fetch',
Success = 'Success',
Error = 'Error',
Expand All @@ -76,10 +74,6 @@ interface FailedAction {
type: ActionType.Failed
}

interface MarkStaleAction {
type: ActionType.MarkStale
}

interface FetchAction {
type: ActionType.Fetch
isFetchingMore?: IsFetchingMoreValue
Expand All @@ -89,7 +83,6 @@ interface SuccessAction<TResult> {
type: ActionType.Success
data: TResult | undefined
canFetchMore?: boolean
isStale: boolean
}

interface ErrorAction<TError> {
Expand All @@ -101,7 +94,6 @@ export type Action<TResult, TError> =
| ErrorAction<TError>
| FailedAction
| FetchAction
| MarkStaleAction
| SuccessAction<TResult>

// CLASS
Expand All @@ -115,14 +107,11 @@ export class Query<TResult, TError> {

private queryCache: QueryCache
private promise?: Promise<TResult | undefined>
private cacheTimeout?: number
private staleTimeout?: number
private gcTimeout?: number
private cancelFetch?: () => void
private continueFetch?: () => void
private isTransportCancelable?: boolean
private notifyGlobalListeners: (query: Query<TResult, TError>) => void
private enableStaleTimeout: boolean
private enableGarbageCollectionTimeout: boolean

constructor(init: QueryInitConfig<TResult, TError>) {
this.config = init.config
Expand All @@ -132,23 +121,7 @@ export class Query<TResult, TError> {
this.notifyGlobalListeners = init.notifyGlobalListeners
this.observers = []
this.state = getDefaultState(init.config)
this.enableStaleTimeout = false
this.enableGarbageCollectionTimeout = false
}

activateStaleTimeout(): void {
this.enableStaleTimeout = true
this.rescheduleStaleTimeout()
}

activateGarbageCollectionTimeout(): void {
this.enableGarbageCollectionTimeout = true
this.rescheduleGarbageCollection()
}

activateTimeouts(): void {
this.activateStaleTimeout()
this.activateGarbageCollectionTimeout()
this.scheduleGc()
}

updateConfig(config: QueryConfig<TResult, TError>): void {
Expand All @@ -161,61 +134,18 @@ export class Query<TResult, TError> {
this.notifyGlobalListeners(this)
}

private rescheduleStaleTimeout(): void {
private scheduleGc(): void {
if (isServer) {
return
}

this.clearStaleTimeout()

if (
!this.enableStaleTimeout ||
this.state.isStale ||
this.state.status !== QueryStatus.Success ||
this.config.staleTime === Infinity
) {
return
}

const staleTime = this.config.staleTime || 0
let timeout = staleTime
if (this.state.updatedAt) {
const timeElapsed = Date.now() - this.state.updatedAt
const timeUntilStale = staleTime - timeElapsed
timeout = Math.max(timeUntilStale, 0)
}

this.staleTimeout = setTimeout(() => {
this.invalidate()
}, timeout)
}
this.clearGcTimeout()

invalidate(): void {
this.clearStaleTimeout()

if (this.state.isStale) {
if (this.config.cacheTime === Infinity || this.observers.length > 0) {
return
}

this.dispatch({ type: ActionType.MarkStale })
}

private rescheduleGarbageCollection(): void {
if (isServer) {
return
}

this.clearCacheTimeout()

if (
!this.enableGarbageCollectionTimeout ||
this.config.cacheTime === Infinity ||
this.observers.length > 0
) {
return
}

this.cacheTimeout = setTimeout(() => {
this.gcTimeout = setTimeout(() => {
this.clear()
}, this.config.cacheTime)
}
Expand All @@ -241,21 +171,14 @@ export class Query<TResult, TError> {

private clearTimersObservers(): void {
this.observers.forEach(observer => {
observer.clearRefetchInterval()
observer.clearTimers()
})
}

private clearStaleTimeout() {
if (this.staleTimeout) {
clearTimeout(this.staleTimeout)
this.staleTimeout = undefined
}
}

private clearCacheTimeout() {
if (this.cacheTimeout) {
clearTimeout(this.cacheTimeout)
this.cacheTimeout = undefined
private clearGcTimeout() {
if (this.gcTimeout) {
clearTimeout(this.gcTimeout)
this.gcTimeout = undefined
}
}

Expand All @@ -275,25 +198,19 @@ export class Query<TResult, TError> {
data = prevData
}

const isStale = this.config.staleTime === 0

// Try to determine if more data can be fetched
const canFetchMore = hasMorePages(this.config, data)

// Set data and mark it as cached
this.dispatch({
type: ActionType.Success,
data,
isStale,
canFetchMore,
})

this.rescheduleStaleTimeout()
}

clear(): void {
this.clearStaleTimeout()
this.clearCacheTimeout()
this.clearGcTimeout()
this.clearTimersObservers()
this.cancel()
delete this.queryCache.queries[this.queryHash]
Expand All @@ -304,12 +221,23 @@ export class Query<TResult, TError> {
return this.observers.some(observer => observer.config.enabled)
}

isStale(): boolean {
return this.observers.some(observer => observer.isStale())
}

isStaleByTime(staleTime = 0): boolean {
return (
!this.state.isSuccess || this.state.updatedAt + staleTime <= Date.now()
)
}

onWindowFocus(): void {
if (
this.state.isStale &&
this.observers.some(
observer =>
observer.config.enabled && observer.config.refetchOnWindowFocus
observer.isStale() &&
observer.config.enabled &&
observer.config.refetchOnWindowFocus
)
) {
this.fetch()
Expand All @@ -319,10 +247,11 @@ export class Query<TResult, TError> {

onOnline(): void {
if (
this.state.isStale &&
this.observers.some(
observer =>
observer.config.enabled && observer.config.refetchOnReconnect
observer.isStale() &&
observer.config.enabled &&
observer.config.refetchOnReconnect
)
) {
this.fetch()
Expand All @@ -348,7 +277,7 @@ export class Query<TResult, TError> {
this.observers.push(observer)

// Stop the query from being garbage collected
this.clearCacheTimeout()
this.clearGcTimeout()
}

unsubscribeObserver(observer: QueryObserver<TResult, TError>): void {
Expand All @@ -362,7 +291,7 @@ export class Query<TResult, TError> {
}
}

this.rescheduleGarbageCollection()
this.scheduleGc()
}

private async tryFetchData<T>(
Expand Down Expand Up @@ -639,12 +568,6 @@ function getDefaultState<TResult, TError>(

const hasInitialData = typeof initialData !== 'undefined'

const isStale =
!config.enabled ||
(typeof config.initialStale === 'function'
? config.initialStale()
: config.initialStale ?? !hasInitialData)

const initialStatus = hasInitialData
? QueryStatus.Success
: config.enabled
Expand All @@ -658,9 +581,8 @@ function getDefaultState<TResult, TError>(
isFetching: initialStatus === QueryStatus.Loading,
isFetchingMore: false,
failureCount: 0,
isStale,
data: initialData,
updatedAt: hasInitialData ? Date.now() : 0,
updatedAt: Date.now(),
canFetchMore: hasMorePages(config, initialData),
}
}
Expand All @@ -675,11 +597,6 @@ export function queryReducer<TResult, TError>(
...state,
failureCount: state.failureCount + 1,
}
case ActionType.MarkStale:
return {
...state,
isStale: true,
}
case ActionType.Fetch:
const status =
typeof state.data !== 'undefined'
Expand All @@ -698,7 +615,6 @@ export function queryReducer<TResult, TError>(
...getStatusProps(QueryStatus.Success),
data: action.data,
error: null,
isStale: action.isStale,
isFetched: true,
isFetching: false,
isFetchingMore: false,
Expand All @@ -714,7 +630,6 @@ export function queryReducer<TResult, TError>(
isFetched: true,
isFetching: false,
isFetchingMore: false,
isStale: true,
failureCount: state.failureCount + 1,
throwInErrorBoundary: true,
}
Expand Down
16 changes: 3 additions & 13 deletions src/core/queryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export class QueryCache {
}
}

return query.invalidate()
return undefined
})
)
} catch (err) {
Expand Down Expand Up @@ -305,7 +305,7 @@ export class QueryCache {
let query
try {
query = this.buildQuery<TResult, TError>(queryKey, configWithoutRetry)
if (options?.force || query.state.isStale) {
if (options?.force || query.isStaleByTime(config.staleTime)) {
await query.fetch()
}
return query.state.data
Expand All @@ -314,14 +314,6 @@ export class QueryCache {
throw error
}
return
} finally {
if (query) {
// When prefetching, no observer is tied to the query,
// so to avoid immediate garbage collection of the still
// empty query, we wait with activating timeouts until
// the prefetch is done
query.activateTimeouts()
}
}
}

Expand All @@ -337,13 +329,11 @@ export class QueryCache {
return
}

const newQuery = this.buildQuery<TResult, TError>(queryKey, {
this.buildQuery<TResult, TError>(queryKey, {
initialStale: typeof config?.staleTime === 'undefined',
initialData: functionalUpdate(updater, undefined),
...config,
})

newQuery.activateTimeouts()
}
}

Expand Down
Loading

1 comment on commit f3b2fe0

@vercel
Copy link

@vercel vercel bot commented on f3b2fe0 Aug 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.