Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: hashAsync and sha256*Async utils #41

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,26 @@ Import:

```js
// ESM
import { hash, objectHash, murmurHash, sha256 } from "ohash";
import { hash, hashAsync, objectHash, murmurHash, sha256 } from "ohash";

// CommonJS
const { hash, objectHash, murmurHash, sha256 } = require("ohash");
const { hash, hashAsync, objectHash, murmurHash, sha256 } = require("ohash");
```

### `hash(object, options?)`
### `hash(object, options?)` / `hashAsync(object, options?)`

Converts object value into a string hash using `objectHash` and then applies `sha256` with Base64 encoding (trimmed by length of 10).

`hashAsync` is slightly faster as will leverage [`SubtleCrypto.digest`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest) when available.

Usage:

```js
import { hash } from "ohash";
import { hash, hashAsync } from "ohash";

// "dZbtA7f0lK"
console.log(hash({ foo: "bar" }));
console.log(await hashAsync({ foo: "bar" }));
```

### `objectHash(object, options?)`
Expand Down Expand Up @@ -121,26 +124,28 @@ import { murmurHash } from "ohash";
console.log(murmurHash("Hello World"));
```

### `sha256`
### `sha256` / `sha256Async`

Create a secure [SHA 256](https://en.wikipedia.org/wiki/SHA-2) digest from input string.

```js
import { sha256 } from "ohash";
import { sha256, sha256Async } from "ohash";

// "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"
console.log(sha256("Hello World"));
console.log(await sha256Async("Hello World"));
```

### `sha256base64`
### `sha256base64` / `sha256base64Async`

Create a secure [SHA 256](https://en.wikipedia.org/wiki/SHA-2) digest in Base64 encoding from input string.

```js
import { sha256base64 } from "ohash";
import { sha256base64, sha256base64Async } from "ohash";

