diff --git a/src/basics-browser.js b/src/basics-browser.js index f4fef934..20e9afec 100644 --- a/src/basics-browser.js +++ b/src/basics-browser.js @@ -1,8 +1,8 @@ // @ts-check import * as base64 from './bases/base64-browser.js' -import { CID, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' +import { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' const bases = { ..._bases, ...base64 } -export { CID, hasher, digest, varint, bytes, hashes, codecs, bases } +export { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics-import.js b/src/basics-import.js index f5f2a224..2421269f 100644 --- a/src/basics-import.js +++ b/src/basics-import.js @@ -1,6 +1,6 @@ -import { CID, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' +import { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' import * as base64 from './bases/base64-import.js' const bases = { ..._bases, ...base64 } -export { CID, hasher, digest, varint, bytes, hashes, codecs, bases } +export { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics.js b/src/basics.js index 66974a19..37de1fe8 100644 --- a/src/basics.js +++ b/src/basics.js @@ -7,10 +7,10 @@ import * as sha2 from './hashes/sha2.js' import raw from './codecs/raw.js' import json from './codecs/json.js' -import { CID, hasher, digest, varint, bytes } from './index.js' +import { CID, Block, hasher, digest, varint, bytes } from './index.js' const bases = { ...base32, ...base58 } const hashes = { ...sha2 } const codecs = { raw, json } -export { CID, hasher, digest, varint, bytes, hashes, bases, codecs } +export { CID, Block, hasher, digest, varint, bytes, hashes, bases, codecs } diff --git a/src/block.js b/src/block.js new file mode 100644 index 00000000..3bed5724 --- /dev/null +++ b/src/block.js @@ -0,0 +1,308 @@ +// @ts-check + +import CID from './cid.js' + +/** + * @template {number} Code + * @template T + * @class + */ +export default class Block { + /** + * @param {CID|null} cid + * @param {Code} code + * @param {T} data + * @param {Uint8Array} bytes + * @param {BlockConfig} config + */ + constructor (cid, code, data, bytes, { hasher }) { + /** @type {CID|Promise|null} */ + this._cid = cid + this.code = code + this.data = data + this.bytes = bytes + this.hasher = hasher + } + + async cid () { + const { _cid: cid } = this + if (cid != null) { + return await cid + } else { + const { bytes, code, hasher } = this + // First we store promise to avoid a race condition if cid is called + // whlie promise is pending. + const promise = createCID(hasher, bytes, code) + this._cid = promise + const cid = await promise + // Once promise resolves we store an actual CID. + this._cid = cid + return cid + } + } + + links () { + return links(this.data, []) + } + + tree () { + return tree(this.data, []) + } + + /** + * @param {string} path + */ + get (path) { + return get(this.data, path.split('/').filter(Boolean)) + } + + /** + * @template {number} Code + * @template T + * @param {Encoder} codec + * @param {BlockConfig} options + */ + static encoder (codec, options) { + return new BlockEncoder(codec, options) + } + + /** + * @template {number} Code + * @template T + * @param {Decoder} codec + * @param {BlockConfig} options + */ + static decoder (codec, options) { + return new BlockDecoder(codec, options) + } + + /** + * @template {number} Code + * @template T + * @param {Object} codec + * @param {Encoder} codec.encoder + * @param {Decoder} codec.decoder + * @param {BlockConfig} options + * @returns {BlockCodec} + */ + + static codec ({ encoder, decoder }, options) { + return new BlockCodec(encoder, decoder, options) + } +} + +/** + * @template T + * @param {T} source + * @param {Array} base + * @returns {Iterable<[string, CID]>} + */ +const links = function * (source, base) { + for (const [key, value] of Object.entries(source)) { + const path = [...base, key] + if (value != null && typeof value === 'object') { + if (Array.isArray(value)) { + for (const [index, element] of value.entries()) { + const elementPath = [...path, index] + const cid = CID.asCID(element) + if (cid) { + yield [elementPath.join('/'), cid] + } else if (typeof element === 'object') { + yield * links(element, elementPath) + } + } + } else { + const cid = CID.asCID(value) + if (cid) { + yield [path.join('/'), cid] + } else { + yield * links(value, path) + } + } + } + } +} + +/** + * @template T + * @param {T} source + * @param {Array} base + * @returns {Iterable} + */ +const tree = function * (source, base) { + for (const [key, value] of Object.entries(source)) { + const path = [...base, key] + yield path.join('/') + if (value != null && typeof value === 'object' && !CID.asCID(value)) { + if (Array.isArray(value)) { + for (const [index, element] of value.entries()) { + const elementPath = [...path, index] + yield elementPath.join('/') + if (typeof element === 'object' && !CID.asCID(element)) { + yield * tree(element, elementPath) + } + } + } else { + yield * tree(value, path) + } + } + } +} + +/** + * @template T + * @param {T} source + * @param {string[]} path + */ +const get = (source, path) => { + let node = source + for (const [index, key] of path.entries()) { + node = node[key] + if (node == null) { + throw new Error(`Object has no property at ${path.slice(0, index - 1).map(part => `[${JSON.stringify(part)}]`).join('')}`) + } + const cid = CID.asCID(node) + if (cid) { + return { value: cid, remaining: path.slice(index).join('/') } + } + } + return { value: node } +} + +/** + * + * @param {Hasher} hasher + * @param {Uint8Array} bytes + * @param {number} code + */ + +const createCID = async (hasher, bytes, code) => { + const multihash = await hasher.digest(bytes) + return CID.createV1(code, multihash) +} + +/** + * @template {number} Code + * @template T + */ +class BlockCodec { + /** + * @param {Encoder} encoder + * @param {Decoder} decoder + * @param {BlockConfig} config + */ + + constructor (encoder, decoder, config) { + this.encoder = new BlockEncoder(encoder, config) + this.decoder = new BlockDecoder(decoder, config) + this.config = config + } + + /** + * @param {Uint8Array} bytes + * @param {Partial} [options] + * @returns {Block} + */ + decode (bytes, options) { + return this.decoder.decode(bytes, { ...this.config, ...options }) + } + + /** + * @param {T} data + * @param {Partial} [options] + * @returns {Block} + */ + encode (data, options) { + return this.encoder.encode(data, { ...this.config, ...options }) + } +} + +/** + * @class + * @template {number} Code + * @template T + */ +class BlockEncoder { + /** + * @param {Encoder} codec + * @param {BlockConfig} config + */ + constructor (codec, config) { + this.codec = codec + this.config = config + } + + /** + * @param {T} data + * @param {Partial} [options] + * @returns {Block} + */ + encode (data, options) { + const { codec } = this + const bytes = codec.encode(data) + return new Block(null, codec.code, data, bytes, { ...this.config, ...options }) + } +} + +/** + * @class + * @template {number} Code + * @template T + */ +class BlockDecoder { + /** + * @param {Decoder} codec + * @param {BlockConfig} config + */ + constructor (codec, config) { + this.codec = codec + this.config = config + } + + /** + * @param {Uint8Array} bytes + * @param {Partial} [options] + * @returns {Block} + */ + decode (bytes, options) { + const data = this.codec.decode(bytes) + return new Block(null, this.codec.code, data, bytes, { ...this.config, ...options }) + } +} +/** + * @typedef {import('./block/interface').Config} BlockConfig + * @typedef {import('./hashes/interface').MultihashHasher} Hasher + **/ + +/** + * @template T + * @typedef {import('./bases/interface').MultibaseEncoder} MultibaseEncoder + */ + +/** + * @template T + * @typedef {import('./bases/interface').MultibaseDecoder} MultibaseDecoder + */ + +/** + * @template T + * @typedef {import('./bases/interface').MultibaseCodec} MultibaseCodec + */ + +/** + * @template {number} Code + * @template T + * @typedef {import('./codecs/interface').BlockEncoder} Encoder + */ + +/** + * @template {number} Code + * @template T + * @typedef {import('./codecs/interface').BlockDecoder} Decoder + */ + +/** + * @template {number} Code + * @template T + * @typedef {import('./codecs/interface').BlockCodec} Codec + */ diff --git a/src/index.js b/src/index.js index d3d3144e..3ce64497 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,11 @@ // @ts-check import CID from './cid.js' +import Block from './block.js' import * as varint from './varint.js' import * as bytes from './bytes.js' import * as hasher from './hashes/hasher.js' import * as digest from './hashes/digest.js' import * as codec from './codecs/codec.js' -export { CID, hasher, digest, varint, bytes, codec } +export { CID, Block, hasher, digest, varint, bytes, codec }