@web3-storage/pail
Version:
DAG based key value store.
152 lines (151 loc) • 5.84 kB
JavaScript
// eslint-disable-next-line no-unused-vars
import * as API from './api.js';
import { ShardFetcher } from './shard.js';
/**
* @typedef {string} K
* @typedef {[before: null, after: API.UnknownLink]} AddV
* @typedef {[before: API.UnknownLink, after: API.UnknownLink]} UpdateV
* @typedef {[before: API.UnknownLink, after: null]} DeleteV
* @typedef {[key: K, value: AddV|UpdateV|DeleteV]} KV
* @typedef {KV[]} KeysDiff
* @typedef {{ keys: KeysDiff, shards: API.ShardDiff }} CombinedDiff
*/
/**
* @param {API.BlockFetcher} blocks Bucket block storage.
* @param {API.ShardLink} a Base DAG.
* @param {API.ShardLink} b Comparison DAG.
* @returns {Promise<CombinedDiff>}
*/
export const difference = async (blocks, a, b) => {
if (isEqual(a, b))
return { keys: [], shards: { additions: [], removals: [] } };
const shards = new ShardFetcher(blocks);
const [ashard, bshard] = await Promise.all([shards.get(a), shards.get(b)]);
const aents = new Map(ashard.value.entries);
const bents = new Map(bshard.value.entries);
const keys = /** @type {Map<K, AddV|UpdateV|DeleteV>} */ (new Map());
const additions = new Map([[bshard.cid.toString(), bshard]]);
const removals = new Map([[ashard.cid.toString(), ashard]]);
// find shards removed in B
for (const [akey, aval] of ashard.value.entries) {
const bval = bents.get(akey);
if (bval)
continue;
if (!Array.isArray(aval)) {
keys.set(`${ashard.value.prefix}${akey}`, [aval, null]);
continue;
}
// if shard link _with_ value
if (aval[1] != null) {
keys.set(`${ashard.value.prefix}${akey}`, [aval[1], null]);
}
for await (const s of collect(shards, aval[0])) {
for (const [k, v] of s.value.entries) {
if (!Array.isArray(v)) {
keys.set(`${s.value.prefix}${k}`, [v, null]);
}
else if (v[1] != null) {
keys.set(`${s.value.prefix}${k}`, [v[1], null]);
}
}
removals.set(s.cid.toString(), s);
}
}
// find shards added or updated in B
for (const [bkey, bval] of bshard.value.entries) {
const aval = aents.get(bkey);
if (!Array.isArray(bval)) {
if (!aval) {
keys.set(`${bshard.value.prefix}${bkey}`, [null, bval]);
}
else if (Array.isArray(aval)) {
keys.set(`${bshard.value.prefix}${bkey}`, [aval[1] ?? null, bval]);
}
else if (!isEqual(aval, bval)) {
keys.set(`${bshard.value.prefix}${bkey}`, [aval, bval]);
}
continue;
}
if (aval && Array.isArray(aval)) { // updated in B
if (isEqual(aval[0], bval[0])) {
if (bval[1] != null && (aval[1] == null || !isEqual(aval[1], bval[1]))) {
keys.set(`${bshard.value.prefix}${bkey}`, [aval[1] ?? null, bval[1]]);
}
continue; // updated value?
}
const res = await difference(blocks, aval[0], bval[0]);
for (const shard of res.shards.additions) {
additions.set(shard.cid.toString(), shard);
}
for (const shard of res.shards.removals) {
removals.set(shard.cid.toString(), shard);
}
for (const [k, v] of res.keys) {
keys.set(k, v);
}
}
else if (aval) { // updated in B value => link+value
if (bval[1] == null) {
keys.set(`${bshard.value.prefix}${bkey}`, [aval, null]);
}
else if (!isEqual(aval, bval[1])) {
keys.set(`${bshard.value.prefix}${bkey}`, [aval, bval[1]]);
}
for await (const s of collect(shards, bval[0])) {
for (const [k, v] of s.value.entries) {
if (!Array.isArray(v)) {
keys.set(`${s.value.prefix}${k}`, [null, v]);
}
else if (v[1] != null) {
keys.set(`${s.value.prefix}${k}`, [null, v[1]]);
}
}
additions.set(s.cid.toString(), s);
}
}
else { // added in B
keys.set(`${bshard.value.prefix}${bkey}`, [null, bval[0]]);
for await (const s of collect(shards, bval[0])) {
for (const [k, v] of s.value.entries) {
if (!Array.isArray(v)) {
keys.set(`${s.value.prefix}${k}`, [null, v]);
}
else if (v[1] != null) {
keys.set(`${s.value.prefix}${k}`, [null, v[1]]);
}
}
additions.set(s.cid.toString(), s);
}
}
}
// filter blocks that were added _and_ removed from B
for (const k of removals.keys()) {
if (additions.has(k)) {
additions.delete(k);
removals.delete(k);
}
}
return {
keys: [...keys.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1),
shards: { additions: [...additions.values()], removals: [...removals.values()] }
};
};
/**
* @param {API.UnknownLink} a
* @param {API.UnknownLink} b
*/
const isEqual = (a, b) => a.toString() === b.toString();
/**
* @param {import('./shard.js').ShardFetcher} shards
* @param {API.ShardLink} root
* @returns {AsyncIterableIterator<API.ShardBlockView>}
*/
async function* collect(shards, root) {
const shard = await shards.get(root);
yield shard;
for (const [, v] of shard.value.entries) {
if (!Array.isArray(v))
continue;
yield* collect(shards, v[0]);
}
}