Skip to content
This repository was archived by the owner on Jun 2, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 2 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
6 changes: 0 additions & 6 deletions basics.js

This file was deleted.

7 changes: 4 additions & 3 deletions defaults.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Block from './basics.js'
import dagcbor from '@ipld/dag-cbor'
Block.multiformats.multicodec.add(dagcbor)
import Block from './index.js'
import { codec as multicodec } from 'multiformats'
import * as dagcbor from '@ipld/dag-cbor'
Block.add(multicodec.codec(dagcbor))

export default Block
284 changes: 136 additions & 148 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,174 +1,162 @@
import withIs from 'class-is'
import transform from 'lodash.transform'
import createReader from './reader.js'
import reader from './reader.js'
import json from 'multiformats/codecs/json'
import raw from 'multiformats/codecs/raw'
import { sha256 } from 'multiformats/hashes/sha2'
import { bytes, CID } from 'multiformats'

const readonly = value => ({ get: () => value, set: () => { throw new Error('Cannot set read-only property') } })

const immutableTypes = new Set(['number', 'string', 'boolean'])

const create = multiformats => {
const { bytes, CID, multihash, multicodec } = multiformats
const { coerce, isBinary } = bytes
const copyBinary = value => {
const b = coerce(value)
return coerce(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength))
const { coerce, isBinary } = bytes
const copyBinary = value => {
const b = coerce(value)
return coerce(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength))
}

