-
Notifications
You must be signed in to change notification settings - Fork 563
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
226 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,218 @@ | ||
import { digest, serialize } from "ohash"; | ||
import { digest } from "ohash"; | ||
|
||
// hash implementation compatible with ohash v1 to reduce cache invalidation in same semver Nitro versions | ||
|
||
export function hash(value: any) { | ||
return digest(compatSerialize(value)).replace(/[-_]/g, "").slice(0, 10); | ||
return digest(typeof value === "string" ? value : serialize(value)) | ||
.replace(/[-_]/g, "") | ||
.slice(0, 10); | ||
} | ||
|
||
function compatSerialize(value: any): string { | ||
if (value === null) { | ||
return "Null"; | ||
/** | ||
Source: https://github.com/unjs/ohash/blob/v1/src/object-hash.ts | ||
Based on https://github.com/puleos/object-hash v3.0.0 (MIT) | ||
*/ | ||
|
||
export function serialize(object: any): string { | ||
const hasher = new Hasher(); | ||
hasher.dispatch(object); | ||
return hasher.buff; | ||
} | ||
|
||
class Hasher { | ||
buff = ""; | ||
#context = new Map(); | ||
|
||
write(str: string) { | ||
this.buff += str; | ||
} | ||
|
||
dispatch(value: any): string | void { | ||
const type = value === null ? "null" : typeof value; | ||
return this[type](value); | ||
} | ||
object(object: any): string | void { | ||
if (object && typeof object.toJSON === "function") { | ||
return this.object(object.toJSON()); | ||
} | ||
|
||
const objString = Object.prototype.toString.call(object); | ||
|
||
let objType = ""; | ||
const objectLength = objString.length; | ||
|
||
// '[object a]'.length === 10, the minimum | ||
objType = | ||
objectLength < 10 | ||
? "unknown:[" + objString + "]" | ||
: objString.slice(8, objectLength - 1); | ||
|
||
objType = objType.toLowerCase(); | ||
|
||
let objectNumber = null; | ||
|
||
if ((objectNumber = this.#context.get(object)) === undefined) { | ||
this.#context.set(object, this.#context.size); | ||
} else { | ||
return this.dispatch("[CIRCULAR:" + objectNumber + "]"); | ||
} | ||
|
||
if ( | ||
typeof Buffer !== "undefined" && | ||
Buffer.isBuffer && | ||
Buffer.isBuffer(object) | ||
) { | ||
this.write("buffer:"); | ||
return this.write(object.toString("utf8")); | ||
} | ||
|
||
if ( | ||
objType !== "object" && | ||
objType !== "function" && | ||
objType !== "asyncfunction" | ||
) { | ||
// @ts-ignore | ||
if (this[objType]) { | ||
// @ts-ignore | ||
this[objType](object); | ||
} else { | ||
this.unknown(object, objType); | ||
} | ||
} else { | ||
const keys = Object.keys(object).sort(); | ||
const extraKeys = [] as readonly string[]; | ||
this.write("object:" + (keys.length + extraKeys.length) + ":"); | ||
const dispatchForKey = (key: string) => { | ||
this.dispatch(key); | ||
this.write(":"); | ||
this.dispatch(object[key]); | ||
this.write(","); | ||
}; | ||
for (const key of keys) { | ||
dispatchForKey(key); | ||
} | ||
for (const key of extraKeys) { | ||
dispatchForKey(key); | ||
} | ||
} | ||
} | ||
array(arr: any, unordered: boolean): string | void { | ||
unordered = unordered === undefined ? false : unordered; | ||
this.write("array:" + arr.length + ":"); | ||
if (!unordered || arr.length <= 1) { | ||
for (const entry of arr) { | ||
this.dispatch(entry); | ||
} | ||
return; | ||
} | ||
const contextAdditions = new Map(); | ||
const entries = arr.map((entry: any) => { | ||
const hasher = new Hasher(); | ||
hasher.dispatch(entry); | ||
for (const [key, value] of hasher.#context) { | ||
contextAdditions.set(key, value); | ||
} | ||
return hasher.toString(); | ||
}); | ||
this.#context = contextAdditions; | ||
entries.sort(); | ||
return this.array(entries, false); | ||
} | ||
date(date: any) { | ||
return this.write("date:" + date.toJSON()); | ||
} | ||
const type = typeof value; | ||
if (type === "string") { | ||
return value; | ||
symbol(sym: any) { | ||
return this.write("symbol:" + sym.toString()); | ||
} | ||
if (type === "number") { | ||
return `number:${value}`; | ||
unknown(value: any, type: string) { | ||
this.write(type); | ||
if (!value) { | ||
return; | ||
} | ||
this.write(":"); | ||
if (value && typeof value.entries === "function") { | ||
return this.array([...value.entries()], true /* ordered */); | ||
} | ||
} | ||
if (type === "boolean") { | ||
return `bool:${value}`; | ||
error(err: any) { | ||
return this.write("error:" + err.toString()); | ||
} | ||
if (type === "undefined") { | ||
return "Undefined"; | ||
boolean(bool: any) { | ||
return this.write("bool:" + bool); | ||
} | ||
const serialized = serialize(value); | ||
if (serialized === "{}") { | ||
return "object:0:"; | ||
string(string: any) { | ||
this.write("string:" + string.length + ":"); | ||
this.write(string); | ||
} | ||
function(fn: any) { | ||
this.write("fn:"); | ||
if (isNativeFunction(fn)) { | ||
this.dispatch("[native]"); | ||
} else { | ||
this.dispatch(fn.toString()); | ||
} | ||
} | ||
number(number: any) { | ||
return this.write("number:" + number); | ||
} | ||
null() { | ||
return this.write("Null"); | ||
} | ||
undefined() { | ||
return this.write("Undefined"); | ||
} | ||
regexp(regex: any) { | ||
return this.write("regex:" + regex.toString()); | ||
} | ||
arraybuffer(arr: any) { | ||
this.write("arraybuffer:"); | ||
return this.dispatch(new Uint8Array(arr)); | ||
} | ||
url(url: any) { | ||
return this.write("url:" + url.toString()); | ||
} | ||
map(map: any) { | ||
this.write("map:"); | ||
const arr = [...map]; | ||
return this.array(arr, false); | ||
} | ||
set(set: any) { | ||
this.write("set:"); | ||
const arr = [...set]; | ||
return this.array(arr, false); | ||
} | ||
bigint(number: number) { | ||
return this.write("bigint:" + number.toString()); | ||
} | ||
} | ||
|
||
for (const type of [ | ||
"uint8array", | ||
"uint8clampedarray", | ||
"unt8array", | ||
"uint16array", | ||
"unt16array", | ||
"uint32array", | ||
"unt32array", | ||
"float32array", | ||
"float64array", | ||
]) { | ||
// @ts-ignore | ||
Hasher.prototype[type] = function (arr: any) { | ||
this.write(type + ":"); | ||
return this.array([...arr], false); | ||
}; | ||
} | ||
|
||
const nativeFunc = "[native code] }"; | ||
const nativeFuncLength = nativeFunc.length; | ||
|
||
/** Check if the given function is a native function */ | ||
function isNativeFunction(f: any) { | ||
if (typeof f !== "function") { | ||
return false; | ||
} | ||
return serialized; | ||
return ( | ||
Function.prototype.toString.call(f).slice(-nativeFuncLength) === nativeFunc | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,28 @@ | ||
import { describe, expect, it, vi } from "vitest"; | ||
import { hash as ohashV1 } from "ohash-v1"; | ||
import { hash } from "../../src/runtime/internal/hash"; | ||
import { hash as ohashV1, objectHash } from "ohash-v1"; | ||
import { hash, serialize } from "../../src/runtime/internal/hash"; | ||
|
||
describe("cache: hash consistency", async () => { | ||
const inputs = ["test", 123, true, false, null, undefined, {}]; | ||
const inputs = [ | ||
"test", | ||
123, | ||
true, | ||
false, | ||
null, | ||
undefined, | ||
{}, | ||
{ foo: "bar" }, | ||
new Uint8Array(0), | ||
new Uint8Array([1, 2, 3]), | ||
[1, "test", true], | ||
Buffer.from("test"), | ||
]; | ||
for (const input of inputs) { | ||
it(`${input}`, () => { | ||
expect(hash(input)).toBe(ohashV1(input)); | ||
it(JSON.stringify(input), () => { | ||
expect( | ||
hash(input), | ||
`${serialize(input)} should be ${objectHash(input)}` | ||
).toBe(ohashV1(input)); | ||
}); | ||
} | ||
}); |