From c8e653dcfeb17cad838cfee10cf95bd461cad2e0 Mon Sep 17 00:00:00 2001 From: Meno Abels <meno.abels@adviser.com> Date: Tue, 30 Jul 2024 08:26:18 +0200 Subject: [PATCH 1/2] feat: enable the BlockEncoder to use promise API in encode/decode 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. --- package.json | 3 +- src/block.ts | 12 ++++---- src/codecs/interface.ts | 4 +-- test/test-block.spec.ts | 47 ++++++++++++++++++++++++-------- test/test-multibase-spec.spec.ts | 6 ++-- 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 1fb02fba..77489cf5 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/block.ts b/src/block.ts index ae59ff13..3e2f9504 100644 --- a/src/block.ts +++ b/src/block.ts @@ -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( @@ -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> @@ -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"') @@ -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') diff --git a/src/codecs/interface.ts b/src/codecs/interface.ts index 8e5d9d1a..61b5394e 100644 --- a/src/codecs/interface.ts +++ b/src/codecs/interface.ts @@ -6,7 +6,7 @@ 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>> } /** @@ -14,7 +14,7 @@ export interface BlockEncoder<Code extends number, T> { */ 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> } /** diff --git a/test/test-block.spec.ts b/test/test-block.spec.ts index 120be93c..30c07e8e 100644 --- a/test/test-block.spec.ts +++ b/test/test-block.spec.ts @@ -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 }) @@ -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) }) @@ -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()) @@ -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, @@ -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>' @@ -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 () => { diff --git a/test/test-multibase-spec.spec.ts b/test/test-multibase-spec.spec.ts index 1f7f2523..db5dbece 100644 --- a/test/test-multibase-spec.spec.ts +++ b/test/test-multibase-spec.spec.ts @@ -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`) From a83bcfd8607668eb2828bc91e8b54ecfb96d8b5b Mon Sep 17 00:00:00 2001 From: Meno Abels <meno.abels@adviser.com> Date: Tue, 30 Jul 2024 13:21:43 +0200 Subject: [PATCH 2/2] fix: remove patch of package.json --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 77489cf5..1fb02fba 100644 --- a/package.json +++ b/package.json @@ -278,8 +278,7 @@ "aegir": "^43.0.1", "buffer": "^6.0.3", "cids": "^1.1.9", - "crypto-hash": "^3.0.0", - "mocha": "^10.7.0" + "crypto-hash": "^3.0.0" }, "aegir": { "test": {