|
1 | | -import { isFunction, UNDEFINED } from './helper' |
| 1 | +import { isUndefined } from './helper' |
2 | 2 |
|
3 | 3 | // use WeakMap to store the object->key mapping |
4 | 4 | // so the objects can be garbage collected. |
5 | 5 | // WeakMap uses a hashtable under the hood, so the lookup |
6 | 6 | // complexity is almost O(1). |
7 | | -const table = new WeakMap() |
| 7 | +const table = new WeakMap<object, number | string>() |
8 | 8 |
|
9 | 9 | // counter of the key |
10 | 10 | let counter = 0 |
11 | 11 |
|
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]) + ',' |
33 | 45 | } |
| 46 | + table.set(arg, result) |
34 | 47 | } |
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 |
36 | 67 | } |
37 | | - return key |
| 68 | + |
| 69 | + return result |
38 | 70 | } |
0 commit comments