const clone = obj => transform(obj, (result, value, key) => {
if (value && value.asCID === value) {
result[key] = value
} else if (isBinary(value)) {
result[key] = copyBinary(value)
} else if (typeof value === 'object' && value !== null) {
result[key] = clone(value)
} else {
result[key] = value
}
const reader = createReader(multiformats)

const clone = obj => transform(obj, (result, value, key) => {
const cid = CID.asCID(value)
if (cid) {
result[key] = cid
} else if (isBinary(value)) {
result[key] = copyBinary(value)
} else if (typeof value === 'object' && value !== null) {
result[key] = clone(value)
} else {
result[key] = value
}
})

class Block {
constructor (opts) {
if (!opts) throw new Error('Block options are required')
if (opts.cid) opts.cid = CID.asCID(opts.cid)
if (typeof opts.codec === 'number') {
opts.code = opts.codec
opts.codec = multicodec.get(opts.code).name
}
if (typeof opts.source === 'undefined' &&
typeof opts.data === 'undefined') {
throw new Error('Block instances must be created with either an encode source or data')
}
if (typeof opts.source !== 'undefined' && !opts.codec && !opts.code) {
throw new Error('Block instances created from source objects must include desired codec')
}
if (opts.data && !opts.cid && !opts.codec && !opts.code) {
throw new Error('Block instances created from data must include cid or codec')
}
if (!opts.cid && !opts.algo) opts.algo = 'sha2-256'
// Do our best to avoid accidental mutations of the options object after instantiation
// Note: we can't actually freeze the object because we mutate it once per property later
opts = Object.assign({}, opts)
Object.defineProperty(this, 'opts', readonly(opts))
}
})

source () {
if (this.opts.cid || this.opts.data ||
this._encoded || this._decoded) return null
if (!this.opts.source) return null
return this.opts.source
}
const setImmutable = (obj, key, value) => {
if (typeof value === 'undefined') throw new Error(`${key} cannot be undefined`)
Object.defineProperty(this, key, readonly(value))
}

async cid () {
if (this.opts.cid) return this.opts.cid
const hash = await multihash.hash(this.encodeUnsafe(), this.opts.algo)
const cid = CID.create(1, this.code, hash)
this.opts.cid = cid
// https://github.com/bcoe/c8/issues/135
/* c8 ignore next */
return cid
}
class Block {
constructor ({ codec, hasher, source, cid, data }) {
setImmutable(this, '_codec', codec)
setImmutable(this, '_hasher', hasher)
if (source) setImmutable(this, '_source', source)
if (cid) setImmutable(this, '_cid', cid)
if (data) setImmutable(this, '_data', data)
if (!source && (!data || !cid)) throw new Error('Missing required argument')
if (source && (!codec || !hasher)) throw new Error('Missing required argument')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think things would be a lot cleaner if Block.encoder and Block.decoder would do above invariant checks instead of deferring that to shared constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, i’m going to move all of these in the new patch to js-multiformats.

setImmutable(this, 'asBlock', this)
}

get codec () {
if (this.opts.code) {
this.opts.codec = multicodec.get(this.opts.code).name
}
if (this.opts.cid) {
this.opts.codec = multicodec.get(this.opts.cid.code).name
}
return this.opts.codec
}
async cid () {
if (this._cid) return this._cid
const hash = await this._hasher.digest(this.encodeUnsafe())
const cid = CID.create(1, this.opts.codec.code, hash)
setImmutable(this, '_cid', cid)
return cid
}

get code () {
if (this.opts.cid) return this.opts.cid.code
if (!this.opts.code) {
this.opts.code = multicodec.get(this.codec).code
}
return this.opts.code
}
get code () {
if (this._cid) return this._cid.code
return this._codec.code
}

async validate () {
// if we haven't created a CID yet we know it will be valid :)
if (!this.opts.cid) return true
const cid = await this.cid()
const data = this.encodeUnsafe()
// https://github.com/bcoe/c8/issues/135
/* c8 ignore next */
return multihash.validate(cid.multihash, data)
}
encode () {
const data = this.encodeUnsafe()
return copyBinary(data)
}

_encode () {
this._encoded = this.opts.data || multicodec.get(this.code).encode(this.opts.source)
encodeUnsafe () {
if (this._data) return this._data
if (!this._codec) {
throw new Error('Do not have codec implemention in this Block interface')
}
const data = this._codec.encode(this._source)
setImmutable(this, '_data', data)
return data
}

encode () {
if (!this._encoded) this._encode()
return copyBinary(this._encoded)
decodeUnsafe () {
if (typeof this.opts._source !== 'undefined') return this._source
if (!this._codec) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is obsolete because validation occurs at construction site.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out, there is a case in which you want to create a Block without a codec attached. i actually have to drop the constructor error and move that check to the encoder/decoder methods, but it’ll still be necessary here so that you get a good error if you try to decode a block that doesn’t have a codec attached

throw new Error('Do not have codec implemention in this Block interface')
}
const source = this._codec.decode(this._data)
setImmutable(this, '_source', source)
return source
}

encodeUnsafe () {
if (!this._encoded) this._encode()
return this._encoded
decode () {
const decoded = this.decodeUnsafe()
if (decoded === null) return null
if (isBinary(decoded)) return copyBinary(decoded)
if (immutableTypes.has(typeof decoded) || decoded === null) {
return decoded
}
return clone(decoded)
}

_decode () {
if (typeof this.opts.source !== 'undefined') this._decoded = this.opts.source
else {
const { decode } = multicodec.get(this.code)
this._decoded = decode(this._encoded || this.opts.data)
}
return this._decoded
}
reader () {
return reader(this.decodeUnsafe())
}

decode () {
// TODO: once we upgrade to the latest data model version of
// dag-pb that @gozala wrote we should be able to remove this
// and treat it like every other codec.
/* c8 ignore next */
if (this.codec === 'dag-pb') return this._decode()
if (!this._decoded) this._decode()
const tt = typeof this._decoded
if (tt === 'number' || tt === 'boolean') {
// return any immutable types
return this._decoded
}
if (isBinary(this._decoded)) return copyBinary(this._decoded)
if (immutableTypes.has(typeof this._decoded) || this._decoded === null) {
return this._decoded
}
return clone(this._decoded)
}
async equals (block) {
if (block === this) return true
if (block.asBlock !== block) throw new Error('Not a block instance')
const [a, b] = await Promise.all([this.cid(), block.cid()])
return a.equals(b)
}
}

decodeUnsafe () {
if (!this._decoded) this._decode()
return this._decoded
}
Block.codecs = new Map()

reader () {
return reader(this.decodeUnsafe())
}
Block.add = codec => {
if (codec.name) Block.codecs.set(codec.name, codec)
if (codec.code) Block.codecs.set(codec.code, codec)
}
Block.add(json)
Block.add(raw)

async equals (block) {
if (block === this) return true
const cid = await this.cid()
if (CID.asCID(block)) return cid.equals(CID.asCID(block))
// https://github.com/bcoe/c8/issues/135
/* c8 ignore next */
return cid.equals(await block.cid())
}
Block.encoder = (source, codec, hasher = sha256) => {
if (typeof codec === 'string') codec = Block.codecs.get(codec)
if (!codec) throw new Error('Missing codec')
return new Block({ source, codec, hasher })
}
Block.decoder = (data, codec, hasher = sha256) => {
if (typeof codec === 'string') codec = Block.codecs.get(codec)
if (!codec) throw new Error('Missing codec')
return new Block({ data, codec, hasher })
}
Block.createUnsafe = (data, cid, { hasher, codec } = {}) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think createUnsafeDecoder is more descriptive name, and also makes it clear why codec needs to be provided. I think it should not take hasher because it does not need it.

Suggested change
Block.createUnsafe = (data, cid, { hasher, codec } = {}) => {
Block.createUnsafeDecoder = (data, cid, { codec } = {}) => {

codec = codec || Block.codecs.get(cid.code)
if (!codec) throw new Error(`Missing codec ${cid.code}`)
return new Block({ data, cid, codec, hasher: hasher || null })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requires change above.

Suggested change
return new Block({ data, cid, codec, hasher: hasher || null })
return new Block({ data, cid, codec })

}
Block.create = async (data, cid, { hasher, codec } = {}) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think calling this decode would be more descriptive and explain why it's async.

Suggested change
Block.create = async (data, cid, { hasher, codec } = {}) => {
Block.decode = async (data, cid, { hasher, codec } = {}) => {

if (typeof cid === 'string') cid = CID.parse(cid)
hasher = hasher || Block.codec.get(cid.multihash.code)
if (!hasher) {
const { code } = cid.multihash.code
throw new Error(`Missing hasher for verification. Pass hasher for hash type ${code} or use createUnsafe()`)
}

const BlockWithIs = withIs(Block, { className: 'Block', symbolName: '@ipld/block' })
BlockWithIs.encoder = (source, codec, algo) => new BlockWithIs({ source, codec, algo })
BlockWithIs.decoder = (data, codec, algo) => new BlockWithIs({ data, codec, algo })
BlockWithIs.create = (data, cid) => {
if (typeof cid === 'string') cid = CID.from(cid)
return new BlockWithIs({ data, cid })
const hash = await hasher.digest(data)
if (!bytes.equals(cid.multihash.bytes, hash.bytes)) {
throw new Error('CID hash does not match data')
}
BlockWithIs.multiformats = multiformats
BlockWithIs.CID = CID
return BlockWithIs
return Block.createUnsafe(data, cid, { hasher, codec })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this this is a reason why createUnsafe to takes hasher. Just using constructor here can simplify createUnsafe.

Suggested change
return Block.createUnsafe(data, cid, { hasher, codec })
return new Block({ cid, data, codec, hasher })

}

export default create
Block.defaults = opts => ({
CID,
defaults: opts,
encoder: (source, codec, hasher) => Block.encoder(source, codec || opts.codec, hasher || opts.hasher),
decoder: (data, codec, hasher) => Block.decoder(data, codec || opts.codec, hasher || opts.hasher),
createUnsafe: (data, cid, { hasher, codec } = {}) => Block.createUnsafe(data, cid, {
hasher: hasher || opts.hasher,
codec: codec || opts.codec
}),
create: (data, cid, { hasher, codec } = {}) => Block.create(data, cid, {
hasher: hasher || opts.hasher,
codec: codec || opts.codec
}),
codecs: Block.codecs,
add: Block.add
})
Block.CID = CID

export default Block
7 changes: 2 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
"standard": "^14.3.4"
},
"dependencies": {
"@ipld/dag-cbor": "1.1.11",
"@ipld/dag-cbor": "2.0.2",
"class-is": "^1.1.0",
"lodash.transform": "^4.6.0",
"multiformats": "^3.0.3"
"multiformats": "^4.0.0"
},
"repository": {
"type": "git",
Expand All @@ -43,9 +43,6 @@
".": {
"import": "./index.js"
},
"./basics": {
"import": "./basics.js"
},
"./defaults": {
"import": "./defaults.js"
}
Expand Down
Loading