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:
parent
e37679c2c9
commit
faa3c48c99
@ -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 }
|
||||||
|
|||||||
9
packages/gallery/src/utils/comparators.js
Normal file
9
packages/gallery/src/utils/comparators.js
Normal 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));
|
||||||
|
}
|
||||||
@ -35,3 +35,7 @@ export function deepAssign(to, ...rest) {
|
|||||||
}
|
}
|
||||||
return to;
|
return to;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractID(doc) {
|
||||||
|
return doc._id;
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
14
packages/gallery/src/utils/set.js
Normal file
14
packages/gallery/src/utils/set.js
Normal 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)));
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user