dequal deep compare with object-hash digests: working versions (tested 2026-10-16)
Deep-compare nested objects, arrays, Maps, Sets and Dates with dequal, then compute stable object-hash digests using unorderedArrays and excludeKeys, fully offline.
passed6 passed · 0 failed2026-10-16 20:33 UTC- Status
- passed
- Last passed
- 2026-10-16 20:33 UTC
- Tests
- 6 / 6
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
dequal@2.0.3object-hash@3.0.0@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38962988948
The code
Exactly the files that passed. Your agent can pull them with get_recipe("dequal-object-hash-compare") (install), or see them on GitHub.
package.json
{
"name": "dequal-object-hash-compare",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"dequal": "2.0.3",
"object-hash": "3.0.0"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/compare.ts
import { dequal } from "dequal";
import hash from "object-hash";
import type { HashOptions } from "object-hash";
export function deepEqual(left: unknown, right: unknown): boolean {
return dequal(left, right);
}
export function stableHash(value: unknown, options: HashOptions = {}): string {
return hash(value, {
algorithm: "sha1",
encoding: "hex",
unorderedObjects: true,
unorderedSets: true,
...options,
});
}
src/object-hash.d.ts
declare module "object-hash" {
export interface HashOptions {
algorithm?: string;
encoding?: "hex" | "buffer" | "base64" | "latin1";
excludeValues?: boolean;
ignoreUnknown?: boolean;
respectFunctionProperties?: boolean;
respectFunctionNames?: boolean;
respectType?: boolean;
replacer?: (value: unknown) => unknown;
unorderedArrays?: boolean;
unorderedSets?: boolean;
unorderedObjects?: boolean;
excludeKeys?: (key: string) => boolean;
}
export default function hash(value: unknown, options?: HashOptions): string;
}
test/compare.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { deepEqual, stableHash } from "../src/compare.ts";
test("dequal deep-compares nested objects, arrays, Maps, Sets and Dates", () => {
const left = {
id: "u1",
tags: ["alpha", "beta"],
profile: { active: true, scores: [1, 2, 3] },
roles: new Set(["admin", "editor"]),
limits: new Map([
["daily", 10],
["monthly", 100],
]),
createdAt: new Date("2024-01-01T00:00:00.000Z"),
};
const right = {
id: "u1",
tags: ["alpha", "beta"],
profile: { active: true, scores: [1, 2, 3] },
roles: new Set(["editor", "admin"]),
limits: new Map([
["monthly", 100],
["daily", 10],
]),
createdAt: new Date("2024-01-01T00:00:00.000Z"),
};
assert.equal(deepEqual(left, right), true);
});
test("dequal reports inequality for different nested values", () => {
assert.equal(
deepEqual({ a: 1, nested: { list: [1, 2] } }, { a: 1, nested: { list: [1, 3] } }),
false,
);
assert.equal(deepEqual(new Set([1, 2]), new Set([1, 3])), false);
assert.equal(deepEqual(new Map([["a", 1]]), new Map([["a", 2]])), false);
assert.equal(deepEqual(new Date("2024-01-01"), new Date("2024-01-02")), false);
});
test("stableHash matches equivalent objects regardless of key order", () => {
const first = { user: { id: "u1", roles: ["admin"] }, nested: { deep: { value: 42 } } };
const second = { nested: { deep: { value: 42 } }, user: { roles: ["admin"], id: "u1" } };
assert.equal(stableHash(first), stableHash(second));
});
test("stableHash differs when a nested value changes", () => {
const base = { user: { id: "u1", roles: ["admin"] }, nested: { deep: { value: 42 } } };
const changed = { user: { id: "u1", roles: ["admin"] }, nested: { deep: { value: 43 } } };
assert.notEqual(stableHash(base), stableHash(changed));
});
test("unorderedArrays makes array hashes order independent", () => {
const first = { ids: [3, 1, 2], name: "x" };
const second = { ids: [1, 2, 3], name: "x" };
assert.equal(
stableHash(first, { unorderedArrays: true }),
stableHash(second, { unorderedArrays: true }),
);
assert.notEqual(stableHash(first), stableHash(second));
});
test("excludeKeys ignores volatile fields", () => {
const before = { id: "u1", name: "Ada", updatedAt: "2024-01-01T00:00:00.000Z" };
const after = { id: "u1", name: "Ada", updatedAt: "2024-06-01T00:00:00.000Z" };
const excludeKeys = (key: string) => key === "updatedAt";
assert.equal(stableHash(before, { excludeKeys }), stableHash(after, { excludeKeys }));
assert.notEqual(stableHash(before), stableHash(after));
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.