@ethersphere/bee-js
Version:
Javascript client for Bee
410 lines (409 loc) • 17 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MantarayNode = exports.Fork = void 0;
const cafe_utility_1 = require("cafe-utility");
const debug_1 = __importDefault(require("debug"));
const __1 = require("..");
const typed_bytes_1 = require("../utils/typed-bytes");
const debug = (0, debug_1.default)('bee-js:manifest');
const ENCODER = new TextEncoder();
const DECODER = new TextDecoder();
const TYPE_VALUE = 2;
const TYPE_EDGE = 4;
const TYPE_WITH_PATH_SEPARATOR = 8;
const TYPE_WITH_METADATA = 16;
const PATH_SEPARATOR = new Uint8Array([47]);
const VERSION_02_HASH_HEX = '5768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f7b';
const VERSION_02_HASH = cafe_utility_1.Binary.hexToUint8Array(VERSION_02_HASH_HEX);
class Fork {
constructor(prefix, node) {
this.prefix = prefix;
this.node = node;
}
static split(a, b) {
const commonPart = cafe_utility_1.Binary.commonPrefix(a.prefix, b.prefix);
if (commonPart.length === a.prefix.length) {
const remainingB = b.prefix.slice(commonPart.length);
b.node.path = b.prefix.slice(commonPart.length);
b.prefix = b.prefix.slice(commonPart.length);
b.node.parent = a.node;
a.node.forks.set(remainingB[0], b);
return a;
}
if (commonPart.length === b.prefix.length) {
const remainingA = a.prefix.slice(commonPart.length);
a.node.path = a.prefix.slice(commonPart.length);
a.prefix = a.prefix.slice(commonPart.length);
a.node.parent = b.node;
b.node.forks.set(remainingA[0], a);
return b;
}
const node = new MantarayNode({ path: commonPart });
const newAFork = new Fork(a.prefix.slice(commonPart.length), a.node);
const newBFork = new Fork(b.prefix.slice(commonPart.length), b.node);
a.node.path = a.prefix.slice(commonPart.length);
b.node.path = b.prefix.slice(commonPart.length);
a.prefix = a.prefix.slice(commonPart.length);
b.prefix = b.prefix.slice(commonPart.length);
node.forks.set(newAFork.prefix[0], newAFork);
node.forks.set(newBFork.prefix[0], newBFork);
newAFork.node.parent = node;
newBFork.node.parent = node;
return new Fork(commonPart, node);
}
marshal() {
if (!this.node.selfAddress) {
throw Error('Fork#marshal node.selfAddress is not set');
}
const data = [];
data.push(new Uint8Array([this.node.determineType()]));
data.push(cafe_utility_1.Binary.numberToUint8(this.prefix.length));
data.push(this.prefix);
if (this.prefix.length < 30) {
data.push(new Uint8Array(30 - this.prefix.length));
}
data.push(this.node.selfAddress);
debug('marshalling fork', {
prefixLength: this.prefix.length,
prefix: DECODER.decode(this.prefix),
address: cafe_utility_1.Binary.uint8ArrayToHex(this.node.selfAddress),
});
if (this.node.metadata) {
const metadataBytes = cafe_utility_1.Binary.padEndToMultiple(new Uint8Array([0x00, 0x00, ...ENCODER.encode(JSON.stringify(this.node.metadata))]), 32, 0x0a);
const metadataLengthBytes = cafe_utility_1.Binary.numberToUint16(metadataBytes.length - 2, 'BE');
metadataBytes.set(metadataLengthBytes, 0);
data.push(metadataBytes);
}
return cafe_utility_1.Binary.concatBytes(...data);
}
static unmarshal(reader, addressLength) {
const type = cafe_utility_1.Binary.uint8ToNumber(reader.read(1));
const prefixLength = cafe_utility_1.Binary.uint8ToNumber(reader.read(1));
const prefix = reader.read(prefixLength);
if (prefixLength < 30) {
reader.read(30 - prefixLength);
}
const selfAddress = reader.read(addressLength);
debug('unmarshalling fork', {
type,
prefixLength,
prefix: DECODER.decode(prefix),
addressLength,
address: cafe_utility_1.Binary.uint8ArrayToHex(selfAddress),
});
let metadata = undefined;
if (isType(type, TYPE_WITH_METADATA)) {
const metadataLength = cafe_utility_1.Binary.uint16ToNumber(reader.read(2), 'BE');
metadata = JSON.parse(DECODER.decode(reader.read(metadataLength)));
}
return new Fork(prefix, new MantarayNode({ selfAddress, metadata, path: prefix }));
}
}
exports.Fork = Fork;
class MantarayNode {
constructor(options) {
this.obfuscationKey = new Uint8Array(32);
this.selfAddress = null;
this.targetAddress = new Uint8Array(32);
this.metadata = null;
this.path = new Uint8Array(0);
this.forks = new Map();
this.parent = null;
if (options?.targetAddress) {
this.targetAddress = options.targetAddress;
}
if (options?.selfAddress) {
this.selfAddress = options.selfAddress;
}
if (options?.metadata) {
this.metadata = options.metadata;
}
if (options?.obfuscationKey) {
this.obfuscationKey = options.obfuscationKey;
}
if (options?.path) {
this.path = options.path;
}
if (options?.parent) {
this.parent = options.parent;
}
}
get fullPath() {
return cafe_utility_1.Binary.concatBytes(this.parent?.fullPath ?? new Uint8Array(0), this.path);
}
get fullPathString() {
return DECODER.decode(this.fullPath);
}
/**
* Returns the metadata at the `/` path to access idiomatic properties.
*/
getRootMetadata() {
const node = this.find('/');
if (node && node.metadata) {
return cafe_utility_1.Optional.of(node.metadata);
}
return cafe_utility_1.Optional.empty();
}
/**
* Returns the `swarm-index-document` and `swarm-error-document` metadata values.
*/
getDocsMetadata() {
const node = this.find('/');
if (!node || !node.metadata) {
return { indexDocument: null, errorDocument: null };
}
return {
indexDocument: node.metadata['website-index-document'] ?? null,
errorDocument: node.metadata['website-error-document'] ?? null,
};
}
/**
* Attempts to resolve the manifest as a feed, returning the latest update.
*/
async resolveFeed(bee, requestOptions) {
const node = this.find('/');
if (!node || !node.metadata) {
return cafe_utility_1.Optional.empty();
}
const owner = node.metadata['swarm-feed-owner'];
const topic = node.metadata['swarm-feed-topic'];
if (!owner || !topic) {
return cafe_utility_1.Optional.empty();
}
return cafe_utility_1.Optional.of(await bee.fetchLatestFeedUpdate(topic, owner, requestOptions));
}
/**
* Gets the binary representation of the node.
*/
async marshal() {
for (const fork of this.forks.values()) {
if (!fork.node.selfAddress) {
fork.node.selfAddress = (await fork.node.calculateSelfAddress()).toUint8Array();
}
}
const header = new Uint8Array(32);
header.set(VERSION_02_HASH, 0);
header.set(cafe_utility_1.Binary.equals(this.targetAddress, __1.NULL_ADDRESS) && cafe_utility_1.Binary.equals(this.path, new Uint8Array([47]))
? cafe_utility_1.Binary.numberToUint8(0)
: cafe_utility_1.Binary.numberToUint8(this.targetAddress.length), 31);
const forkBitmap = new Uint8Array(32);
for (const fork of this.forks.keys()) {
cafe_utility_1.Binary.setBit(forkBitmap, fork, 1, 'LE');
}
const forks = [];
for (let i = 0; i < 256; i++) {
if (cafe_utility_1.Binary.getBit(forkBitmap, i, 'LE')) {
forks.push(this.forks.get(i).marshal());
}
}
const data = cafe_utility_1.Binary.xorCypher(cafe_utility_1.Binary.concatBytes(header, cafe_utility_1.Binary.equals(this.targetAddress, __1.NULL_ADDRESS) && cafe_utility_1.Binary.equals(this.path, new Uint8Array([47]))
? new Uint8Array(0)
: this.targetAddress, forkBitmap, ...forks), this.obfuscationKey);
return cafe_utility_1.Binary.concatBytes(this.obfuscationKey, data);
}
/**
* Downloads and unmarshals a MantarayNode from the given reference.
*
* Do not forget calling `loadRecursively` on the returned node to load the entire tree.
*/
static async unmarshal(bee, reference, options, requestOptions) {
reference = new typed_bytes_1.Reference(reference);
const data = (await bee.downloadData(reference, options, requestOptions)).toUint8Array();
return this.unmarshalFromData(data, reference.toUint8Array());
}
/**
* Unmarshals a MantarayNode from the given data.
*
* Do not forget calling `loadRecursively` on the returned node to load the entire tree.
*/
static unmarshalFromData(data, selfAddress) {
const obfuscationKey = data.subarray(0, 32);
const decrypted = cafe_utility_1.Binary.xorCypher(data.subarray(32), obfuscationKey);
const reader = new cafe_utility_1.Uint8ArrayReader(decrypted);
const versionHash = reader.read(31);
if (!cafe_utility_1.Binary.equals(versionHash, VERSION_02_HASH.slice(0, 31))) {
throw new Error('MantarayNode#unmarshal invalid version hash');
}
const targetAddressLength = cafe_utility_1.Binary.uint8ToNumber(reader.read(1));
const targetAddress = targetAddressLength ? reader.read(targetAddressLength) : __1.NULL_ADDRESS;
const node = new MantarayNode({ selfAddress, targetAddress, obfuscationKey });
const forkBitmap = reader.read(32);
for (let i = 0; i < 256; i++) {
if (cafe_utility_1.Binary.getBit(forkBitmap, i, 'LE')) {
const newFork = Fork.unmarshal(reader, selfAddress.length);
node.forks.set(i, newFork);
newFork.node.parent = node;
}
}
return node;
}
/**
* Adds a fork to the node.
*/
addFork(path, reference, metadata) {
this.selfAddress = null;
path = path instanceof Uint8Array ? path : ENCODER.encode(path);
debug('adding fork', { path: DECODER.decode(path), reference: new typed_bytes_1.Reference(reference).represent() });
// TODO: this should not be ignored
// eslint-disable-next-line @typescript-eslint/no-this-alias
let tip = this;
while (path.length) {
const prefix = path.slice(0, 30);
path = path.slice(30);
const isLast = path.length === 0;
const [bestMatch, matchedPath] = tip.findClosest(prefix);
const remainingPath = prefix.slice(matchedPath.length);
if (matchedPath.length) {
tip = bestMatch;
}
if (!remainingPath.length) {
continue;
}
const newFork = new Fork(remainingPath, new MantarayNode({
targetAddress: isLast ? new typed_bytes_1.Reference(reference).toUint8Array() : undefined,
metadata: isLast ? metadata : undefined,
path: remainingPath,
}));
const existing = bestMatch.forks.get(remainingPath[0]);
if (existing) {
const fork = Fork.split(newFork, existing);
tip.forks.set(remainingPath[0], fork);
fork.node.parent = tip;
tip.selfAddress = null;
tip = newFork.node;
}
else {
tip.forks.set(remainingPath[0], newFork);
newFork.node.parent = tip;
tip.selfAddress = null;
tip = newFork.node;
}
}
}
/**
* Removes a fork from the node.
*/
removeFork(path) {
this.selfAddress = null;
path = path instanceof Uint8Array ? path : ENCODER.encode(path);
if (path.length === 0) {
throw Error('MantarayNode#removeFork [path] parameter cannot be empty');
}
const match = this.find(path);
if (!match) {
throw Error('MantarayNode#removeFork fork not found');
}
const [parent, matchedPath] = this.findClosest(path.slice(0, path.length - 1));
parent.forks.delete(path.slice(matchedPath.length)[0]);
for (const fork of match.forks.values()) {
parent.addFork(cafe_utility_1.Binary.concatBytes(match.path, fork.prefix), fork.node.targetAddress, fork.node.metadata);
}
}
/**
* Calculates the self address of the node.
*/
async calculateSelfAddress() {
if (this.selfAddress) {
return new typed_bytes_1.Reference(this.selfAddress);
}
return new typed_bytes_1.Reference((await cafe_utility_1.MerkleTree.root(await this.marshal())).hash());
}
/**
* Saves the node and its children recursively.
*/
async saveRecursively(bee, postageBatchId, options, requestOptions) {
for (const fork of this.forks.values()) {
await fork.node.saveRecursively(bee, postageBatchId, options, requestOptions);
}
const result = await bee.uploadData(postageBatchId, await this.marshal(), options, requestOptions);
this.selfAddress = result.reference.toUint8Array();
return result;
}
/**
* Loads the node and its children recursively.
*/
async loadRecursively(bee, options, requestOptions) {
for (const fork of this.forks.values()) {
if (!fork.node.selfAddress) {
throw Error('MantarayNode#loadRecursively fork.node.selfAddress is not set');
}
const node = await MantarayNode.unmarshal(bee, fork.node.selfAddress, options, requestOptions);
fork.node.targetAddress = node.targetAddress;
fork.node.forks = node.forks;
fork.node.path = fork.prefix;
fork.node.parent = this;
await fork.node.loadRecursively(bee, options, requestOptions);
}
}
/**
* Finds a node in the tree by its path.
*/
find(path) {
const [closest, match] = this.findClosest(path);
return match.length === path.length ? closest : null;
}
/**
* Finds the closest node in the tree to the given path.
*/
findClosest(path, current = new Uint8Array()) {
path = path instanceof Uint8Array ? path : ENCODER.encode(path);
if (path.length === 0) {
return [this, current];
}
const fork = this.forks.get(path[0]);
if (fork && cafe_utility_1.Binary.commonPrefix(fork.prefix, path).length === fork.prefix.length) {
return fork.node.findClosest(path.slice(fork.prefix.length), cafe_utility_1.Binary.concatBytes(current, fork.prefix));
}
return [this, current];
}
/**
* Returns an array of all nodes in the tree which have a target address set.
*
* Must be called after `loadRecursively`.
*/
collect(nodes = []) {
for (const fork of this.forks.values()) {
if (!cafe_utility_1.Binary.equals(fork.node.targetAddress, __1.NULL_ADDRESS)) {
nodes.push(fork.node);
}
fork.node.collect(nodes);
}
return nodes;
}
/**
* Returns a path:reference map of all nodes in the tree which have a target address set.
*
* Must be called after `loadRecursively`.
*/
collectAndMap() {
const nodes = this.collect();
const result = {};
for (const node of nodes) {
result[node.fullPathString] = new typed_bytes_1.Reference(node.targetAddress).toHex();
}
return result;
}
determineType() {
let type = 0;
if (!cafe_utility_1.Binary.equals(this.targetAddress, __1.NULL_ADDRESS) || cafe_utility_1.Binary.equals(this.path, PATH_SEPARATOR)) {
type |= TYPE_VALUE;
}
if (this.forks.size > 0) {
type |= TYPE_EDGE;
}
if (cafe_utility_1.Binary.indexOf(this.path, PATH_SEPARATOR) !== -1 && !cafe_utility_1.Binary.equals(this.path, PATH_SEPARATOR)) {
type |= TYPE_WITH_PATH_SEPARATOR;
}
if (this.metadata) {
type |= TYPE_WITH_METADATA;
}
return type;
}
}
exports.MantarayNode = MantarayNode;
function isType(value, type) {
return (value & type) === type;
}