Skip to content

Commit

Permalink
update with full backport
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Feb 21, 2025
1 parent 080ebf9 commit 3ae858f
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 23 deletions.
223 changes: 205 additions & 18 deletions src/runtime/internal/hash.ts
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
);
}
26 changes: 21 additions & 5 deletions test/unit/hash.test.ts
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));
});
}
});

0 comments on commit 3ae858f

Please sign in to comment.