Skip to content

Commit c3a650c

Browse files
authored
feat: add sync multihash hasher (#160)
1 parent 1c49d64 commit c3a650c

File tree

6 files changed

+93
-26
lines changed

6 files changed

+93
-26
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ node_modules
88
yarn.lock
99
types
1010
test/ts-use/tsconfig.tsbuildinfo
11+
test/tsconfig.tsbuildinfo

src/hashes/hasher.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const from = ({ name, code, encode }) => new Hasher(name, code, encode)
1717
* @template {string} Name
1818
* @template {number} Code
1919
* @class
20-
* @implements {MultihashHasher}
20+
* @implements {MultihashHasher<Code>}
2121
*/
2222
export class Hasher {
2323
/**
@@ -34,12 +34,15 @@ export class Hasher {
3434

3535
/**
3636
* @param {Uint8Array} input
37-
* @returns {Promise<Digest.Digest<Code, number>>}
37+
* @returns {Await<Digest.Digest<Code, number>>}
3838
*/
39-
async digest (input) {
39+
digest (input) {
4040
if (input instanceof Uint8Array) {
41-
const digest = await this.encode(input)
42-
return Digest.create(this.code, digest)
41+
const result = this.encode(input)
42+
return result instanceof Uint8Array
43+
? Digest.create(this.code, result)
44+
/* c8 ignore next 1 */
45+
: result.then(digest => Digest.create(this.code, digest))
4346
} else {
4447
throw Error('Unknown type, must be binary type')
4548
/* c8 ignore next 1 */
@@ -48,6 +51,7 @@ export class Hasher {
4851
}
4952

5053
/**
54+
* @template {number} Alg
5155
* @typedef {import('./interface').MultihashHasher} MultihashHasher
5256
*/
5357

src/hashes/identity.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
// @ts-check
2-
3-
import { from } from './hasher.js'
41
import { coerce } from '../bytes.js'
2+
import * as Digest from './digest.js'
3+
4+
export const code = 0x0
5+
export const name = 'identity'
6+
7+
/**
8+
* @param {Uint8Array} input
9+
* @returns {Digest.Digest<typeof code, number>}
10+
*/
11+
export const digest = (input) => Digest.create(code, coerce(input))
512

6-
export const identity = from({
7-
name: 'identity',
8-
code: 0x0,
9-
encode: (input) => coerce(input)
10-
})
13+
/** @type {import('./interface').SyncMultihashHasher<typeof code>} */
14+
export const identity = { code, name, digest }

src/hashes/interface.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
// a bunch of places that parse it to extract (code, digest, size). By creating
1010
// this first class representation we avoid reparsing and things generally fit
1111
// really nicely.
12-
export interface MultihashDigest {
12+
export interface MultihashDigest<Code extends number = number> {
1313
/**
1414
* Code of the multihash
1515
*/
16-
code: number
16+
code: Code
1717

1818
/**
1919
* Raw digest (without a hashing algorithm info)
@@ -35,20 +35,38 @@ export interface MultihashDigest {
3535
* Hasher represents a hashing algorithm implementation that produces as
3636
* `MultihashDigest`.
3737
*/
38-
export interface MultihashHasher {
38+
export interface MultihashHasher<Code extends number = number> {
3939
/**
40-
* Takes binary `input` and returns it (multi) hash digest.
40+
* Takes binary `input` and returns it (multi) hash digest. Return value is
41+
* either promise of a digest or a digest. This way general use can `await`
42+
* while performance critical code may asses return value to decide whether
43+
* await is needed.
44+
*
4145
* @param {Uint8Array} input
4246
*/
43-
digest(input: Uint8Array): Promise<MultihashDigest>
47+
digest(input: Uint8Array): Promise<MultihashDigest> | MultihashDigest
4448

4549
/**
4650
* Name of the multihash
4751
*/
48-
name: string
52+
name: string
4953

5054
/**
5155
* Code of the multihash
5256
*/
53-
code: number
57+
code: Code
58+
}
59+
60+
/**
61+
* Sync variant of `MultihashHasher` that refines return type of the `digest`
62+
* to `MultihashDigest`. It is subtype of `MultihashHasher` so implementations
63+
* of this interface can be passed anywhere `MultihashHasher` is expected,
64+
* allowing consumer to either `await` or check the return type to decide
65+
* whether to await or proceed with return value.
66+
*
67+
* `SyncMultihashHasher` is useful in certain APIs where async hashing would be
68+
* impractical e.g. implementation of Hash Array Mapped Trie (HAMT).
69+
*/
70+
export interface SyncMultihashHasher<Code extends number = number> extends MultihashHasher<Code> {
71+
digest(input: Uint8Array): MultihashDigest
5472
}

test/test-multihash.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ describe('multihash', () => {
5353
assert.deepStrictEqual(hash2.bytes, hash.bytes)
5454
})
5555

56+
if (typeof navigator === 'undefined') {
57+
it('sync sha-256', () => {
58+
const hash = sha256.digest(fromString('test'))
59+
if (hash instanceof Promise) {
60+
assert.fail('expected sync result')
61+
} else {
62+
assert.deepStrictEqual(hash.code, sha256.code)
63+
assert.deepStrictEqual(hash.digest, slSha256(fromString('test')))
64+
65+
const hash2 = decodeDigest(hash.bytes)
66+
assert.deepStrictEqual(hash2.code, sha256.code)
67+
assert.deepStrictEqual(hash2.bytes, hash.bytes)
68+
}
69+
})
70+
}
71+
5672
it('hash sha2-512', async () => {
5773
const hash = await sha512.digest(fromString('test'))
5874
assert.deepStrictEqual(hash.code, sha512.code)
@@ -63,7 +79,7 @@ describe('multihash', () => {
6379
assert.deepStrictEqual(hash2.bytes, hash.bytes)
6480
})
6581

66-
it('hash identity', async () => {
82+
it('hash identity async', async () => {
6783
const hash = await identity.digest(fromString('test'))
6884
assert.deepStrictEqual(hash.code, identity.code)
6985
assert.deepStrictEqual(identity.code, 0)
@@ -73,6 +89,17 @@ describe('multihash', () => {
7389
assert.deepStrictEqual(hash2.code, identity.code)
7490
assert.deepStrictEqual(hash2.bytes, hash.bytes)
7591
})
92+
93+
it('hash identity sync', () => {
94+
const hash = identity.digest(fromString('test'))
95+
assert.deepStrictEqual(hash.code, identity.code)
96+
assert.deepStrictEqual(identity.code, 0)
97+
assert.deepStrictEqual(hash.digest, fromString('test'))
98+
99+
const hash2 = decodeDigest(hash.bytes)
100+
assert.deepStrictEqual(hash2.code, identity.code)
101+
assert.deepStrictEqual(hash2.bytes, hash.bytes)
102+
})
76103
})
77104
describe('decode', () => {
78105
for (const { encoding, hex, size } of valid) {
@@ -105,7 +132,11 @@ describe('multihash', () => {
105132
})
106133

107134
it('throw on hashing non-buffer', async () => {
108-
// @ts-expect-error - string is incompatible arg
109-
await assert.isRejected(sha256.digest('asdf'), 'Unknown type, must be binary type')
135+
try {
136+
// @ts-expect-error - string is incompatible arg
137+
await sha256.digest('asdf')
138+
} catch (error) {
139+
assert.match(String(error), /Unknown type, must be binary type/)
140+
}
110141
})
111142
})

test/tsconfig.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
}
66
],
77
"compilerOptions": {
8+
"composite": true,
89
"allowJs": true,
910
"checkJs": true,
1011
"forceConsistentCasingInFileNames": true,
@@ -28,13 +29,21 @@
2829
"skipLibCheck": true,
2930
"stripInternal": true,
3031
"resolveJsonModule": true,
31-
"noEmit": true
32+
"noEmit": true,
33+
"paths": {
34+
"multiformats": [
35+
"../types/src/index"
36+
],
37+
"multiformats/*": [
38+
"../types/src/*"
39+
]
40+
}
3241
},
3342
"include": [
34-
"test/",
3543
"."
3644
],
3745
"exclude": [
38-
"ts-use/"
46+
"ts-use/",
47+
"node_modules"
3948
]
4049
}

0 commit comments

Comments
 (0)