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.
This commit is contained in:
Timothy Farrell 2017-11-02 08:38:13 -05:00
parent e37679c2c9
commit faa3c48c99
5 changed files with 107 additions and 21 deletions

View File

@ -6,7 +6,6 @@ import { LiveArray } from '../utils/livearray.js';
export function AlbumView(vm, model) { export function AlbumView(vm, model) {
const { remove, db } = model; const { remove, db } = model;
let data = null; let data = null;
let currentMemberLen = -1;
let title = null; let title = null;
function removeImageFromAlbum(id, rev) { function removeImageFromAlbum(id, rev) {
@ -15,14 +14,13 @@ export function AlbumView(vm, model) {
return function(vm, model, key, opts) { return function(vm, model, key, opts) {
const { doc, remove } = model; const { doc, remove } = model;
const { props, members } = doc; const { props } = doc;
if (title !== props.title || currentMemberLen !== members.length) { if (title !== props.title) {
if (data) { if (data) {
data.cleanup(); data.cleanup();
} }
title = props.title; title = props.title;
currentMemberLen = members.length;
const SELECTOR = Object.assign( const SELECTOR = Object.assign(
{ {
[`tags.${title}`]: { $eq: true } [`tags.${title}`]: { $eq: true }

View File

@ -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));
}

View File

@ -35,3 +35,7 @@ export function deepAssign(to, ...rest) {
} }
return to; return to;
} }
export function extractID(doc) {
return doc._id;
}

View File

@ -1,35 +1,96 @@
import { observable, computed } from 'frptools'; import { observable, computed } from 'frptools';
import { group, groupEnd, log } from '../services/console.js'; import { matchesSelector } from 'pouchdb-selector-core';
import { Watcher } from './watcher.js';
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) { export function LiveArray(db, selector, watcher) {
const _watcher = watcher || Watcher(db, selector); const _watcher = watcher || Watcher(db, selector);
const data = observable({ docs: [] });
const docs = computed(r => r.docs, [data]);
let changeSub = null; let changeSub = null;
const accessor = docs; const ready = observable(false);
accessor.ready = observable(false); const data = observable({ docs: [] });
accessor.cleanup = () => { const docs = computed(r => r.docs, [data], pouchDocArrayComparator);
docs.detach();
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) { if (changeSub) {
changeSub(); changeSub();
changeSub = null;
} }
accessor.ready.unsubscribeAll(); [...idSet()].forEach(removeThisID);
data({ docs: [] }); data({ docs: [] });
}; };
async function refresh() { const refresh = async function refresh() {
group('LiveArray Refreshing'); const oldIdSet = idSet();
log(selector);
data(await db.find({ selector })); data(await db.find({ selector }));
log(data()); const currentIDSet = idSet();
groupEnd('LiveArray Refreshing'); // 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(() => { refresh().then(() => {
changeSub = _watcher(refresh); changeSub = _watcher(refresh);
accessor.ready(true); ready(true);
}); });
return accessor; return docs;
} }

View File

@ -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)));
}