From faa3c48c99f7a4c47b53fd59ddeb29343584f587 Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Thu, 2 Nov 2017 08:38:13 -0500 Subject: [PATCH] Redo LiveArray to watch all returned IDs for removal from the selector. There is still a big TODO here in that the globalWatcher mechanism assumes one database. Need to re-structure this whole thing around a db instance. --- packages/gallery/src/interface/album.js | 6 +- packages/gallery/src/utils/comparators.js | 9 +++ packages/gallery/src/utils/conversion.js | 4 + packages/gallery/src/utils/livearray.js | 95 +++++++++++++++++++---- packages/gallery/src/utils/set.js | 14 ++++ 5 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 packages/gallery/src/utils/comparators.js create mode 100644 packages/gallery/src/utils/set.js diff --git a/packages/gallery/src/interface/album.js b/packages/gallery/src/interface/album.js index e54e5c8..a2eaf74 100644 --- a/packages/gallery/src/interface/album.js +++ b/packages/gallery/src/interface/album.js @@ -6,7 +6,6 @@ import { LiveArray } from '../utils/livearray.js'; export function AlbumView(vm, model) { const { remove, db } = model; let data = null; - let currentMemberLen = -1; let title = null; function removeImageFromAlbum(id, rev) { @@ -15,14 +14,13 @@ export function AlbumView(vm, model) { return function(vm, model, key, opts) { const { doc, remove } = model; - const { props, members } = doc; + const { props } = doc; - if (title !== props.title || currentMemberLen !== members.length) { + if (title !== props.title) { if (data) { data.cleanup(); } title = props.title; - currentMemberLen = members.length; const SELECTOR = Object.assign( { [`tags.${title}`]: { $eq: true } diff --git a/packages/gallery/src/utils/comparators.js b/packages/gallery/src/utils/comparators.js new file mode 100644 index 0000000..a8336a7 --- /dev/null +++ b/packages/gallery/src/utils/comparators.js @@ -0,0 +1,9 @@ +import { extractID } from './conversion.js'; +import { equals } from './set.js'; + +export function pouchDocArrayComparator(a, b) { + const aIDs = a.map(extractID); + const bIDs = b.map(extractID); + + return equals(new Set(...aIDs), new Set(...bIDs)); +} diff --git a/packages/gallery/src/utils/conversion.js b/packages/gallery/src/utils/conversion.js index 87e8f9f..361eb12 100644 --- a/packages/gallery/src/utils/conversion.js +++ b/packages/gallery/src/utils/conversion.js @@ -35,3 +35,7 @@ export function deepAssign(to, ...rest) { } return to; } + +export function extractID(doc) { + return doc._id; +} diff --git a/packages/gallery/src/utils/livearray.js b/packages/gallery/src/utils/livearray.js index a1176f4..daffbef 100644 --- a/packages/gallery/src/utils/livearray.js +++ b/packages/gallery/src/utils/livearray.js @@ -1,35 +1,96 @@ import { observable, computed } from 'frptools'; -import { group, groupEnd, log } from '../services/console.js'; -import { Watcher } from './watcher.js'; +import { matchesSelector } from 'pouchdb-selector-core'; +import { getDatabase } from '../services/db.js'; +import { Watcher } from './watcher.js'; +import { pouchDocArrayComparator } from './comparators.js'; +import { difference } from './set.js'; + +// The point of the globalWatcher mechanism is that PouchDB.changes doesn't register when a document changes in such a way that removes it from the selector specifications. +// For Example: a selector looks for images with a specific tag. If a change removes that tag, the changes API will not register a change event. globalWatcher watches the document IDs for exactly this type of change and triggers the LiveArray to refresh. + +const globalWatcher = Watcher(getDatabase(), {}, true); +const watchingIDs = new Map(); +let globalWatcherSubscription = null; + +function checkDocs(id, deleted, doc) { + // Is the changed doc one that we're watching? + if (watchingIDs.has(id)) { + const refresherMap = watchingIDs.get(id); + // if the doc doesn't match a watching selector, then refresh its LA + [...refresherMap.keys()] + .filter(s => !matchesSelector(doc, s)) + .forEach(s => refresherMap.get(s)()); + return; + } +} + +function addID(id, selector, refresher) { + if (!watchingIDs.has(id)) { + watchingIDs.set(id, new Map()); + } + watchingIDs.get(id).set(selector, refresher); + if (globalWatcherSubscription === null) { + globalWatcherSubscription = globalWatcher(checkDocs); + } +} + +function removeID(id, selector) { + if (watchingIDs.has(id)) { + const idSet = watchingIDs.get(id); + idSet.delete(selector); + if (idSet.size === 0) { + watchingIDs.delete(selector); + if (watchingIDs.size === 0) { + globalWatcherSubscription(); + globalWatcherSubscription = null; + } + } + } +} + +// LiveArray is a subscribable property function that always returns the db results that match the provided selector and calls subscribers when the results change. export function LiveArray(db, selector, watcher) { const _watcher = watcher || Watcher(db, selector); - const data = observable({ docs: [] }); - const docs = computed(r => r.docs, [data]); let changeSub = null; - const accessor = docs; - accessor.ready = observable(false); - accessor.cleanup = () => { - docs.detach(); + const ready = observable(false); + const data = observable({ docs: [] }); + const docs = computed(r => r.docs, [data], pouchDocArrayComparator); + + const idSet = () => docs().reduce((acc, d) => acc.add(d._id), new Set()); + const addThisID = id => addID(id, selector, refresh); + const removeThisID = id => removeID(id, selector); + + const cleanup = () => { + docs.unsubscribeAll(); + ready.unsubscribeAll(); if (changeSub) { changeSub(); + changeSub = null; } - accessor.ready.unsubscribeAll(); + [...idSet()].forEach(removeThisID); data({ docs: [] }); }; - async function refresh() { - group('LiveArray Refreshing'); - log(selector); + const refresh = async function refresh() { + const oldIdSet = idSet(); data(await db.find({ selector })); - log(data()); - groupEnd('LiveArray Refreshing'); - } + const currentIDSet = idSet(); + // Removes IDs not in the new set + [...difference(oldIdSet, currentIDSet)].forEach(removeThisID); + // Add IDs in the new set + [...difference(currentIDSet, oldIdSet)].forEach(addThisID); + }; + + docs.ready = ready; + docs.cleanup = cleanup; + docs.selector = selector; + docs.db = db; refresh().then(() => { changeSub = _watcher(refresh); - accessor.ready(true); + ready(true); }); - return accessor; + return docs; } diff --git a/packages/gallery/src/utils/set.js b/packages/gallery/src/utils/set.js new file mode 100644 index 0000000..f8e6887 --- /dev/null +++ b/packages/gallery/src/utils/set.js @@ -0,0 +1,14 @@ +export function equals(a, b) { + return ( + [...a].reduce((acc, d) => acc && b.has(d), true) && + [...b].reduce((acc, d) => acc && a.has(d), true) + ); +} + +export function intersection(a, b) { + return new Set([...a].filter(x => b.has(x))); +} + +export function difference(a, b) { + return new Set([...a].filter(x => !b.has(x))); +}