Skip to content

Commit

Permalink
retain CID type info in Block impl
Browse files Browse the repository at this point in the history
  • Loading branch information
Gozala committed Jun 10, 2022
1 parent 91f780e commit a98adaa
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 59 deletions.
75 changes: 48 additions & 27 deletions src/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const readonly = ({ enumerable = true, configurable = false } = {}) => ({
* @template T
* @param {T} source
* @param {Array<string|number>} base
* @returns {Iterable<[string, CID]>}
* @returns {Iterable<[string, API.CIDView]>}
*/
const links = function * (source, base) {
if (source == null) return
Expand Down Expand Up @@ -74,6 +74,7 @@ const tree = function * (source, base) {
* @template T
* @param {T} source
* @param {string[]} path
* @return {API.BlockCursorView}
*/
const get = (source, path) => {
/** @type {Record<string, any>} */
Expand All @@ -92,19 +93,23 @@ const get = (source, path) => {
}

/**
* @template T
* @template {unknown} T - Logical type of the data encoded in the block
* @template {number} C - multicodec code corresponding to codec used to encode the block
* @template {number} A - multicodec code corresponding to the hashing algorithm used in CID creation.
* @template {API.CIDVersion} V - CID version
* @implements {API.BlockView<T, C, A, V>}
*/
class Block {
/**
* @param {Object} options
* @param {API.CID} options.cid
* @param {API.CIDView<C, A, V>} options.cid
* @param {API.ByteView<T>} options.bytes
* @param {T} options.value
*/
constructor ({ cid, bytes, value }) {
if (!cid || !bytes || typeof value === 'undefined') throw new Error('Missing required argument')
if (!cid || !bytes || typeof value === 'undefined') { throw new Error('Missing required argument') }

this.cid = /** @type {CID} */(cid)
this.cid = cid
this.bytes = bytes
this.value = value
this.asBlock = this
Expand All @@ -127,43 +132,47 @@ class Block {
}

/**
* @param {string} [path]
*/
* @param {string} [path]
*/
get (path = '/') {
return get(this.value, path.split('/').filter(Boolean))
}
}

/**
* @template T
* @template {number} Code
* @template {number} Alg
* @template {unknown} T - Logical type of the data encoded in the block
* @template {number} Code - multicodec code corresponding to codec used to encode the block
* @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation.
* @param {Object} options
* @param {T} options.value
* @param {API.BlockEncoder<Code, T>} options.codec
* @param {API.MultihashHasher<Alg>} options.hasher
* @returns {Promise<Block<T>>}
* @returns {Promise<API.BlockView<T, Code, Alg>>}
*/
const encode = async ({ value, codec, hasher }) => {
if (typeof value === 'undefined') throw new Error('Missing required argument "value"')
if (!codec || !hasher) throw new Error('Missing required argument: codec or hasher')

const bytes = codec.encode(value)
const hash = await hasher.digest(bytes)
const cid = CID.create(1, codec.code, hash)
const cid = CID.create(
1,
codec.code,
hash
)

return new Block({ value, bytes, cid })
}

/**
* @template T
* @template {number} Code
* @template {number} Alg
* @template {unknown} T - Logical type of the data encoded in the block
* @template {number} Code - multicodec code corresponding to codec used to encode the block
* @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation.
* @param {Object} options
* @param {API.ByteView<T>} options.bytes
* @param {API.BlockDecoder<Code, T>} options.codec
* @param {API.MultihashHasher<Alg>} options.hasher
* @returns {Promise<Block<T>>}
* @returns {Promise<API.BlockView<T, Code, Alg>>}
*/
const decode = async ({ bytes, codec, hasher }) => {
if (!bytes) throw new Error('Missing required argument "bytes"')
Expand All @@ -182,10 +191,12 @@ const decode = async ({ bytes, codec, hasher }) => {
*/

/**
* @template T
* @template {number} Code
* @param {{ cid: API.CID, value:T, codec?: API.BlockDecoder<Code, T>, bytes: API.ByteView<T> }|{cid:API.CID, bytes:API.ByteView<T>, value?:void, codec:API.BlockDecoder<Code, T>}} options
* @returns {Block<T>}
* @template {unknown} T - Logical type of the data encoded in the block
* @template {number} Code - multicodec code corresponding to codec used to encode the block
* @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation.
* @template {API.CIDVersion} V - CID version
* @param {{ cid: API.Link<T, Code, Alg, V>, value:T, codec?: API.BlockDecoder<Code, T>, bytes: API.ByteView<T> }|{cid:API.Link<T, Code, Alg, V>, bytes:API.ByteView<T>, value?:void, codec:API.BlockDecoder<Code, T>}} options
* @returns {API.BlockView<T, Code, Alg, V>}
*/
const createUnsafe = ({ bytes, cid, value: maybeValue, codec }) => {
const value = maybeValue !== undefined
Expand All @@ -194,19 +205,24 @@ const createUnsafe = ({ bytes, cid, value: maybeValue, codec }) => {

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

return new Block({ cid, bytes, value })
return new Block({
cid: /** @type {API.CIDView<Code, Alg, V>} */(cid),
bytes,
value
})
}

/**
* @template T
* @template {number} Code
* @template {number} Alg
* @template {unknown} T - Logical type of the data encoded in the block
* @template {number} Code - multicodec code corresponding to codec used to encode the block
* @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation.
* @template {API.CIDVersion} V - CID version
* @param {Object} options
* @param {API.CID<Code, Alg>} options.cid
* @param {API.CID<Code, Alg, V>} options.cid
* @param {API.ByteView<T>} options.bytes
* @param {API.BlockDecoder<Code, T>} options.codec
* @param {API.MultihashHasher<Alg>} options.hasher
* @returns {Promise<Block<T>>}
* @returns {Promise<API.BlockView<T, Code, Alg, V>>}
*/
const create = async ({ bytes, cid, hasher, codec }) => {
if (!bytes) throw new Error('Missing required argument "bytes"')
Expand All @@ -217,7 +233,12 @@ const create = async ({ bytes, cid, hasher, codec }) => {
throw new Error('CID hash does not match bytes')
}

return createUnsafe({ bytes, cid, value, codec })
return createUnsafe({
bytes,
cid,
value,
codec
})
}

export { encode, decode, create, createUnsafe, Block }
1 change: 1 addition & 0 deletions src/block/interface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// this is dummy module overlayed by interface.ts
69 changes: 69 additions & 0 deletions src/block/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable no-use-before-define */
import { Link, CIDView, CIDVersion } from '../cid/interface.js'

/**
* A byte-encoded representation of some type of `Data`.
*
* A `ByteView` is essentially a `Uint8Array` that's been "tagged" with
* a `Data` type parameter indicating the type of encoded data.
*
* For example, a `ByteView<{ hello: "world" }>` is a `Uint8Array` containing a
* binary representation of a `{hello: "world"}.
*/
export interface ByteView<Data> extends Uint8Array, Phantom<Data> {}

/**
* A utility type to retain an unused type parameter `T`.
* Similar to [phantom type parameters in Rust](https://doc.rust-lang.org/rust-by-example/generics/phantom.html).
*
* Capturing unused type parameters allows us to define "nominal types," which
* TypeScript does not natively support. Nominal types in turn allow us to capture
* semantics not represented in the actual type structure, without requring us to define
* new classes or pay additional runtime costs.
*
* For a concrete example, see {@link ByteView}, which extends the `Uint8Array` type to capture
* type information about the structure of the data encoded into the array.
*/
export interface Phantom<T> {
// This field can not be represented because field name is non-existings
// unique symbol. But given that field is optional any object will valid
// type contstraint.
[Marker]?: T
}
declare const Marker: unique symbol

/**
* Represents an IPLD block (including its CID) that can be decoded to data of
* type `T`.
*
* @template T - Logical type of the data encoded in the block
* @template C - multicodec code corresponding to codec used to encode the block
* @template A - multicodec code corresponding to the hashing algorithm used in CID creation.
* @template V - CID version
*/
export interface Block<
T = unknown,
C extends number = number,
A extends number = number,
V extends CIDVersion = 1
> {
bytes: ByteView<T>
cid: Link<T, C, A, V>
}

export type BlockCursorView =
| { value: unknown, remaining?: undefined }
| { value: CIDView, remaining: string }

export interface BlockView<
T = unknown,
C extends number = number,
A extends number = number,
V extends CIDVersion = 1
> extends Block<T, C, A, V> {
cid: CIDView<C, A, V> & Link<T, C, A, V>

links(): Iterable<[string, CIDView]>
tree(): Iterable<string>
get(path:string): BlockCursorView
}
45 changes: 27 additions & 18 deletions src/cid.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export * from './cid/interface.js'
* @template {number} Format
* @template {number} Alg
* @template {API.CIDVersion} Version
* @template T
* @param {API.CID<Format, Alg, Version>|T} input
* @template {unknown} U
* @param {API.CID<Format, Alg, Version>|U} input
* @returns {API.CID<Format, Alg, Version>|null}
*/
export const asCID = (input) => {
Expand Down Expand Up @@ -342,9 +342,9 @@ export class CID {
throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0')
}

return /** @type {CID<API.DAG_PB, API.SHA_256, 0>} */(createV0(
/** @type {API.MultihashDigest<API.SHA_256>} */ (multihash)
))
return /** @type {CID<API.DAG_PB, API.SHA_256, 0>} */ (
createV0(/** @type {API.MultihashDigest<API.SHA_256>} */ (multihash))
)
}
default: {
throw Error(
Expand All @@ -362,7 +362,9 @@ export class CID {
case 0: {
const { code, digest } = this.multihash
const multihash = Digest.create(code, digest)
return /** @type {CID<Format, Alg, 1>} */(createV1(this.code, multihash))
return /** @type {CID<Format, Alg, 1>} */ (
createV1(this.code, multihash)
)
}
case 1: {
return /** @type {CID<Format, Alg, 1>} */ (this)
Expand Down Expand Up @@ -445,10 +447,15 @@ export class CID {
}

/**
* @param {any} value
* @template {number} C
* @template {number} A
* @template {API.CIDVersion} V
* @template {unknown} U
* @param {API.CID<C, A, V>|U} value
* @returns {CID<C, A, V>|null}
*/
static asCID (value) {
return /** @type {CID|null} */(asCID(value))
return /** @type {CID<C, A, V>|null} */ (asCID(value))
}

/**
Expand All @@ -460,7 +467,9 @@ export class CID {
* @param {API.MultihashDigest<Alg>} digest - (Multi)hash of the of the content.
*/
static create (version, code, digest) {
return /** @type {CID<Format, Alg, Version>} */(create(version, code, digest))
return /** @type {CID<Format, Alg, Version>} */ (
create(version, code, digest)
)
}

/**
Expand All @@ -472,12 +481,12 @@ export class CID {
}

/**
* Simplified version of `create` for CIDv1.
* @template {number} Code
* @template {number} Alg
* @param {Code} code - Content encoding format code.
* @param {API.MultihashDigest<Alg>} digest - Miltihash of the content.
*/
* Simplified version of `create` for CIDv1.
* @template {number} Code
* @template {number} Alg
* @param {Code} code - Content encoding format code.
* @param {API.MultihashDigest<Alg>} digest - Miltihash of the content.
*/
static createV1 (code, digest) {
return CID.create(1, code, digest)
}
Expand All @@ -487,14 +496,14 @@ export class CID {
*/

static decode (bytes) {
return /** @type {CID} */(decode(bytes))
return /** @type {CID} */ (decode(bytes))
}

/**
* @param {Uint8Array} bytes
*/
static decodeFirst (bytes) {
return /** @type {[CID, Uint8Array]} */(decodeFirst(bytes))
return /** @type {[CID, Uint8Array]} */ (decodeFirst(bytes))
}

/**
Expand All @@ -510,7 +519,7 @@ export class CID {
* @param {API.MultibaseDecoder<Prefix>} [base]
*/
static parse (source, base) {
return /** @type {CID} */(parse(source, base))
return /** @type {CID} */ (parse(source, base))
}
}

Expand Down
21 changes: 20 additions & 1 deletion src/cid/interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */
/* eslint-disable no-use-before-define */
import type { MultihashDigest } from '../hashes/interface'
import type { MultibaseEncoder, MultibaseDecoder } from '../bases/interface'
import type { Phantom } from '../block/interface'
export type { CID as CIDView } from '../cid'

export type { MultihashDigest, MultibaseEncoder, MultibaseDecoder }
export type CIDVersion = 0 | 1
Expand All @@ -26,12 +29,28 @@ export interface CID<
equals(other: unknown): other is CID<Format, Alg, Version>

toString(base?: MultibaseEncoder<string>): string
toJSON(): {version: Version, code:Format, hash:Uint8Array}
toJSON(): { version: Version, code:Format, hash:Uint8Array }

toV0(): CIDv0
toV1(): CIDv1
}

/**
* Represents an IPLD link to a specific data of type `T`.
*
* @template T - Logical type of the data being linked to.
* @template C - multicodec code corresponding to a codec linked data is encoded with
* @template A - multicodec code corresponding to the hashing algorithm of the CID
* @template V - CID version
*/
export interface Link<
T extends unknown = unknown,
C extends number = number,
A extends number = number,
V extends CIDVersion = 1
> extends CID<C, A, V>, Phantom<T> {
}

export interface CIDv0 extends CID<DAG_PB, SHA_256, 0> {
readonly version: 0
}
Expand Down
Loading

0 comments on commit a98adaa

Please sign in to comment.