433 lines
11 KiB
JavaScript
433 lines
11 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
const defaults = Object.freeze({
|
||
|
|
ignoreUnknown: false,
|
||
|
|
respectType: false,
|
||
|
|
respectFunctionNames: false,
|
||
|
|
respectFunctionProperties: false,
|
||
|
|
unorderedObjects: true,
|
||
|
|
unorderedArrays: false,
|
||
|
|
unorderedSets: false,
|
||
|
|
excludeKeys: void 0,
|
||
|
|
excludeValues: void 0,
|
||
|
|
replacer: void 0
|
||
|
|
});
|
||
|
|
function objectHash(object, options) {
|
||
|
|
if (options) {
|
||
|
|
options = { ...defaults, ...options };
|
||
|
|
} else {
|
||
|
|
options = defaults;
|
||
|
|
}
|
||
|
|
const hasher = createHasher(options);
|
||
|
|
hasher.dispatch(object);
|
||
|
|
return hasher.toString();
|
||
|
|
}
|
||
|
|
const defaultPrototypesKeys = Object.freeze([
|
||
|
|
"prototype",
|
||
|
|
"__proto__",
|
||
|
|
"constructor"
|
||
|
|
]);
|
||
|
|
function createHasher(options) {
|
||
|
|
let buff = "";
|
||
|
|
let context = /* @__PURE__ */ new Map();
|
||
|
|
const write = (str) => {
|
||
|
|
buff += str;
|
||
|
|
};
|
||
|
|
return {
|
||
|
|
toString() {
|
||
|
|
return buff;
|
||
|
|
},
|
||
|
|
getContext() {
|
||
|
|
return context;
|
||
|
|
},
|
||
|
|
dispatch(value) {
|
||
|
|
if (options.replacer) {
|
||
|
|
value = options.replacer(value);
|
||
|
|
}
|
||
|
|
const type = value === null ? "null" : typeof value;
|
||
|
|
return this[type](value);
|
||
|
|
},
|
||
|
|
object(object) {
|
||
|
|
if (object && typeof object.toJSON === "function") {
|
||
|
|
return this.object(object.toJSON());
|
||
|
|
}
|
||
|
|
const objString = Object.prototype.toString.call(object);
|
||
|
|
let objType = "";
|
||
|
|
const objectLength = objString.length;
|
||
|
|
if (objectLength < 10) {
|
||
|
|
objType = "unknown:[" + objString + "]";
|
||
|
|
} else {
|
||
|
|
objType = objString.slice(8, objectLength - 1);
|
||
|
|
}
|
||
|
|
objType = objType.toLowerCase();
|
||
|
|
let objectNumber = null;
|
||
|
|
if ((objectNumber = context.get(object)) === void 0) {
|
||
|
|
context.set(object, context.size);
|
||
|
|
} else {
|
||
|
|
return this.dispatch("[CIRCULAR:" + objectNumber + "]");
|
||
|
|
}
|
||
|
|
if (typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(object)) {
|
||
|
|
write("buffer:");
|
||
|
|
return write(object.toString("utf8"));
|
||
|
|
}
|
||
|
|
if (objType !== "object" && objType !== "function" && objType !== "asyncfunction") {
|
||
|
|
if (this[objType]) {
|
||
|
|
this[objType](object);
|
||
|
|
} else if (!options.ignoreUnknown) {
|
||
|
|
this.unkown(object, objType);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
let keys = Object.keys(object);
|
||
|
|
if (options.unorderedObjects) {
|
||
|
|
keys = keys.sort();
|
||
|
|
}
|
||
|
|
let extraKeys = [];
|
||
|
|
if (options.respectType !== false && !isNativeFunction(object)) {
|
||
|
|
extraKeys = defaultPrototypesKeys;
|
||
|
|
}
|
||
|
|
if (options.excludeKeys) {
|
||
|
|
keys = keys.filter((key) => {
|
||
|
|
return !options.excludeKeys(key);
|
||
|
|
});
|
||
|
|
extraKeys = extraKeys.filter((key) => {
|
||
|
|
return !options.excludeKeys(key);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
write("object:" + (keys.length + extraKeys.length) + ":");
|
||
|
|
const dispatchForKey = (key) => {
|
||
|
|
this.dispatch(key);
|
||
|
|
write(":");
|
||
|
|
if (!options.excludeValues) {
|
||
|
|
this.dispatch(object[key]);
|
||
|
|
}
|
||
|
|
write(",");
|
||
|
|
};
|
||
|
|
for (const key of keys) {
|
||
|
|
dispatchForKey(key);
|
||
|
|
}
|
||
|
|
for (const key of extraKeys) {
|
||
|
|
dispatchForKey(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
array(arr, unordered) {
|
||
|
|
unordered = unordered === void 0 ? options.unorderedArrays !== false : unordered;
|
||
|
|
write("array:" + arr.length + ":");
|
||
|
|
if (!unordered || arr.length <= 1) {
|
||
|
|
for (const entry of arr) {
|
||
|
|
this.dispatch(entry);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const contextAdditions = /* @__PURE__ */ new Map();
|
||
|
|
const entries = arr.map((entry) => {
|
||
|
|
const hasher = createHasher(options);
|
||
|
|
hasher.dispatch(entry);
|
||
|
|
for (const [key, value] of hasher.getContext()) {
|
||
|
|
contextAdditions.set(key, value);
|
||
|
|
}
|
||
|
|
return hasher.toString();
|
||
|
|
});
|
||
|
|
context = contextAdditions;
|
||
|
|
entries.sort();
|
||
|
|
return this.array(entries, false);
|
||
|
|
},
|
||
|
|
date(date) {
|
||
|
|
return write("date:" + date.toJSON());
|
||
|
|
},
|
||
|
|
symbol(sym) {
|
||
|
|
return write("symbol:" + sym.toString());
|
||
|
|
},
|
||
|
|
unkown(value, type) {
|
||
|
|
write(type);
|
||
|
|
if (!value) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
write(":");
|
||
|
|
if (value && typeof value.entries === "function") {
|
||
|
|
return this.array(
|
||
|
|
Array.from(value.entries()),
|
||
|
|
true
|
||
|
|
/* ordered */
|
||
|
|
);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
error(err) {
|
||
|
|
return write("error:" + err.toString());
|
||
|
|
},
|
||
|
|
boolean(bool) {
|
||
|
|
return write("bool:" + bool);
|
||
|
|
},
|
||
|
|
string(string) {
|
||
|
|
write("string:" + string.length + ":");
|
||
|
|
write(string);
|
||
|
|
},
|
||
|
|
function(fn) {
|
||
|
|
write("fn:");
|
||
|
|
if (isNativeFunction(fn)) {
|
||
|
|
this.dispatch("[native]");
|
||
|
|
} else {
|
||
|
|
this.dispatch(fn.toString());
|
||
|
|
}
|
||
|
|
if (options.respectFunctionNames !== false) {
|
||
|
|
this.dispatch("function-name:" + String(fn.name));
|
||
|
|
}
|
||
|
|
if (options.respectFunctionProperties) {
|
||
|
|
this.object(fn);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
number(number) {
|
||
|
|
return write("number:" + number);
|
||
|
|
},
|
||
|
|
xml(xml) {
|
||
|
|
return write("xml:" + xml.toString());
|
||
|
|
},
|
||
|
|
null() {
|
||
|
|
return write("Null");
|
||
|
|
},
|
||
|
|
undefined() {
|
||
|
|
return write("Undefined");
|
||
|
|
},
|
||
|
|
regexp(regex) {
|
||
|
|
return write("regex:" + regex.toString());
|
||
|
|
},
|
||
|
|
uint8array(arr) {
|
||
|
|
write("uint8array:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
uint8clampedarray(arr) {
|
||
|
|
write("uint8clampedarray:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
int8array(arr) {
|
||
|
|
write("int8array:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
uint16array(arr) {
|
||
|
|
write("uint16array:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
int16array(arr) {
|
||
|
|
write("int16array:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
uint32array(arr) {
|
||
|
|
write("uint32array:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
int32array(arr) {
|
||
|
|
write("int32array:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
float32array(arr) {
|
||
|
|
write("float32array:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
float64array(arr) {
|
||
|
|
write("float64array:");
|
||
|
|
return this.dispatch(Array.prototype.slice.call(arr));
|
||
|
|
},
|
||
|
|
arraybuffer(arr) {
|
||
|
|
write("arraybuffer:");
|
||
|
|
return this.dispatch(new Uint8Array(arr));
|
||
|
|
},
|
||
|
|
url(url) {
|
||
|
|
return write("url:" + url.toString());
|
||
|
|
},
|
||
|
|
map(map) {
|
||
|
|
write("map:");
|
||
|
|
const arr = [...map];
|
||
|
|
return this.array(arr, options.unorderedSets !== false);
|
||
|
|
},
|
||
|
|
set(set) {
|
||
|
|
write("set:");
|
||
|
|
const arr = [...set];
|
||
|
|
return this.array(arr, options.unorderedSets !== false);
|
||
|
|
},
|
||
|
|
file(file) {
|
||
|
|
write("file:");
|
||
|
|
return this.dispatch([file.name, file.size, file.type, file.lastModfied]);
|
||
|
|
},
|
||
|
|
blob() {
|
||
|
|
if (options.ignoreUnknown) {
|
||
|
|
return write("[blob]");
|
||
|
|
}
|
||
|
|
throw new Error(
|
||
|
|
'Hashing Blob objects is currently not supported\nUse "options.replacer" or "options.ignoreUnknown"\n'
|
||
|
|
);
|
||
|
|
},
|
||
|
|
domwindow() {
|
||
|
|
return write("domwindow");
|
||
|
|
},
|
||
|
|
bigint(number) {
|
||
|
|
return write("bigint:" + number.toString());
|
||
|
|
},
|
||
|
|
/* Node.js standard native objects */
|
||
|
|
process() {
|
||
|
|
return write("process");
|
||
|
|
},
|
||
|
|
timer() {
|
||
|
|
return write("timer");
|
||
|
|
},
|
||
|
|
pipe() {
|
||
|
|
return write("pipe");
|
||
|
|
},
|
||
|
|
tcp() {
|
||
|
|
return write("tcp");
|
||
|
|
},
|
||
|
|
udp() {
|
||
|
|
return write("udp");
|
||
|
|
},
|
||
|
|
tty() {
|
||
|
|
return write("tty");
|
||
|
|
},
|
||
|
|
statwatcher() {
|
||
|
|
return write("statwatcher");
|
||
|
|
},
|
||
|
|
securecontext() {
|
||
|
|
return write("securecontext");
|
||
|
|
},
|
||
|
|
connection() {
|
||
|
|
return write("connection");
|
||
|
|
},
|
||
|
|
zlib() {
|
||
|
|
return write("zlib");
|
||
|
|
},
|
||
|
|
context() {
|
||
|
|
return write("context");
|
||
|
|
},
|
||
|
|
nodescript() {
|
||
|
|
return write("nodescript");
|
||
|
|
},
|
||
|
|
httpparser() {
|
||
|
|
return write("httpparser");
|
||
|
|
},
|
||
|
|
dataview() {
|
||
|
|
return write("dataview");
|
||
|
|
},
|
||
|
|
signal() {
|
||
|
|
return write("signal");
|
||
|
|
},
|
||
|
|
fsevent() {
|
||
|
|
return write("fsevent");
|
||
|
|
},
|
||
|
|
tlswrap() {
|
||
|
|
return write("tlswrap");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const nativeFunc = "[native code] }";
|
||
|
|
const nativeFuncLength = nativeFunc.length;
|
||
|
|
function isNativeFunction(f) {
|
||
|
|
if (typeof f !== "function") {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return Function.prototype.toString.call(f).slice(-nativeFuncLength) === nativeFunc;
|
||
|
|
}
|
||
|
|
|
||
|
|
function diff(obj1, obj2, opts = {}) {
|
||
|
|
const h1 = _toHashedObject(obj1, opts);
|
||
|
|
const h2 = _toHashedObject(obj2, opts);
|
||
|
|
return _diff(h1, h2, opts);
|
||
|
|
}
|
||
|
|
function _diff(h1, h2, opts = {}) {
|
||
|
|
const diffs = [];
|
||
|
|
const allProps = /* @__PURE__ */ new Set([
|
||
|
|
...Object.keys(h1.props || {}),
|
||
|
|
...Object.keys(h2.props || {})
|
||
|
|
]);
|
||
|
|
if (h1.props && h2.props) {
|
||
|
|
for (const prop of allProps) {
|
||
|
|
const p1 = h1.props[prop];
|
||
|
|
const p2 = h2.props[prop];
|
||
|
|
if (p1 && p2) {
|
||
|
|
diffs.push(..._diff(h1.props?.[prop], h2.props?.[prop], opts));
|
||
|
|
} else if (p1 || p2) {
|
||
|
|
diffs.push(
|
||
|
|
new DiffEntry((p2 || p1).key, p1 ? "removed" : "added", p2, p1)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (allProps.size === 0 && h1.hash !== h2.hash) {
|
||
|
|
diffs.push(new DiffEntry((h2 || h1).key, "changed", h2, h1));
|
||
|
|
}
|
||
|
|
return diffs;
|
||
|
|
}
|
||
|
|
function _toHashedObject(obj, opts, key = "") {
|
||
|
|
if (obj && typeof obj !== "object") {
|
||
|
|
return new DiffHashedObject(key, obj, objectHash(obj, opts));
|
||
|
|
}
|
||
|
|
const props = {};
|
||
|
|
const hashes = [];
|
||
|
|
for (const _key in obj) {
|
||
|
|
props[_key] = _toHashedObject(
|
||
|
|
obj[_key],
|
||
|
|
opts,
|
||
|
|
key ? `${key}.${_key}` : _key
|
||
|
|
);
|
||
|
|
hashes.push(props[_key].hash);
|
||
|
|
}
|
||
|
|
return new DiffHashedObject(key, obj, `{${hashes.join(":")}}`, props);
|
||
|
|
}
|
||
|
|
class DiffEntry {
|
||
|
|
constructor(key, type, newValue, oldValue) {
|
||
|
|
this.key = key;
|
||
|
|
this.type = type;
|
||
|
|
this.newValue = newValue;
|
||
|
|
this.oldValue = oldValue;
|
||
|
|
}
|
||
|
|
toString() {
|
||
|
|
return this.toJSON();
|
||
|
|
}
|
||
|
|
toJSON() {
|
||
|
|
switch (this.type) {
|
||
|
|
case "added": {
|
||
|
|
return `Added \`${this.key}\``;
|
||
|
|
}
|
||
|
|
case "removed": {
|
||
|
|
return `Removed \`${this.key}\``;
|
||
|
|
}
|
||
|
|
case "changed": {
|
||
|
|
return `Changed \`${this.key}\` from \`${this.oldValue?.toString() || "-"}\` to \`${this.newValue.toString()}\``;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
class DiffHashedObject {
|
||
|
|
constructor(key, value, hash, props) {
|
||
|
|
this.key = key;
|
||
|
|
this.value = value;
|
||
|
|
this.hash = hash;
|
||
|
|
this.props = props;
|
||
|
|
}
|
||
|
|
toString() {
|
||
|
|
if (this.props) {
|
||
|
|
return `{${Object.keys(this.props).join(",")}}`;
|
||
|
|
} else {
|
||
|
|
return JSON.stringify(this.value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
toJSON() {
|
||
|
|
const k = this.key || ".";
|
||
|
|
if (this.props) {
|
||
|
|
return `${k}({${Object.keys(this.props).join(",")}})`;
|
||
|
|
}
|
||
|
|
return `${k}(${this.value})`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function isEqual(object1, object2, hashOptions = {}) {
|
||
|
|
if (object1 === object2) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (objectHash(object1, hashOptions) === objectHash(object2, hashOptions)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
exports.diff = diff;
|
||
|
|
exports.isEqual = isEqual;
|
||
|
|
exports.objectHash = objectHash;
|