Skip to content

Commit

Permalink
feat: enable the BlockEncoder to use promise API in encode/decode
Browse files Browse the repository at this point in the history
      methods.
      Why we need that:
        https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
      To enable the async a breaking change to createUnsafe is
      required.
      The existing signature is not able to pass a promise.
      Therefore the tests need to change.
      For me, measuring the blast radius of the change is impossible.
      If the blast radius is too big then there is still the possibility to
      create a new method createUnsafeAsync and throw an error
      in the createUnsafe if the decode sends a promise.
  • Loading branch information
mabels committed Jul 30, 2024
1 parent 2189443 commit c8e653d
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 23 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@
"aegir": "^43.0.1",
"buffer": "^6.0.3",
"cids": "^1.1.9",
"crypto-hash": "^3.0.0"
"crypto-hash": "^3.0.0",
"mocha": "^10.7.0"
},
"aegir": {
"test": {
Expand Down
12 changes: 6 additions & 6 deletions src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export async function encode <T, Code extends number, Alg extends number> ({ val
if (typeof value === 'undefined') throw new Error('Missing required argument "value"')
if (codec == null || hasher == null) throw new Error('Missing required argument: codec or hasher')

const bytes = codec.encode(value)
const bytes = await Promise.resolve(codec.encode(value))
const hash = await hasher.digest(bytes)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const cid = CID.create(
Expand All @@ -168,7 +168,7 @@ export async function decode <T, Code extends number, Alg extends number> ({ byt
if (bytes == null) throw new Error('Missing required argument "bytes"')
if (codec == null || hasher == null) throw new Error('Missing required argument: codec or hasher')

const value = codec.decode(bytes)
const value = await Promise.resolve(codec.decode(bytes))
const hash = await hasher.digest(bytes)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const cid = CID.create(1, codec.code, hash) as CID<T, Code, Alg, 1>
Expand All @@ -194,10 +194,10 @@ type CreateUnsafeInput <T, Code extends number, Alg extends number, V extends AP
* @template Alg - multicodec code corresponding to the hashing algorithm used in CID creation.
* @template V - CID version
*/
export function createUnsafe <T, Code extends number, Alg extends number, V extends API.Version> ({ bytes, cid, value: maybeValue, codec }: CreateUnsafeInput<T, Code, Alg, V>): API.BlockView<T, Code, Alg, V> {
const value = maybeValue !== undefined
export async function createUnsafe <T, Code extends number, Alg extends number, V extends API.Version> ({ bytes, cid, value: maybeValue, codec }: CreateUnsafeInput<T, Code, Alg, V>): Promise<API.BlockView<T, Code, Alg, V>> {
const value = await Promise.resolve(maybeValue !== undefined
? maybeValue
: (codec?.decode(bytes))
: (codec?.decode(bytes)))

if (value === undefined) throw new Error('Missing required argument, must either provide "value" or "codec"')

Expand All @@ -224,7 +224,7 @@ interface CreateInput <T, Code extends number, Alg extends number, V extends API
export async function create <T, Code extends number, Alg extends number, V extends API.Version> ({ bytes, cid, hasher, codec }: CreateInput<T, Code, Alg, V>): Promise<API.BlockView<T, Code, Alg, V>> {
if (bytes == null) throw new Error('Missing required argument "bytes"')
if (hasher == null) throw new Error('Missing required argument "hasher"')
const value = codec.decode(bytes)
const value = await Promise.resolve(codec.decode(bytes))
const hash = await hasher.digest(bytes)
if (!binary.equals(cid.multihash.bytes, hash.bytes)) {
throw new Error('CID hash does not match bytes')
Expand Down
4 changes: 2 additions & 2 deletions src/codecs/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import type { ArrayBufferView, ByteView } from '../block/interface.js'
export interface BlockEncoder<Code extends number, T> {
name: string
code: Code
encode(data: T): ByteView<T>
encode(data: T): ByteView<T> | PromiseLike<ByteView<T>>
}

/**
* IPLD decoder part of the codec.
*/
export interface BlockDecoder<Code extends number, T> {
code: Code
decode(bytes: ByteView<T> | ArrayBufferView<T>): T
decode(bytes: ByteView<T> | ArrayBufferView<T>): T | PromiseLike<T>
}

/**
Expand Down
47 changes: 35 additions & 12 deletions test/test-block.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ const fixture = { hello: 'world' }
const link = CID.parse('bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae')
const buff = bytes.fromString('sadf')

describe('promise-resolve-semantics', () => {
it('should resolve to the value', async () => {
const value = await Promise.resolve('hello')
assert.equal(value, 'hello')
})
it('should resolve to the promise value', async () => {
// eslint-disable-next-line promise/param-names
const value = await Promise.resolve(new Promise<string>(function (rs: (value: string) => void) {
setTimeout(function () { rs('hello') }, 10)
}))
assert.equal(value, 'hello')
})
})

describe('block', () => {
it('basic encode/decode roundtrip', async () => {
const block = await main.encode({ value: fixture, codec, hasher })
Expand All @@ -23,7 +37,7 @@ describe('block', () => {

it('createUnsafe', async () => {
const block = await main.encode({ value: fixture, codec, hasher })
const block2 = main.createUnsafe({ bytes: block.bytes, cid: block.cid, codec })
const block2 = await main.createUnsafe({ bytes: block.bytes, cid: block.cid, codec })
assert.deepStrictEqual(block.cid.equals(block2.cid), true)
})

Expand All @@ -36,25 +50,29 @@ describe('block', () => {
// @ts-expect-error - 'string' is not assignable to parameter of type 'ArrayLike<number>'
bytes: Uint8Array.from('1234')
}
// @ts-expect-error - 'boolean' is not assignable to type 'CID'
const block = main.createUnsafe({ value, codec, hasher, cid: true, bytes: true })

it('links', () => {
it('links', async () => {
const expected = ['link', 'arr/0']
// @ts-expect-error - 'boolean' is not assignable to type 'CID'
const block = await main.createUnsafe({ value, codec, hasher, cid: true, bytes: true })
for (const [path, cid] of block.links()) {
assert.deepStrictEqual(path, expected.shift())
assert.deepStrictEqual(cid.toString(), link.toString())
}
})

it('tree', () => {
it('tree', async () => {
const expected = ['link', 'nope', 'arr', 'arr/0', 'obj', 'obj/arr', 'obj/arr/0', 'obj/arr/0/obj', 'bytes']
// @ts-expect-error - 'boolean' is not assignable to type 'CID'
const block = await main.createUnsafe({ value, codec, hasher, cid: true, bytes: true })
for (const path of block.tree()) {
assert.deepStrictEqual(path, expected.shift())
}
})

it('get', () => {
it('get', async () => {
// @ts-expect-error - 'boolean' is not assignable to type 'CID'
const block = await main.createUnsafe({ value, codec, hasher, cid: true, bytes: true })
let ret = block.get('link/test')
assert.deepStrictEqual(ret.remaining, 'test')
assert.deepStrictEqual(String(ret.value), link.toString())
Expand All @@ -63,8 +81,8 @@ describe('block', () => {
assert.deepStrictEqual(ret, { value: 'skip' })
})

it('null links/tree', () => {
const block = main.createUnsafe({
it('null links/tree', async () => {
const block = await main.createUnsafe({
value: null,
codec,
hasher,
Expand Down Expand Up @@ -95,9 +113,9 @@ describe('block', () => {
assert.equal(links[0][1].toString(), link.toString())
})

it('kitchen sink', () => {
it('kitchen sink', async () => {
const sink = { one: { two: { arr: [true, false, null], three: 3, buff, link } } }
const block = main.createUnsafe({
const block = await main.createUnsafe({
value: sink,
codec,
// @ts-expect-error - 'boolean' is not assignable to type 'ByteView<unknown>'
Expand Down Expand Up @@ -132,8 +150,13 @@ describe('block', () => {
})

it('createUnsafe', async () => {
// @ts-expect-error testing invalid usage
assert.throws(() => main.createUnsafe({}), 'Missing required argument, must either provide "value" or "codec"')
try {
// @ts-expect-error testing invalid usage
await main.createUnsafe({})
assert(false, 'Missing required argument, must either provide "value" or "codec"')
} catch (/** @type {Error} */ err) {
/* c8 ignore next */
}
})

it('create', async () => {
Expand Down
6 changes: 4 additions & 2 deletions test/test-multibase-spec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,11 @@ describe('spec test', () => {
}

for (const base of Object.values(bases)) {
it('should fail decode with invalid char', function () {
// eslint-disable-next-line @typescript-eslint/method-signature-style
it('should fail decode with invalid char', function (this: { skip: () => void }) {
if (base.name === 'identity') {
return this.skip()
this.skip()
return
}

assert.throws(() => base.decode(base.prefix + '^!@$%!#$%@#y'), `Non-${base.name} character`)
Expand Down

0 comments on commit c8e653d

Please sign in to comment.