// "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4"
console.log(sha256base64("Hello World"));
console.log(await sha256base64Async("Hello World"));
```

## 💻 Development
Expand Down
16 changes: 16 additions & 0 deletions benchmark/_utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function generateItems(num) {
return new Array(num).fill(0).map(() => {
return {
propNum: Math.random(),
propBool: Math.random() > 0.5,
propString: Math.random().toString(16),
propDate: new Date(),
propObj: {
propNum: Math.random(),
propBool: Math.random() > 0.5,
propString: Math.random().toString(16),
propDate: new Date(),
},
};
});
}
34 changes: 34 additions & 0 deletions benchmark/hash.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Benchmark from "benchmark";
import { hash, hashAsync } from "ohash";
import largeJson from "./fixture/large.mjs";
import { generateItems } from "./_utils.mjs";

const dataSets = {
emptyObject: {},
singleObject: generateItems(1)[0],
tinyArray: generateItems(10),
mediumArray: generateItems(100),
largeArray: generateItems(1000),
largeJson,
};

const suite = new Benchmark.Suite();

for (const [name, data] of Object.entries(dataSets)) {
suite.add(`hash(${name})`, () => {
hash(data);
});
suite.add(
`hashAsync(${name})`,
(ctx) => {
hashAsync(data).then(() => ctx.resolve());
},
{ defer: true },
);
}

suite
.on("cycle", (event) => {
console.log(event.target.toString());
})
.run({ async: false });
33 changes: 9 additions & 24 deletions benchmark/object-hash.mjs
Original file line number Diff line number Diff line change
@@ -1,51 +1,36 @@
import Benchmark from "benchmark";
import { objectHash } from "ohash";
import largeJson from "./fixture/large.mjs";
import { generateItems } from "./_utils.mjs";

function generateItems(num) {
return new Array(num).fill(0).map(() => {
return {
propNum: Math.random(),
propBool: Math.random() > 0.5,
propString: Math.random().toString(16),
propDate: new Date(),
propObj: {
propNum: Math.random(),
propBool: Math.random() > 0.5,
propString: Math.random().toString(16),
propDate: new Date(),
},
};
});
}

const suite = new Benchmark.Suite();
const singleObject = generateItems(1)[0];
const tinyArray = generateItems(10);
const mediumArray = generateItems(100);
const largeArray = generateItems(1000);

suite.add("hash({})", function () {
const suite = new Benchmark.Suite();

suite.add("objectHash({})", function () {
const v = objectHash({});
});

suite.add("hash(singleObject)", function () {
suite.add("objectHash(singleObject)", function () {
const v = objectHash(singleObject);
});

suite.add("hash(tinyArray)", function () {
suite.add("objectHash(tinyArray)", function () {
const v = objectHash(tinyArray);
});

suite.add("hash(mediumArray)", function () {
suite.add("objectHash(mediumArray)", function () {
const v = objectHash(mediumArray);
});

suite.add("hash(largeArray)", function () {
suite.add("objectHash(largeArray)", function () {
const v = objectHash(largeArray);
});

suite.add("hash(largeJson)", function () {
suite.add("objectHash(largeJson)", function () {
const v = objectHash(largeJson);
});

Expand Down
47 changes: 47 additions & 0 deletions src/crypto/sha256.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ export function sha256(message: string) {
return new SHA256().finalize(message).toString();
}

export function sha256Async(message: string) {
if (!globalThis.crypto?.subtle?.digest) {
return new SHA256().finalize(message).toString();
}
return globalThis.crypto.subtle
.digest("SHA-256", new TextEncoder().encode(message))
.then((digest) => arrayBufferToHex(digest));
}

/**
* Calculates the SHA-256 hash of the given message and encodes it in Base64.
*
Expand All @@ -157,3 +166,41 @@ export function sha256(message: string) {
export function sha256base64(message: string) {
return new SHA256().finalize(message).toString(Base64);
}

export function sha256base64Async(message: string) {
if (!globalThis.crypto?.subtle?.digest) {
return Promise.resolve(sha256base64(message));
}
return globalThis.crypto.subtle
.digest("SHA-256", new TextEncoder().encode(message))
.then((digest) => arrayBufferToBase64(digest));
}

// --- internal ---

function arrayBufferToBase64(buffer: ArrayBuffer) {
if (globalThis.Buffer) {
return globalThis.Buffer.from(buffer)
.toString("base64")
.replace(/[+/=]/g, "");
}
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/[+/=]/g, "");
}

function arrayBufferToHex(buffer: ArrayBuffer) {
if (globalThis.Buffer) {
return globalThis.Buffer.from(buffer).toString("hex");
}
const bytes = new Uint8Array(buffer);
const hex = [];
for (const byte of bytes) {
hex.push(byte.toString(16).padStart(2, "0"));
}
return hex.join("");
}
18 changes: 17 additions & 1 deletion src/hash.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { objectHash, HashOptions } from "./object-hash";
import { sha256base64 } from "./crypto/sha256";
import { sha256base64, sha256base64Async } from "./crypto/sha256";

/**
* Hash any JS value into a string
Expand All @@ -13,3 +13,19 @@ export function hash(object: any, options: HashOptions = {}): string {
typeof object === "string" ? object : objectHash(object, options);
return sha256base64(hashed).slice(0, 10);
}

/**
* Hash any JS value into a string
* @param {object} object value to hash
* @param {HashOptions} options hashing options
* @return {Promise<string>} hash value
* @api public
*/
export function hashAsync(
object: any,
options: HashOptions = {},
): Promise<string> {
const hashed =
typeof object === "string" ? object : objectHash(object, options);
return sha256base64Async(hashed).then((digest) => digest.slice(0, 10));
}
9 changes: 7 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export { objectHash } from "./object-hash";
export { hash } from "./hash";
export { hash, hashAsync } from "./hash";
export { murmurHash } from "./crypto/murmur";
export { sha256, sha256base64 } from "./crypto/sha256";
export {
sha256,
sha256Async,
sha256base64,
sha256base64Async,
} from "./crypto/sha256";
export { isEqual } from "./utils";
export { diff } from "./diff";
36 changes: 34 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { describe, expect, it } from "vitest";
import { murmurHash, objectHash, hash, sha256, isEqual, diff } from "../src";
import { sha256base64 } from "../src/crypto/sha256";
import {
murmurHash,
objectHash,
hash,
sha256,
sha256Async,
sha256base64,
sha256base64Async,
isEqual,
diff,
hashAsync,
} from "../src";

describe("objectHash", () => {
it("basic object", () => {
Expand Down Expand Up @@ -79,6 +89,15 @@ it("sha256", () => {
);
});

it("sha256Async", async () => {
expect(await sha256Async("Hello World")).toMatchInlineSnapshot(
'"a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"',
);
expect(await sha256Async("")).toMatchInlineSnapshot(
'"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"',
);
});

it("sha256base64", () => {
expect(sha256base64("Hello World")).toMatchInlineSnapshot(
'"pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4"',
Expand All @@ -88,10 +107,23 @@ it("sha256base64", () => {
);
});

it("sha256base64Async", async () => {
expect(await sha256base64Async("Hello World")).toMatchInlineSnapshot(
'"pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4"',
);
expect(await sha256base64Async("")).toMatchInlineSnapshot(
'"47DEQpj8HBSaTImW5JCeuQeRkm5NMpJWZG3hSuFU"',
);
});

it("hash", () => {
expect(hash({ foo: "bar" })).toMatchInlineSnapshot('"dZbtA7f0lK"');
});

it("hashAsync", async () => {
expect(await hashAsync({ foo: "bar" })).toMatchInlineSnapshot('"dZbtA7f0lK"');
});

describe("isEqual", () => {
const cases = [
[{ foo: "bar" }, { foo: "bar" }, true],
Expand Down
Loading