@ucanto/core
Version:
144 lines (125 loc) • 3.73 kB
JavaScript
import * as API from '@ucanto/interface'
import { CarBufferReader } from '@ipld/car/buffer-reader'
import * as CarBufferWriter from '@ipld/car/buffer-writer'
import { base32 } from 'multiformats/bases/base32'
import { create as createLink } from './link.js'
import { sha256 } from 'multiformats/hashes/sha2'
// @see https://www.iana.org/assignments/media-types/application/vnd.ipld.car
export const contentType = 'application/vnd.ipld.car'
export const name = 'CAR'
/** @type {API.MulticodecCode<0x0202, 'CAR'>} */
export const code = 0x0202
/**
* @typedef {{
* roots: API.IPLDBlock[]
* blocks: Map<string, API.IPLDBlock>
* }} Model
*/
class Writer {
/**
* @param {API.IPLDBlock[]} blocks
* @param {number} byteLength
*/
constructor(blocks = [], byteLength = 0) {
this.written = new Set()
this.blocks = blocks
this.byteLength = byteLength
}
/**
* @param {API.IPLDBlock[]} blocks
*/
write(...blocks) {
for (const block of blocks) {
const id = block.cid.toString(base32)
if (!this.written.has(id)) {
this.blocks.push(block)
this.byteLength += CarBufferWriter.blockLength(
/** @type {any} */ (block)
)
this.written.add(id)
}
}
return this
}
/**
* @param {API.IPLDBlock[]} rootBlocks
*/
flush(...rootBlocks) {
const roots = []
// We reverse the roots so that the first root is the last block in the CAR
for (const block of rootBlocks.reverse()) {
const id = block.cid.toString(base32)
if (!this.written.has(id)) {
this.blocks.unshift(block)
this.byteLength += CarBufferWriter.blockLength({
cid: /** @type {CarBufferWriter.CID} */ (block.cid),
bytes: block.bytes,
})
this.written.add(id)
}
// We unshift here because we want to preserve the order of the roots
roots.unshift(/** @type {CarBufferWriter.CID} */ (block.cid))
}
this.byteLength += CarBufferWriter.headerLength({ roots })
const buffer = new ArrayBuffer(this.byteLength)
const writer = CarBufferWriter.createWriter(buffer, { roots })
for (const block of /** @type {CarBufferWriter.Block[]} */ (this.blocks)) {
writer.write(block)
}
return writer.close()
}
}
export const createWriter = () => new Writer()
/**
* @template {Partial<Model>} T
* @param {T} input
* @returns {API.ByteView<T>}
*/
export const encode = ({ roots = [], blocks }) => {
const writer = new Writer()
if (blocks) {
writer.write(...blocks.values())
}
return writer.flush(...roots)
}
/**
* @param {API.ByteView<Partial<Model>>} bytes
* @returns {Model}
*/
export const decode = bytes => {
const reader = CarBufferReader.fromBytes(bytes)
/** @type {API.IPLDBlock[]} */
const roots = []
const blocks = new Map()
for (const root of reader.getRoots()) {
const block = /** @type {API.IPLDBlock} */ (reader.get(root))
if (block) {
roots.push(block)
}
}
for (const block of reader.blocks()) {
blocks.set(block.cid.toString(), block)
}
return { roots, blocks }
}
/**
* @template {Partial<Model>} T
* @param {API.ByteView<T>} bytes
* @param {{hasher?: API.MultihashHasher }} options
*/
export const link = async (bytes, { hasher = sha256 } = {}) => {
return /** @type {API.Link<T, typeof code, typeof hasher.code>} */ (
createLink(code, await hasher.digest(bytes))
)
}
/**
* @template {Partial<Model>} T
* @param {T} data
* @param {{hasher?: API.MultihashHasher }} [options]
* @returns {Promise<API.Block<T, typeof code>>}
*/
export const write = async (data, options) => {
const bytes = encode(data)
const cid = await link(bytes, options)
return { bytes, cid }
}