Skip to content

Commit f4cd174

Browse files
authored
Stable hash (vercel#1429)
* code refactor * serialize object keys * (wip) serialization * add stable hash * add hash tests * optimize * support Number, String, Date * adjust the order * add regex * fix string encoding * reorganize code * reorganize code * handle objects and circular refs * correctly serialize date * adjust code flow * add support for object key * add test * code refactor
1 parent ec778e7 commit f4cd174

21 files changed

+234
-92
lines changed

immutable/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"types": "./dist/immutable",
77
"peerDependencies": {
88
"swr": "*",
9-
"react": "*",
10-
"dequal": "*"
9+
"react": "*"
1110
}
1211
}

infinite/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,10 @@ export const infinite = ((<Data, Error>(useSWRNext: SWRHook) => (
216216
let size
217217
if (isFunction(arg)) {
218218
size = arg(resolvePageSize())
219-
} else if (typeof arg === 'number') {
219+
} else if (typeof arg == 'number') {
220220
size = arg
221221
}
222-
if (typeof size !== 'number') return
222+
if (typeof size != 'number') return
223223

224224
cache.set(pageSizeCacheKey, size)
225225
lastPageSizeRef.current = size

infinite/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"types": "./dist/infinite",
77
"peerDependencies": {
88
"swr": "*",
9-
"react": "*",
10-
"dequal": "*"
9+
"react": "*"
1110
}
1211
}

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,5 @@
9898
"peerDependencies": {
9999
"react": "^16.11.0 || ^17.0.0"
100100
},
101-
"dependencies": {
102-
"dequal": "2.0.2"
103-
}
101+
"dependencies": {}
104102
}

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ type SWRHookWithMiddleware = <Data = any, Error = any>(
8989

9090
export type Middleware = (useSWRNext: SWRHook) => SWRHookWithMiddleware
9191

92-
export type ValueKey = string | any[] | null
92+
export type ValueKey = string | any[] | object | null
9393

9494
export type MutatorCallback<Data = any> = (
9595
currentValue?: Data

src/use-swr.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { IS_SERVER, rAF, useIsomorphicLayoutEffect } from './utils/env'
55
import { serialize } from './utils/serialize'
66
import { isUndefined, UNDEFINED, mergeObjects } from './utils/helper'
77
import ConfigProvider from './utils/config-context'
8-
import useStateWithDeps from './utils/state'
9-
import withArgs from './utils/resolve-args'
8+
import { useStateWithDeps } from './utils/state'
9+
import { withArgs } from './utils/resolve-args'
1010
import { subscribeCallback } from './utils/subscribe-key'
1111
import { broadcastState } from './utils/broadcast-state'
1212
import { getTimestamp } from './utils/timestamp'
@@ -349,7 +349,7 @@ export const useSWRHandler = <Data = any, Error = any>(
349349
isValidating: updatedIsValidating
350350
},
351351
// if data is undefined we should not update stateRef.current.data
352-
!compare(updatedData, stateRef.current.data)
352+
!compare(stateRef.current.data, updatedData)
353353
? {
354354
data: updatedData
355355
}
@@ -362,7 +362,7 @@ export const useSWRHandler = <Data = any, Error = any>(
362362
// revalidation from the outside.
363363
let nextFocusRevalidatedAt = 0
364364
const onRevalidate = (type: RevalidateEvent) => {
365-
if (type === revalidateEvents.FOCUS_EVENT) {
365+
if (type == revalidateEvents.FOCUS_EVENT) {
366366
const now = Date.now()
367367
if (
368368
getConfig().revalidateOnFocus &&
@@ -372,11 +372,11 @@ export const useSWRHandler = <Data = any, Error = any>(
372372
nextFocusRevalidatedAt = now + getConfig().focusThrottleInterval
373373
softRevalidate()
374374
}
375-
} else if (type === revalidateEvents.RECONNECT_EVENT) {
375+
} else if (type == revalidateEvents.RECONNECT_EVENT) {
376376
if (getConfig().revalidateOnReconnect && isActive()) {
377377
softRevalidate()
378378
}
379-
} else if (type === revalidateEvents.MUTATE_EVENT) {
379+
} else if (type == revalidateEvents.MUTATE_EVENT) {
380380
return revalidate()
381381
}
382382
return
@@ -444,7 +444,7 @@ export const useSWRHandler = <Data = any, Error = any>(
444444
(refreshWhenHidden || getConfig().isVisible()) &&
445445
(refreshWhenOffline || getConfig().isOnline())
446446
) {
447-
revalidate(WITH_DEDUPE).then(() => next())
447+
revalidate(WITH_DEDUPE).then(next)
448448
} else {
449449
// Schedule next interval to check again.
450450
next()

src/utils/cache.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ import {
1313
ConfigOptions
1414
} from '../types'
1515

16-
function revalidateAllKeys(
16+
const revalidateAllKeys = (
1717
revalidators: Record<string, RevalidateCallback[]>,
1818
type: RevalidateEvent
19-
) {
19+
) => {
2020
for (const key in revalidators) {
2121
if (revalidators[key][0]) revalidators[key][0](type)
2222
}
2323
}
2424

25-
export function initCache<Data = any>(
25+
export const initCache = <Data = any>(
2626
provider: Cache<Data>,
2727
options?: Partial<ConfigOptions>
28-
): [Cache<Data>, ScopedMutator<Data>] | undefined {
28+
): [Cache<Data>, ScopedMutator<Data>] | undefined => {
2929
// The global state for a specific provider will be used to deduplicate
3030
// requests and store listeners. As well as a mutate function that bound to
3131
// the cache.

src/utils/config.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { dequal } from 'dequal/lite'
2-
1+
import { stableHash } from './hash'
32
import { initCache } from './cache'
43
import { preset } from './web-preset'
54
import { slowConnection } from './env'
@@ -14,13 +13,13 @@ import {
1413
import { isUndefined, noop, mergeObjects } from './helper'
1514

1615
// error retry
17-
function onErrorRetry(
16+
const onErrorRetry = (
1817
_: unknown,
1918
__: string,
2019
config: Readonly<PublicConfiguration>,
2120
revalidate: Revalidator,
2221
opts: Required<RevalidatorOptions>
23-
): void {
22+
): void => {
2423
if (!preset.isVisible()) {
2524
// If it's hidden, stop. It will auto revalidate when refocusing.
2625
return
@@ -69,7 +68,8 @@ export const defaultConfig: FullConfiguration = mergeObjects(
6968
loadingTimeout: slowConnection ? 5000 : 3000,
7069

7170
// providers
72-
compare: dequal,
71+
compare: (currentData: any, newData: any) =>
72+
stableHash(currentData) == stableHash(newData),
7373
isPaused: () => false,
7474
cache,
7575
mutate,

src/utils/env.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { useEffect, useLayoutEffect } from 'react'
2+
import { hasWindow } from './helper'
23

3-
export const IS_SERVER = typeof window === 'undefined' || 'Deno' in window
4+
export const IS_SERVER = !hasWindow || 'Deno' in window
45

5-
const __requestAnimationFrame = !IS_SERVER
6-
? window['requestAnimationFrame']
7-
: null
8-
9-
// polyfill for requestAnimationFrame
10-
export const rAF = __requestAnimationFrame
11-
? (f: FrameRequestCallback) => __requestAnimationFrame(f)
12-
: (f: (...args: any[]) => void) => setTimeout(f, 1)
6+
// Polyfill requestAnimationFrame
7+
export const rAF =
8+
(hasWindow && window['requestAnimationFrame']) ||
9+
((f: (...args: any[]) => void) => setTimeout(f, 1))
1310

1411
// React currently throws a warning when using useLayoutEffect on the server.
1512
// To get around it, we can conditionally useEffect on the server (no-op) and

src/utils/hash.ts

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,70 @@
1-
import { isFunction, UNDEFINED } from './helper'
1+
import { isUndefined } from './helper'
22

33
// use WeakMap to store the object->key mapping
44
// so the objects can be garbage collected.
55
// WeakMap uses a hashtable under the hood, so the lookup
66
// complexity is almost O(1).
7-
const table = new WeakMap()
7+
const table = new WeakMap<object, number | string>()
88

99
// counter of the key
1010
let counter = 0
1111

12-
// hashes an array of objects and returns a string
13-
export default function hash(args: any[]): string {
14-
if (!args.length) return ''
15-
let key = 'arg'
16-
for (let i = 0; i < args.length; ++i) {
17-
const arg = args[i]
18-
19-
let _hash: any = UNDEFINED
20-
if (arg === null || (typeof arg !== 'object' && !isFunction(arg))) {
21-
// need to consider the case that `arg` is a string:
22-
// "undefined" -> '"undefined"'
23-
// 123 -> '123'
24-
// "null" -> '"null"'
25-
// null -> 'null'
26-
_hash = JSON.stringify(arg)
27-
} else {
28-
if (!table.has(arg)) {
29-
_hash = counter
30-
table.set(arg, counter++)
31-
} else {
32-
_hash = table.get(arg)
12+
// A stable hash implementation that supports:
13+
// - Fast and ensures unique hash properties
14+
// - Handles unserializable values
15+
// - Handles object key ordering
16+
// - Generates short results
17+
//
18+
// This is not a serialization function, and the result is not guaranteed to be
19+
// parsible.
20+
export const stableHash = (arg: any): string => {
21+
const type = typeof arg
22+
const constructor = arg && arg.constructor
23+
const isDate = constructor == Date
24+
25+
let result: any
26+
let index: any
27+
28+
if (Object(arg) === arg && !isDate && constructor != RegExp) {
29+
// Object/function, not null/date/regexp. Use WeakMap to store the id first.
30+
// If it's already hashed, directly return the result.
31+
result = table.get(arg)
32+
if (result) return result
33+
34+
// Store the hash first for circular reference detection before entering the
35+
// recursive `stableHash` calls.
36+
// For other objects like set and map, we use this id directly as the hash.
37+
result = ++counter + '~'
38+
table.set(arg, result)
39+
40+
if (constructor == Array) {
41+
// Array.
42+
result = '@'
43+
for (index = 0; index < arg.length; index++) {
44+
result += stableHash(arg[index]) + ','
3345
}
46+
table.set(arg, result)
3447
}
35-
key += '$' + _hash
48+
if (constructor == Object) {
49+
// Object, sort keys.
50+
result = '#'
51+
const keys = Object.keys(arg).sort()
52+
while (!isUndefined((index = keys.pop() as string))) {
53+
if (!isUndefined(arg[index])) {
54+
result += index + ':' + stableHash(arg[index]) + ','
55+
}
56+
}
57+
table.set(arg, result)
58+
}
59+
} else {
60+
result = isDate
61+
? arg.toJSON()
62+
: type == 'symbol'
63+
? arg.toString()
64+
: type == 'string'
65+
? JSON.stringify(arg)
66+
: '' + arg
3667
}
37-
return key
68+
69+
return result
3870
}

0 commit comments

Comments
 (0)