+239
@@ -0,0 +1,239 @@
|
||||
import { bytes as binary, CID } from './index.js'
|
||||
import type * as API from './interface.js'
|
||||
|
||||
function readonly ({ enumerable = true, configurable = false } = {}): { enumerable: boolean, configurable: boolean, writable: false } {
|
||||
return { enumerable, configurable, writable: false }
|
||||
}
|
||||
|
||||
function * linksWithin (path: [string | number, string], value: any): Iterable<[string, CID]> {
|
||||
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 != null) {
|
||||
yield [elementPath.join('/'), cid]
|
||||
} else if (typeof element === 'object') {
|
||||
yield * links(element, elementPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const cid = CID.asCID(value)
|
||||
if (cid != null) {
|
||||
yield [path.join('/'), cid]
|
||||
} else {
|
||||
yield * links(value, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function * links <T> (source: T, base: Array<string | number>): Iterable<[string, CID]> {
|
||||
if (source == null || source instanceof Uint8Array) {
|
||||
return
|
||||
}
|
||||
const cid = CID.asCID(source)
|
||||
if (cid != null) {
|
||||
yield [base.join('/'), cid]
|
||||
}
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
const path = [...base, key] as [string | number, string]
|
||||
yield * linksWithin(path, value)
|
||||
}
|
||||
}
|
||||
|
||||
function * treeWithin (path: [string | number, string], value: any): Iterable<string> {
|
||||
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) == null)) {
|
||||
yield * tree(element, elementPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
yield * tree(value, path)
|
||||
}
|
||||
}
|
||||
|
||||
function * tree <T> (source: T, base: Array<string | number>): Iterable<string> {
|
||||
if (source == null || typeof source !== 'object') {
|
||||
return
|
||||
}
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
const path = [...base, key] as [string | number, string]
|
||||
yield path.join('/')
|
||||
if (value != null && !(value instanceof Uint8Array) && typeof value === 'object' && (CID.asCID(value) == null)) {
|
||||
yield * treeWithin(path, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get <T> (source: T, path: string[]): API.BlockCursorView<unknown> {
|
||||
let node = source as Record<string, any>
|
||||
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 != null) {
|
||||
return { value: cid, remaining: path.slice(index + 1).join('/') }
|
||||
}
|
||||
}
|
||||
return { value: node }
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 class Block<T, C extends number, A extends number, V extends API.Version> implements API.BlockView<T, C, A, V> {
|
||||
readonly cid: CID<T, C, A, V>
|
||||
readonly bytes: API.ByteView<T>
|
||||
readonly value: T
|
||||
readonly asBlock: this
|
||||
|
||||
constructor ({ cid, bytes, value }: { cid: CID<T, C, A, V>, bytes: API.ByteView<T>, value: T }) {
|
||||
if (cid == null || bytes == null || typeof value === 'undefined') { throw new Error('Missing required argument') }
|
||||
|
||||
this.cid = cid
|
||||
this.bytes = bytes
|
||||
this.value = value
|
||||
this.asBlock = this
|
||||
|
||||
// Mark all the properties immutable
|
||||
Object.defineProperties(this, {
|
||||
cid: readonly(),
|
||||
bytes: readonly(),
|
||||
value: readonly(),
|
||||
asBlock: readonly()
|
||||
})
|
||||
}
|
||||
|
||||
links (): Iterable<[string, CID<unknown, number, number, API.Version>]> {
|
||||
return links(this.value, [])
|
||||
}
|
||||
|
||||
tree (): Iterable<string> {
|
||||
return tree(this.value, [])
|
||||
}
|
||||
|
||||
get (path = '/'): API.BlockCursorView<unknown> {
|
||||
return get(this.value, path.split('/').filter(Boolean))
|
||||
}
|
||||
}
|
||||
|
||||
interface EncodeInput <T, Code extends number, Alg extends number> {
|
||||
value: T
|
||||
codec: API.BlockEncoder<Code, T>
|
||||
hasher: API.MultihashHasher<Alg>
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T - Logical type of the data encoded in the block
|
||||
* @template Code - multicodec code corresponding to codec used to encode the block
|
||||
* @template Alg - multicodec code corresponding to the hashing algorithm used in CID creation.
|
||||
*/
|
||||
export async function encode <T, Code extends number, Alg extends number> ({ value, codec, hasher }: EncodeInput<T, Code, Alg>): Promise<API.BlockView<T, Code, Alg>> {
|
||||
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 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>
|
||||
|
||||
return new Block({ value, bytes, cid })
|
||||
}
|
||||
|
||||
interface DecodeInput <T, Code extends number, Alg extends number> {
|
||||
bytes: API.ByteView<T>
|
||||
codec: API.BlockDecoder<Code, T>
|
||||
hasher: API.MultihashHasher<Alg>
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T - Logical type of the data encoded in the block
|
||||
* @template Code - multicodec code corresponding to codec used to encode the block
|
||||
* @template Alg - multicodec code corresponding to the hashing algorithm used in CID creation.
|
||||
*/
|
||||
export async function decode <T, Code extends number, Alg extends number> ({ bytes, codec, hasher }: DecodeInput<T, Code, Alg>): Promise<API.BlockView<T, Code, Alg>> {
|
||||
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 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>
|
||||
|
||||
return new Block({ value, bytes, cid })
|
||||
}
|
||||
|
||||
type CreateUnsafeInput <T, Code extends number, Alg extends number, V extends API.Version> = {
|
||||
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>
|
||||
value?: undefined
|
||||
codec: API.BlockDecoder<Code, T>
|
||||
bytes: API.ByteView<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T - Logical type of the data encoded in the block
|
||||
* @template Code - multicodec code corresponding to codec used to encode the block
|
||||
* @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
|
||||
? maybeValue
|
||||
: (codec?.decode(bytes))
|
||||
|
||||
if (value === undefined) throw new Error('Missing required argument, must either provide "value" or "codec"')
|
||||
|
||||
return new Block({
|
||||
cid: cid as CID<T, Code, Alg, V>,
|
||||
bytes,
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
interface CreateInput <T, Code extends number, Alg extends number, V extends API.Version> {
|
||||
bytes: API.ByteView<T>
|
||||
cid: API.Link<T, Code, Alg, V>
|
||||
hasher: API.MultihashHasher<Alg>
|
||||
codec: API.BlockDecoder<Code, T>
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T - Logical type of the data encoded in the block
|
||||
* @template Code - multicodec code corresponding to codec used to encode the block
|
||||
* @template Alg - multicodec code corresponding to the hashing algorithm used in CID creation.
|
||||
* @template V - CID version
|
||||
*/
|
||||
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 hash = await hasher.digest(bytes)
|
||||
if (!binary.equals(cid.multihash.bytes, hash.bytes)) {
|
||||
throw new Error('CID hash does not match bytes')
|
||||
}
|
||||
|
||||
return createUnsafe({
|
||||
bytes,
|
||||
cid,
|
||||
value,
|
||||
codec
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user