UNPKG

hyper-readings

Version:

a tool for making and sharing p2p distributed reading lists

196 lines (178 loc) 6.13 kB
import nanoiterator from 'nanoiterator' import toStream from 'nanoiterator/to-stream' // TODO: add smart caching to avoid a lot of unnecessary iteration const ContainerBehavior = {} ContainerBehavior.firstItem = function () { return this.get('co:firstItem') } ContainerBehavior.itemContent = function () { return this.get('co:itemContent') } ContainerBehavior.contains = function () { return this.all('po:contains') } ContainerBehavior.next = function () { return this.get('co:nextItem') } ContainerBehavior.iterator = function (opts) { var next = null var returnContent = !(opts && opts.listItems) var iterator = nanoiterator({ next: (cb) => { var promised = (!next) ? this.firstItem() : next.next() promised .then(node => { next = node return returnContent ? next && next.itemContent() : next }) .then(node => cb(null, node)) .catch(cb) } }) return iterator } ContainerBehavior.stream = function (opts) { return toStream(this.iterator(opts)) } ContainerBehavior.iterate = async function (fn, opts) { // this could be replace with stream as so, but is much slower // return new Promise((resolve, reject) => { // const stream = this.stream(opts) // stream.on('data', fn) // stream.on('end', resolve) // stream.on('error', reject) // }) var next = await this.firstItem() while (next) { // these should probably be called in parallel to speed things up if (fn) { if (opts && opts.listItems) { await fn(next) } else { var contents = await next.itemContent() await fn(contents) } } next = await next.next() } } ContainerBehavior.lastItem = async function () { // first check if a last item is explicitly set var last = await this.get('co:lastItem') if (last) return last // else iterate over next items until there are none left await this.iterate((node) => { last = node }, { listItems: true }) return last } /* TODO: implement index insertion */ ContainerBehavior.insertNode = async function (node, index) { // validate newNode // 1. inserts`< node co:contains newNode >` await this.add('po:contains', node) // 2. make newItem `< newItem co:hasContent newNode >` var newItem = await this.hr.createNode('co:ListItem') await newItem.set('co:itemContent', node) // 3. if `< node co:firstItem ? >` does not exist, insert`< node co:firstItem newItem >` var alreadyHasFirst = await this.has('co:firstItem') // console.log('alreadyHasFirst', alreadyHasFirst) if (!alreadyHasFirst) { await this.set('co:firstItem', newItem) } else { // else find lastItem of node, or not at index-1, and insert `<lastItemNode co:nextItem > newItem >` var lastItem = await this.lastItem() // console.log('lastItem', lastItem && lastItem.name) if (lastItem) { await lastItem.set('co:nextItem', newItem) } } // 4. return existing node return this } ContainerBehavior.updateList = async function (nodeIds) { // this is one of many possible implementations. // it could be smarter - determine programmatically whether // procedurally updating itemContent is more effective // than adjusting co:firstItem and co:nextItem pointers. // This probably would have to factor in if the nodeIds are new // or if they are just a reordering of existing content. let count = 0 // 1. iterate over existing list // TODO: allow the iterator to stop early await this.iterate(async (child) => { // 1.2 check if current child is within range of array // 1.2.1 remove all nodes that are greater than nodeIds length if (count > nodeIds.length) return if (count === nodeIds.length) { // destroying this list node cascades down all the remaining nodes in the list // and in effect deletes all remaining nodes and their content. // We add this.name to .destroy's ignore list, so that the connection to // this.name -> po:contains does not prevent the removal of itemContent. await child.parent.destroy([this.name]) count += 1 return } const newNodeName = nodeIds[count] // 1.3 check if content is the same // if so do nothing if (child.name === newNodeName) { count += 1 return } // if not then we want to update content pointer await child.parent.setContent({ name: newNodeName }) // and remove the current node from container if (!nodeIds.includes(child.name)) { await this.remove('po:contains', child) } await this.add('po:contains', { name: newNodeName }) count += 1 }) // 2. insert remaining nodes while (count < nodeIds.length) { await this.insertNode({ name: nodeIds[count] }) count += 1 } } ContainerBehavior.at = async function (index, opts) { let count = 0 const iterator = this.iterator(opts) return new Promise((resolve, reject) => { iterator.next(onNext) function onNext (err, val) { if (err) { iterator.destroy() return reject(err) } if (val === null || count > index) { iterator.destroy() return resolve(null) } if (count === index) { iterator.destroy(() => resolve(val)) } count++ iterator.next(onNext) } }) } ContainerBehavior.removeNodeAt = async function (index) { // const node = await this.at(index, { listItems: true }) const node = await this.at(index) if (!node) return return node.destroy() } ContainerBehavior.removeNodesFrom = async function (index) { const node = await this.at(index, { listItems: true }) if (!node) return return node.destroy() } ContainerBehavior.removeNode = async function (node) { // 1. find node or node at index // 2. get nodeToDeletes nextItem // 3. delete key `< node po:contains nodeToDelete >` // 4. file all references `< ? co:nextItem nodeToDelete >` // 5. update all to `< ? co:nextItem nextItemOfNoteToDelete >` // 6. if `< node co:firstItem nodeToDelete >` exist // update it to `< node co:firstItem nextItemOfNoteToDelete >` } export default ContainerBehavior