@fireproof/database
Version:
Live database for the web
1,135 lines (1,117 loc) • 41.6 kB
JavaScript
console.log("Node CJS build");
'use strict';
var block$1 = require('@alanshaw/pail/block');
var car = require('@ipld/car');
var block = require('multiformats/block');
var sha2 = require('multiformats/hashes/sha2');
var raw = require('multiformats/codecs/raw');
var CBW = require('@ipld/car/buffer-writer');
var codec$1 = require('@ipld/dag-cbor');
var webcrypto = require('@peculiar/webcrypto');
var multiformats = require('multiformats');
var buffer = require('buffer');
var cidSet = require('prolly-trees/cid-set');
var utils = require('prolly-trees/utils');
var cache = require('prolly-trees/cache');
var crdt = require('@alanshaw/pail/crdt');
var clock = require('@alanshaw/pail/clock');
var charwise = require('charwise');
var DbIndex = require('prolly-trees/db-index');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var raw__namespace = /*#__PURE__*/_interopNamespaceDefault(raw);
var CBW__namespace = /*#__PURE__*/_interopNamespaceDefault(CBW);
var codec__namespace = /*#__PURE__*/_interopNamespaceDefault(codec$1);
var DbIndex__namespace = /*#__PURE__*/_interopNamespaceDefault(DbIndex);
function writeQueue(worker, payload = Infinity) {
const queue = [];
let isProcessing = false;
async function process() {
if (isProcessing || queue.length === 0)
return;
isProcessing = true;
const tasksToProcess = queue.splice(0, payload);
const updates = tasksToProcess.map(item => item.task);
const result = await worker(updates);
tasksToProcess.forEach(task => task.resolve(result));
isProcessing = false;
void process();
}
return {
push(task) {
return new Promise((resolve) => {
queue.push({ task, resolve });
void process();
});
}
};
}
async function innerMakeCarFile(fp, t) {
const { cid, bytes } = await encodeCarHeader(fp);
await t.put(cid, bytes);
return encodeCarFile(cid, t);
}
async function encodeCarFile(carHeaderBlockCid, t) {
let size = 0;
// console.log('encodeCarFile', carHeaderBlockCid.bytes.byteLength, { carHeaderBlockCid }, CBW.headerLength)
const headerSize = CBW__namespace.headerLength({ roots: [carHeaderBlockCid] });
size += headerSize;
for (const { cid, bytes } of t.entries()) {
size += CBW__namespace.blockLength({ cid, bytes });
}
const buffer = new Uint8Array(size);
const writer = CBW__namespace.createWriter(buffer, { headerSize });
writer.addRoot(carHeaderBlockCid);
for (const { cid, bytes } of t.entries()) {
writer.write({ cid, bytes });
}
writer.close();
return await block.encode({ value: writer.bytes, hasher: sha2.sha256, codec: raw__namespace });
}
async function encodeCarHeader(fp) {
return (await block.encode({
value: { fp },
hasher: sha2.sha256,
codec: codec__namespace
}));
}
async function parseCarFile(reader) {
const roots = await reader.getRoots();
const header = await reader.get(roots[0]);
if (!header)
throw new Error('missing header block');
const { value } = await block.decode({ bytes: header.bytes, hasher: sha2.sha256, codec: codec__namespace });
// @ts-ignore
if (value && value.fp === undefined)
throw new Error('missing fp');
const { fp } = value;
return fp;
}
// from https://github.com/mikeal/encrypted-block
// const crypto = new Crypto()
function getCrypto() {
try {
return new webcrypto.Crypto();
}
catch (e) {
return null;
}
}
const crypto = getCrypto();
function randomBytes(size) {
const bytes = buffer.Buffer.allocUnsafe(size);
if (size > 0) {
crypto.getRandomValues(bytes);
}
return bytes;
}
const enc32 = (value) => {
value = +value;
const buff = new Uint8Array(4);
buff[3] = (value >>> 24);
buff[2] = (value >>> 16);
buff[1] = (value >>> 8);
buff[0] = (value & 0xff);
return buff;
};
const readUInt32LE = (buffer) => {
const offset = buffer.byteLength - 4;
return ((buffer[offset]) |
(buffer[offset + 1] << 8) |
(buffer[offset + 2] << 16)) +
(buffer[offset + 3] * 0x1000000);
};
const concat = (buffers) => {
const uint8Arrays = buffers.map(b => b instanceof ArrayBuffer ? new Uint8Array(b) : b);
const totalLength = uint8Arrays.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of uint8Arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
};
const encode = ({ iv, bytes }) => concat([iv, bytes]);
const decode = (bytes) => {
const iv = bytes.subarray(0, 12);
bytes = bytes.slice(12);
return { iv, bytes };
};
const code = 0x300000 + 1337;
async function subtleKey(key) {
return await crypto.subtle.importKey('raw', // raw or jwk
key, // raw data
'AES-GCM', false, // extractable
['encrypt', 'decrypt']);
}
const decrypt$1 = async ({ key, value }) => {
let { bytes, iv } = value;
const cryKey = await subtleKey(key);
const deBytes = await crypto.subtle.decrypt({
name: 'AES-GCM',
iv,
tagLength: 128
}, cryKey, bytes);
bytes = new Uint8Array(deBytes);
const len = readUInt32LE(bytes.subarray(0, 4));
const cid = multiformats.CID.decode(bytes.subarray(4, 4 + len));
bytes = bytes.subarray(4 + len);
return { cid, bytes };
};
const encrypt$1 = async ({ key, cid, bytes }) => {
const len = enc32(cid.bytes.byteLength);
const iv = randomBytes(12);
const msg = concat([len, cid.bytes, bytes]);
try {
const cryKey = await subtleKey(key);
const deBytes = await crypto.subtle.encrypt({
name: 'AES-GCM',
iv,
tagLength: 128
}, cryKey, msg);
bytes = new Uint8Array(deBytes);
}
catch (e) {
console.log('e', e);
throw e;
}
return { value: { bytes, iv } };
};
const cryptoFn = (key) => {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return { encrypt: opts => encrypt$1({ key, ...opts }), decrypt: opts => decrypt$1({ key, ...opts }) };
};
const name = 'jchris@encrypted-block:aes-gcm';
var codec = /*#__PURE__*/Object.freeze({
__proto__: null,
code: code,
crypto: cryptoFn,
decode: decode,
decrypt: decrypt$1,
encode: encode,
encrypt: encrypt$1,
getCrypto: getCrypto,
name: name,
randomBytes: randomBytes
});
const encrypt = async function* ({ get, cids, hasher, key, cache, chunker, root }) {
const set = new Set();
let eroot;
for (const cid of cids) {
const unencrypted = await get(cid);
if (!unencrypted)
throw new Error('missing cid: ' + cid.toString());
const encrypted = await encrypt$1({ ...unencrypted, key });
const block$1 = await block.encode({ ...encrypted, codec, hasher });
yield block$1;
set.add(block$1.cid.toString());
if (unencrypted.cid.equals(root))
eroot = block$1.cid;
}
if (!eroot)
throw new Error('cids does not include root');
const list = [...set].map(s => multiformats.CID.parse(s));
let last;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
for await (const node of cidSet.create({ list, get, cache, chunker, hasher, codec: codec__namespace })) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const block = await node.block;
yield block;
last = block;
}
if (!last)
throw new Error('missing last block');
const head = [eroot, last.cid];
const block$1 = await block.encode({ value: head, codec: codec__namespace, hasher });
yield block$1;
};
const decrypt = async function* ({ root, get, key, cache, chunker, hasher }) {
const getWithDecode = async (cid) => get(cid).then(async (block$1) => {
if (!block$1)
return;
const decoded = await block.decode({ ...block$1, codec: codec__namespace, hasher });
return decoded;
});
const getWithDecrypt = async (cid) => get(cid).then(async (block$1) => {
if (!block$1)
return;
const decoded = await block.decode({ ...block$1, codec, hasher });
return decoded;
});
const decodedRoot = await getWithDecode(root);
if (!decodedRoot)
throw new Error('missing root');
if (!decodedRoot.bytes)
throw new Error('missing bytes');
const { value: [eroot, tree] } = decodedRoot;
const rootBlock = await get(eroot);
if (!rootBlock)
throw new Error('missing root block');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const cidset = await cidSet.load({ cid: tree, get: getWithDecode, cache, chunker, codec, hasher });
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const { result: nodes } = await cidset.getAllEntries();
const unwrap = async (eblock) => {
if (!eblock)
throw new Error('missing block');
if (!eblock.value) {
eblock = await block.decode({ ...eblock, codec, hasher });
}
const { bytes, cid } = await decrypt$1({ ...eblock, key }).catch(e => {
throw e;
});
const block$1 = await block.create({ cid, bytes, hasher, codec });
return block$1;
};
const promises = [];
for (const { cid } of nodes) {
if (!rootBlock.cid.equals(cid))
promises.push(getWithDecrypt(cid).then(unwrap));
}
yield* promises;
yield unwrap(rootBlock);
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const chunker = utils.bf(30);
async function encryptedMakeCarFile(key, fp, t) {
const { cid, bytes } = await encodeCarHeader(fp);
await t.put(cid, bytes);
return encryptedEncodeCarFile(key, cid, t);
}
async function encryptedEncodeCarFile(key, rootCid, t) {
const encryptionKeyBuffer = buffer.Buffer.from(key, 'hex');
const encryptionKey = encryptionKeyBuffer.buffer.slice(encryptionKeyBuffer.byteOffset, encryptionKeyBuffer.byteOffset + encryptionKeyBuffer.byteLength);
const encryptedBlocks = new block$1.MemoryBlockstore();
const cidsToEncrypt = [];
for (const { cid } of t.entries()) {
cidsToEncrypt.push(cid);
}
let last = null;
for await (const block of encrypt({
cids: cidsToEncrypt,
get: t.get.bind(t),
key: encryptionKey,
hasher: sha2.sha256,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
chunker,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
cache: cache.nocache,
root: rootCid
})) {
await encryptedBlocks.put(block.cid, block.bytes);
last = block;
}
if (!last)
throw new Error('no blocks encrypted');
const encryptedCar = await encodeCarFile(last.cid, encryptedBlocks);
return encryptedCar;
}
async function decodeEncryptedCar(key, reader) {
const roots = await reader.getRoots();
const root = roots[0];
return await decodeCarBlocks(root, reader.get.bind(reader), key);
}
async function decodeCarBlocks(root, get, keyMaterial) {
const decryptionKeyBuffer = buffer.Buffer.from(keyMaterial, 'hex');
const decryptionKey = decryptionKeyBuffer.buffer.slice(decryptionKeyBuffer.byteOffset, decryptionKeyBuffer.byteOffset + decryptionKeyBuffer.byteLength);
const decryptedBlocks = new block$1.MemoryBlockstore();
let last = null;
for await (const block of decrypt({
root,
get,
key: decryptionKey,
hasher: sha2.sha256,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
chunker,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
cache: cache.nocache
})) {
await decryptedBlocks.put(block.cid, block.bytes);
last = block;
}
if (!last)
throw new Error('no blocks decrypted');
return { blocks: decryptedBlocks, root: last.cid };
}
class Loader {
name;
opts = {};
headerStore;
carStore;
carLog = [];
carReaders = new Map();
ready;
key;
keyId;
static defaultHeader;
constructor(name, opts) {
this.name = name;
this.opts = opts || this.opts;
this.ready = this.initializeStores().then(async () => {
if (!this.headerStore || !this.carStore)
throw new Error('stores not initialized');
const meta = await this.headerStore.load('main');
return await this.ingestCarHeadFromMeta(meta);
});
}
async commit(t, done, compact = false) {
await this.ready;
const fp = this.makeCarHeader(done, this.carLog, compact);
const { cid, bytes } = this.key ? await encryptedMakeCarFile(this.key, fp, t) : await innerMakeCarFile(fp, t);
await this.carStore.save({ cid, bytes });
if (compact) {
for (const cid of this.carLog) {
await this.carStore.remove(cid);
}
this.carLog.splice(0, this.carLog.length, cid);
}
else {
this.carLog.push(cid);
}
await this.headerStore.save({ car: cid, key: this.key || null });
return cid;
}
async getBlock(cid) {
await this.ready;
for (const [, reader] of [...this.carReaders]) {
const block = await reader.get(cid);
if (block) {
return block;
}
}
}
async initializeStores() {
const isBrowser = typeof window !== 'undefined';
console.log('is browser?', isBrowser);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const module = isBrowser ? await require('./store-browser') : await require('./store-fs');
if (module) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
this.headerStore = new module.HeaderStore(this.name);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
this.carStore = new module.CarStore(this.name);
}
else {
throw new Error('Failed to initialize stores.');
}
}
async loadCar(cid) {
if (!this.headerStore || !this.carStore)
throw new Error('stores not initialized');
if (this.carReaders.has(cid.toString()))
return this.carReaders.get(cid.toString());
const car$1 = await this.carStore.load(cid);
if (!car$1)
throw new Error(`missing car file ${cid.toString()}`);
const reader = await this.ensureDecryptedReader(await car.CarReader.fromBytes(car$1.bytes));
this.carReaders.set(cid.toString(), reader);
this.carLog.push(cid);
return reader;
}
async ensureDecryptedReader(reader) {
if (!this.key)
return reader;
const { blocks, root } = await decodeEncryptedCar(this.key, reader);
return {
getRoots: () => [root],
get: blocks.get.bind(blocks)
};
}
async setKey(key) {
if (this.key && this.key !== key)
throw new Error('key already set');
this.key = key;
const crypto = getCrypto();
if (!crypto)
throw new Error('missing crypto module');
const subtle = crypto.subtle;
const encoder = new TextEncoder();
const data = encoder.encode(key);
const hashBuffer = await subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
this.keyId = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
async ingestCarHeadFromMeta(meta) {
if (!this.headerStore || !this.carStore)
throw new Error('stores not initialized');
if (!meta) {
// generate a random key
if (!this.opts.public) {
if (getCrypto()) {
await this.setKey(randomBytes(32).toString('hex'));
}
else {
console.warn('missing crypto module, using public mode');
}
}
console.log('no meta, returning default header', this.name, this.keyId);
return this.defaultHeader;
}
const { car: cid, key } = meta;
console.log('ingesting car head from meta', { car: cid, key });
if (key) {
await this.setKey(key);
}
const reader = await this.loadCar(cid);
this.carLog = [cid]; // this.carLog.push(cid)
const carHeader = await parseCarFile(reader);
await this.getMoreReaders(carHeader.cars);
return carHeader;
}
async getMoreReaders(cids) {
await Promise.all(cids.map(cid => this.loadCar(cid)));
}
}
class DbLoader extends Loader {
static defaultHeader = { cars: [], compact: [], head: [] };
defaultHeader = DbLoader.defaultHeader;
makeCarHeader({ head }, cars, compact = false) {
return compact ? { head, cars: [], compact: cars } : { head, cars, compact: [] };
}
}
class IdxLoader extends Loader {
static defaultHeader = { cars: [], compact: [], indexes: new Map() };
defaultHeader = IdxLoader.defaultHeader;
makeCarHeader({ indexes }, cars, compact = false) {
return compact ? { indexes, cars: [], compact: cars } : { indexes, cars, compact: [] };
}
}
class Transaction extends block$1.MemoryBlockstore {
parent;
constructor(parent) {
super();
this.parent = parent;
this.parent = parent;
}
async get(cid) {
return this.parent.get(cid);
}
async superGet(cid) {
return super.get(cid);
}
}
class FireproofBlockstore {
ready;
name = null;
loader = null;
opts = {};
transactions = new Set();
constructor(name, LoaderClass, opts) {
this.opts = opts || this.opts;
if (name) {
this.name = name;
this.loader = new LoaderClass(name, this.opts);
this.ready = this.loader.ready;
}
else {
this.ready = Promise.resolve(LoaderClass.defaultHeader);
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async put() {
throw new Error('use a transaction to put');
}
async get(cid) {
for (const f of this.transactions) {
const v = await f.superGet(cid);
if (v)
return v;
}
if (!this.loader)
return;
return await this.loader.getBlock(cid);
}
async commitCompaction(t, head) {
this.transactions.clear();
this.transactions.add(t);
return await this.loader?.commit(t, { head }, true);
}
async *entries() {
const seen = new Set();
for (const t of this.transactions) {
for await (const blk of t.entries()) {
if (seen.has(blk.cid.toString()))
continue;
seen.add(blk.cid.toString());
yield blk;
}
}
}
async executeTransaction(fn, commitHandler) {
const t = new Transaction(this);
this.transactions.add(t);
const done = await fn(t);
const { car, done: result } = await commitHandler(t, done);
return car ? { ...result, car } : result;
}
}
class IndexBlockstore extends FireproofBlockstore {
constructor(name, opts) {
super(name || null, IdxLoader, opts);
}
async transaction(fn, indexes) {
return this.executeTransaction(fn, async (t, done) => {
indexes.set(done.name, done);
const car = await this.loader?.commit(t, { indexes });
return { car, done };
});
}
}
class TransactionBlockstore extends FireproofBlockstore {
constructor(name, opts) {
// todo this will be a map of headers by branch name
super(name || null, DbLoader, opts);
}
async transaction(fn) {
return this.executeTransaction(fn, async (t, done) => {
const car = await this.loader?.commit(t, done);
return { car, done };
});
}
}
async function applyBulkUpdateToCrdt(tblocks, head, updates, options) {
for (const update of updates) {
const link = await makeLinkForDoc(tblocks, update);
const result = await crdt.put(tblocks, head, update.key, link, options);
for (const { cid, bytes } of [...result.additions, ...result.removals, result.event]) {
tblocks.putSync(cid, bytes);
}
head = result.head;
}
return { head };
}
async function makeLinkForDoc(blocks, update) {
let value;
if (update.del) {
value = { del: true };
}
else {
value = { doc: update.value };
}
const block$1 = await block.encode({ value, hasher: sha2.sha256, codec: codec__namespace });
blocks.putSync(block$1.cid, block$1.bytes);
return block$1.cid;
}
async function getValueFromCrdt(blocks, head, key) {
const link = await crdt.get(blocks, head, key);
if (!link)
throw new Error(`Missing key ${key}`);
return await getValueFromLink(blocks, link);
}
async function getValueFromLink(blocks, link) {
const block$1 = await blocks.get(link);
if (!block$1)
throw new Error(`Missing block ${link.toString()}`);
const { value } = (await block.decode({ bytes: block$1.bytes, hasher: sha2.sha256, codec: codec__namespace }));
return value;
}
async function clockChangesSince(blocks, head, since) {
const eventsFetcher = new clock.EventFetcher(blocks);
const keys = new Set();
const updates = await gatherUpdates(blocks, eventsFetcher, head, since, [], keys);
return { result: updates.reverse(), head };
}
async function gatherUpdates(blocks, eventsFetcher, head, since, updates = [], keys) {
const sHead = head.map(l => l.toString());
for (const link of since) {
if (sHead.includes(link.toString())) {
return updates;
}
}
for (const link of head) {
const { value: event } = await eventsFetcher.get(link);
const { key, value } = event.data;
if (keys.has(key))
continue;
keys.add(key);
const docValue = await getValueFromLink(blocks, value);
updates.push({ key, value: docValue.doc, del: docValue.del });
if (event.parents) {
updates = await gatherUpdates(blocks, eventsFetcher, event.parents, since, updates, keys);
}
}
return updates;
}
async function doCompact(blocks, head) {
const blockLog = new LoggingFetcher(blocks);
const newBlocks = new Transaction(blocks);
for await (const [, link] of crdt.entries(blockLog, head)) {
const bl = await blocks.get(link);
if (!bl)
throw new Error('Missing block: ' + link.toString());
await newBlocks.put(link, bl.bytes);
}
for (const cid of blockLog.cids) {
const bl = await blocks.get(cid);
if (!bl)
throw new Error('Missing block: ' + cid.toString());
await newBlocks.put(cid, bl.bytes);
}
await blocks.commitCompaction(newBlocks, head);
}
class LoggingFetcher {
blocks;
cids = new Set();
constructor(blocks) {
this.blocks = blocks;
}
async get(cid) {
this.cids.add(cid);
return await this.blocks.get(cid);
}
}
class CRDT {
name;
opts = {};
ready;
blocks;
indexBlocks;
indexers = new Map();
_head = [];
constructor(name, opts) {
this.name = name || null;
this.opts = opts || this.opts;
this.blocks = new TransactionBlockstore(name, this.opts);
this.indexBlocks = new IndexBlockstore(name ? name + '.idx' : undefined, this.opts);
this.ready = this.blocks.ready.then((header) => {
// @ts-ignore
if (header.indexes)
throw new Error('cannot have indexes in crdt header');
if (header.head) {
this._head = header.head;
} // todo multi head support here
});
}
async bulk(updates, options) {
await this.ready;
const tResult = await this.blocks.transaction(async (tblocks) => {
const { head } = await applyBulkUpdateToCrdt(tblocks, this._head, updates, options);
this._head = head; // we need multi head support here if allowing calls to bulk in parallel
return { head };
});
return tResult;
}
// async getAll(rootCache: any = null): Promise<{root: any, cids: CIDCounter, clockCIDs: CIDCounter, result: T[]}> {
async get(key) {
await this.ready;
const result = await getValueFromCrdt(this.blocks, this._head, key);
if (result.del)
return null;
return result;
}
async changes(since = []) {
await this.ready;
return await clockChangesSince(this.blocks, this._head, since);
}
async compact() {
await this.ready;
return await doCompact(this.blocks, this._head);
}
}
class Database {
static databases = new Map();
name;
opts = {};
_listeners = new Set();
_crdt;
_writeQueue;
constructor(name, opts) {
this.name = name;
this.opts = opts || this.opts;
this._crdt = new CRDT(name, this.opts);
this._writeQueue = writeQueue(async (updates) => {
const r = await this._crdt.bulk(updates);
await this._notify(updates);
return r;
});
}
async get(id) {
const got = await this._crdt.get(id).catch(e => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
e.message = `Not found: ${id} - ` + e.message;
throw e;
});
if (!got)
throw new Error(`Not found: ${id}`);
const { doc } = got;
return { _id: id, ...doc };
}
async put(doc) {
const { _id, ...value } = doc;
const docId = _id || 'f' + Math.random().toString(36).slice(2); // todo uuid v7
const result = await this._writeQueue.push({ key: docId, value });
return { id: docId, clock: result?.head };
}
async del(id) {
const result = await this._writeQueue.push({ key: id, del: true });
return { id, clock: result?.head };
}
async changes(since = []) {
const { result, head } = await this._crdt.changes(since);
const rows = result.map(({ key, value }) => ({
key, value: { _id: key, ...value }
}));
return { rows, clock: head };
}
subscribe(listener) {
this._listeners.add(listener);
return () => {
this._listeners.delete(listener);
};
}
async _notify(updates) {
if (this._listeners.size) {
const docs = updates.map(({ key, value }) => ({ _id: key, ...value }));
for (const listener of this._listeners) {
await listener(docs);
}
}
}
}
function database(name, opts) {
if (!Database.databases.has(name)) {
Database.databases.set(name, new Database(name, opts));
}
return Database.databases.get(name);
}
class IndexTree {
cid = null;
root = null;
}
const refCompare = (aRef, bRef) => {
if (Number.isNaN(aRef))
return -1;
if (Number.isNaN(bRef))
throw new Error('ref may not be Infinity or NaN');
if (aRef === Infinity)
return 1;
// if (!Number.isFinite(bRef)) throw new Error('ref may not be Infinity or NaN')
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
return utils.simpleCompare(aRef, bRef);
};
const compare = (a, b) => {
const [aKey, aRef] = a;
const [bKey, bRef] = b;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const comp = utils.simpleCompare(aKey, bKey);
if (comp !== 0)
return comp;
return refCompare(aRef, bRef);
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const byKeyOpts = { cache: cache.nocache, chunker: utils.bf(30), codec: codec__namespace, hasher: sha2.sha256, compare };
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const byIdOpts = { cache: cache.nocache, chunker: utils.bf(30), codec: codec__namespace, hasher: sha2.sha256, compare: utils.simpleCompare };
function indexEntriesForChanges(changes, mapFn) {
const indexEntries = [];
changes.forEach(({ key: _id, value, del }) => {
if (del || !value)
return;
let mapCalled = false;
const mapReturn = mapFn({ _id, ...value }, (k, v) => {
mapCalled = true;
if (typeof k === 'undefined')
return;
indexEntries.push({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
key: [charwise.encode(k), _id],
value: v || null
});
});
if (!mapCalled && mapReturn) {
indexEntries.push({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
key: [charwise.encode(mapReturn), _id],
value: null
});
}
});
return indexEntries;
}
function makeProllyGetBlock(blocks) {
return async (address) => {
const block$1 = await blocks.get(address);
if (!block$1)
throw new Error(`Missing block ${address.toString()}`);
const { cid, bytes } = block$1;
return block.create({ cid, bytes, hasher: sha2.sha256, codec: codec__namespace });
};
}
async function bulkIndex(tblocks, inIndex, indexEntries, opts) {
if (!indexEntries.length)
return inIndex;
if (!inIndex.root) {
if (!inIndex.cid) {
let returnRootBlock = null;
let returnNode = null;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
for await (const node of await DbIndex__namespace.create({ get: makeProllyGetBlock(tblocks), list: indexEntries, ...opts })) {
const block = await node.block;
await tblocks.put(block.cid, block.bytes);
returnRootBlock = block;
returnNode = node;
}
if (!returnNode || !returnRootBlock)
throw new Error('failed to create index');
return { root: returnNode, cid: returnRootBlock.cid };
}
else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
inIndex.root = await DbIndex__namespace.load({ cid: inIndex.cid, get: makeProllyGetBlock(tblocks), ...opts });
}
}
const { root, blocks: newBlocks } = await inIndex.root.bulk(indexEntries);
if (root) {
for await (const block of newBlocks) {
await tblocks.put(block.cid, block.bytes);
}
return { root, cid: (await root.block).cid };
}
else {
return { root: null, cid: null };
}
}
async function loadIndex(tblocks, cid, opts) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
return await DbIndex__namespace.load({ cid, get: makeProllyGetBlock(tblocks), ...opts });
}
async function applyQuery(crdt, resp, query) {
if (query.descending) {
resp.result = resp.result.reverse();
}
if (query.limit) {
resp.result = resp.result.slice(0, query.limit);
}
if (query.includeDocs) {
resp.result = await Promise.all(resp.result.map(async (row) => {
const val = await crdt.get(row.id);
const doc = val ? { _id: row.id, ...val.doc } : null;
return { ...row, doc };
}));
}
return {
rows: resp.result.map(row => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
row.key = charwise.decode(row.key);
return row;
})
};
}
function encodeRange(range) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
return range.map(key => charwise.encode(key));
}
function encodeKey(key) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
return charwise.encode(key);
}
function index({ _crdt }, name, mapFn, meta) {
if (mapFn && meta)
throw new Error('cannot provide both mapFn and meta');
if (mapFn && mapFn.constructor.name !== 'Function')
throw new Error('mapFn must be a function');
if (_crdt.indexers.has(name)) {
const idx = _crdt.indexers.get(name);
idx.applyMapFn(name, mapFn, meta);
}
else {
const idx = new Index(_crdt, name, mapFn, meta);
_crdt.indexers.set(name, idx);
}
return _crdt.indexers.get(name);
}
class Index {
blocks;
crdt;
name = null;
mapFn = null;
mapFnString = '';
byKey = new IndexTree();
byId = new IndexTree();
indexHead = undefined;
includeDocsDefault = false;
initError = null;
ready;
constructor(crdt, name, mapFn, meta) {
this.blocks = crdt.indexBlocks;
this.crdt = crdt;
this.applyMapFn(name, mapFn, meta);
if (!(this.mapFnString || this.initError))
throw new Error('missing mapFnString');
this.ready = this.blocks.ready.then((header) => {
// @ts-ignore
if (header.head)
throw new Error('cannot have head in idx header');
if (header.indexes === undefined)
throw new Error('missing indexes in idx header');
for (const [name, idx] of Object.entries(header.indexes)) {
index({ _crdt: crdt }, name, undefined, idx);
}
});
}
applyMapFn(name, mapFn, meta) {
if (mapFn && meta)
throw new Error('cannot provide both mapFn and meta');
if (this.name && this.name !== name)
throw new Error('cannot change name');
this.name = name;
try {
if (meta) {
// hydrating from header
if (this.indexHead &&
this.indexHead.map(c => c.toString()).join() !== meta.head.map(c => c.toString()).join()) {
throw new Error('cannot apply meta to existing index');
}
this.byId.cid = meta.byId;
this.byKey.cid = meta.byKey;
this.indexHead = meta.head;
if (this.mapFnString) {
// we already initialized from application code
if (this.mapFnString !== meta.map)
throw new Error('cannot apply different mapFn meta');
}
else {
// we are first
this.mapFnString = meta.map;
}
}
else {
if (this.mapFn) {
// we already initialized from application code
if (mapFn) {
if (this.mapFn.toString() !== mapFn.toString())
throw new Error('cannot apply different mapFn app2');
}
}
else {
// application code is creating an index
if (!mapFn) {
mapFn = makeMapFnFromName(name);
}
if (this.mapFnString) {
// we already loaded from a header
if (this.mapFnString !== mapFn.toString())
throw new Error('cannot apply different mapFn app');
}
else {
// we are first
this.mapFnString = mapFn.toString();
}
this.mapFn = mapFn;
}
}
const matches = /=>\s*(.*)/.test(this.mapFnString);
this.includeDocsDefault = matches;
}
catch (e) {
this.initError = e;
}
}
async query(opts = {}) {
await this._updateIndex();
await this._hydrateIndex();
if (!this.byKey.root)
return await applyQuery(this.crdt, { result: [] }, opts);
if (this.includeDocsDefault && opts.includeDocs === undefined)
opts.includeDocs = true;
if (opts.range) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const { result, ...all } = await this.byKey.root.range(...encodeRange(opts.range));
return await applyQuery(this.crdt, { result, ...all }, opts);
}
if (opts.key) {
const encodedKey = encodeKey(opts.key);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
return await applyQuery(this.crdt, await this.byKey.root.get(encodedKey), opts);
}
if (opts.prefix) {
if (!Array.isArray(opts.prefix))
opts.prefix = [opts.prefix];
const start = [...opts.prefix, NaN];
const end = [...opts.prefix, Infinity];
const encodedR = encodeRange([start, end]);
return await applyQuery(this.crdt, await this.byKey.root.range(...encodedR), opts);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const { result, ...all } = await this.byKey.root.getAllEntries(); // funky return type
return await applyQuery(this.crdt, {
result: result.map(({ key: [k, id], value }) => ({ key: k, id, value })),
...all
}, opts);
}
async _hydrateIndex() {
if (this.byId.root && this.byKey.root)
return;
if (!this.byId.cid || !this.byKey.cid)
return;
this.byId.root = await loadIndex(this.blocks, this.byId.cid, byIdOpts);
this.byKey.root = await loadIndex(this.blocks, this.byKey.cid, byKeyOpts);
}
async _updateIndex() {
await this.ready;
if (this.initError)
throw this.initError;
if (!this.mapFn)
throw new Error('No map function defined');
const { result, head } = await this.crdt.changes(this.indexHead);
if (result.length === 0) {
this.indexHead = head;
return { byId: this.byId, byKey: this.byKey };
}
let staleKeyIndexEntries = [];
let removeIdIndexEntries = [];
if (this.byId.root) {
const removeIds = result.map(({ key }) => key);
const { result: oldChangeEntries } = await this.byId.root.getMany(removeIds);
staleKeyIndexEntries = oldChangeEntries.map(key => ({ key, del: true }));
removeIdIndexEntries = oldChangeEntries.map((key) => ({ key: key[1], del: true }));
}
const indexEntries = indexEntriesForChanges(result, this.mapFn); // use a getter to translate from string
const byIdIndexEntries = indexEntries.map(({ key }) => ({ key: key[1], value: key }));
const indexerMeta = new Map();
for (const [name, indexer] of this.crdt.indexers) {
if (indexer.indexHead) {
indexerMeta.set(name, {
byId: indexer.byId.cid,
byKey: indexer.byKey.cid,
head: indexer.indexHead,
map: indexer.mapFnString,
name: indexer.name
});
}
}
return await this.blocks.transaction(async (tblocks) => {
this.byId = await bulkIndex(tblocks, this.byId, removeIdIndexEntries.concat(byIdIndexEntries), byIdOpts);
this.byKey = await bulkIndex(tblocks, this.byKey, staleKeyIndexEntries.concat(indexEntries), byKeyOpts);
this.indexHead = head;
return { byId: this.byId.cid, byKey: this.byKey.cid, head, map: this.mapFnString, name: this.name };
}, indexerMeta);
}
}
function makeMapFnFromName(name) {
return (doc) => {
if (doc[name])
return doc[name];
};
}
exports.Database = Database;
exports.Index = Index;
exports.database = database;
exports.index = index;
//# sourceMappingURL=fireproof.cjs.map