So much in here

- Image.upload calls File.upload
- doc deletes are PUT instead of deleted
- generate thumbnails is called off of processImportables
- got rid of stupid context loaders
- added Type.getOrCreate()
- fix massively broken comparators
- get rid of global watcher (unnecessary with new delete method)

Still broken
- indexes (albums)
- files not deleted along with images
- some wonky jpgs
This commit is contained in:
Timothy Farrell 2017-11-18 00:42:55 -06:00
parent d14bfdeee2
commit 278fc68831
12 changed files with 231 additions and 344 deletions

View File

@ -2,17 +2,15 @@
import { createView } from 'domvm/dist/micro/domvm.micro.js'; import { createView } from 'domvm/dist/micro/domvm.micro.js';
import * as styles from './app.css'; import * as styles from './app.css';
import generateThumbnails from './contextLoaders/generateThumbnails.js';
import { GalleryView } from './interface/gallery.js'; import { GalleryView } from './interface/gallery.js';
import { router } from './services/router.js'; import { router } from './services/router.js';
import { getDatabase } from './services/db.js';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
EventEmitter.defaultMaxListeners = 1000; // https://github.com/pouchdb/pouchdb/issues/6123 EventEmitter.defaultMaxListeners = 1000; // https://github.com/pouchdb/pouchdb/issues/6123
// Attach our root view to the DOM // Attach our root view to the DOM
createView(GalleryView, { db: getDatabase() }).mount(document.querySelector('#app')); createView(GalleryView, {}).mount(document.querySelector('#app'));
// Start the router // Start the router
router.start('home'); router.start('home');

View File

@ -1,7 +1,6 @@
import pica from 'pica/dist/pica'; import pica from 'pica/dist/pica';
import { generateAttachmentUrl, getDatabase } from '../services/db.js'; import { FileType } from '../data/file.js';
import { find, update, addAttachment } from '../data/image.js';
export function maxLinearSize(width, height, max) { export function maxLinearSize(width, height, max) {
const ratio = width / height; const ratio = width / height;
@ -45,28 +44,21 @@ async function resizeImage(imageBlob, mimetype, width, height) {
}); });
} }
export async function generateThumbnailForImage(id) { export async function generateThumbnailForImage(doc) {
const results = await find([id], { attachments: true, binary: true }); if (doc.sizes.thumbnail) {
const doc = results.rows[0].doc;
if (doc.attachmentUrls.thumbnail && doc._attachments.thumbnail) {
return; return;
} }
const attachment = doc._attachments.image; const attachment = await FileType.getFromURL(doc.sizes.full);
const mimetype = attachment.content_type; const mimetype = attachment.content_type || attachment.type;
const { width, height } = maxLinearSize(doc.width, doc.height, 320); const { width, height } = maxLinearSize(doc.width, doc.height, 320);
const resizedBlob = await resizeImage(attachment.data, mimetype, width, height); const resizedBlob = await resizeImage(attachment, mimetype, width, height);
const url = generateAttachmentUrl(getDatabase().name, id, 'thumbnail');
await addAttachment(doc, 'thumbnail', resizedBlob); const thumbfile = await FileType.upload(resizedBlob);
await update(doc._id, {
attachmentUrls: { await doc.update({
thumbnail: url sizes: {
thumbnail: FileType.getURL(thumbfile)
} }
}); });
return resizedBlob;
} }
export const invoke = generateThumbnailForImage;

View File

@ -1,11 +0,0 @@
import * as image from '../data/image.js';
// Watch for new images, generate thumbnails if they need them.
image.watcher(async function generateThumbnails(id, deleted, doc) {
if (deleted || (doc.attachmentUrls.thumbnail && doc._attachments.thumbnail)) {
return;
}
const module = await import('../context/generateThumbnails');
module.invoke(id);
});

View File

@ -20,30 +20,33 @@ export const FileType = PouchDB.registerType({
// } // }
// }, // },
methods: { methods: {
upload: async function(fileListOrEvent) { getURL: doc => `/${FileType.prefix}/${doc._id}/data`,
const files = Array.from( getFromURL: async path => {
fileListOrEvent instanceof Event ? fileListOrEvent.currentTarget.files : fileListOrEvent if (path.endsWith('/')) {
); path = path.substr(0, path.length - 1);
return files.map(async f => { }
const digest = await sha256(await blobToArrayBuffer(f)); const [_, db, id, attname] = path.split('/');
const file = FileType.new({ const doc = await FileType.find(id);
name: f.name, return await doc.getAttachment(attname);
mimetype: f.type, },
size: f.size, upload: async function(blob) {
modifiedDate: new Date(f.lastModified), const digest = await sha256(await blobToArrayBuffer(blob));
addDate: new Date(), const lastModified = blob.lastModified ? new Date(blob.lastModified) : new Date();
return await FileType.getOrCreate({
name: blob.name,
mimetype: blob.type,
size: blob.size,
lastModified: lastModified.toISOString(),
addDate: new Date().toISOString(),
digest, digest,
tags: {}, tags: {},
_attachments: { _attachments: {
data: { data: {
content_type: f.type, content_type: blob.type,
data: f data: blob
} }
} }
}); });
await file.save();
return file;
});
} }
} }
}); });

View File

@ -1,150 +1,104 @@
import { getDatabase, generateAttachmentUrl } from '../services/db.js'; import { PouchDB, TYPES as t } from '../services/db.js';
import { log, error } from '../services/console.js'; import { blobToArrayBuffer } from '../utils/conversion.js';
import { sha256 } from '../utils/crypto.js'; import { backgroundTask } from '../utils/event.js';
import { blobToArrayBuffer, deepAssign } from '../utils/conversion.js';
import { Event, backgroundTask } from '../utils/event.js';
import { Watcher } from '../utils/watcher.js';
import { FileType } from './file.js'; import { FileType } from './file.js';
const db = getDatabase(); export const ImageType = PouchDB.registerType({
const PROCESS_PREFIX = 'importing'; name: 'Image',
const PREFIX = 'image'; getUniqueID: doc => doc.digest.substr(0, 16),
export const SELECTOR = { getSequence: doc => new Date(doc.originalDate).getTime(),
_id: { // schema: {
$gt: `${PREFIX}_`, // originalDate: t.REQUIRED_DATE,
$lt: `${PREFIX}_\ufff0` // digest: t.REQUIRED_STRING,
// width: t.INTEGER,
// height: t.INTEGER,
// sizes: {
// type: 'object',
// properties: {
// full: t.REQUIRED_STRING,
// thumbnail: t.STRING,
// }
// },
// orientation: t.INTEGER,
// make: t.STRING,
// model: t.STRING,
// flash: t.BOOLEAN,
// iso: t.INTEGER,
// gps: {
// type: 'object',
// properties: {
// latitude: t.NUMBER,
// longitude: t.NUMBER,
// altitude: t.NUMBER,
// heading: t.NUMBER,
// }
// },
// tags: {
// type: "object",
// additionalProperties: t.BOOLEAN
// }
// },
methods: {
upload: async function(blob) {
const f = await FileType.upload(blob, false);
return await ImageType.getOrCreate({
digest: f.digest,
originalDate: f.lastModified,
importing: true,
width: 0,
height: 0,
sizes: {
full: FileType.getURL(f)
} }
}; });
const IMPORT_SELECTOR = { },
_id: { processImportables: backgroundTask(async function _processImportables(importables) {
$gt: `${PROCESS_PREFIX}_`,
$lt: `${PROCESS_PREFIX}_\ufff0`
}
};
// Events
export const imported = new Event('Image.imported');
export const removed = new Event('Image.removed');
// Watchers
export const watcher = Watcher(db, SELECTOR, true);
export const importWatcher = Watcher(db, IMPORT_SELECTOR);
// Methods
const getId = id => (id.startsWith(PREFIX) ? id : `${PREFIX}_${id}`);
export async function find(keys, options = {}) {
let opts = { include_docs: true };
if (Array.isArray(keys)) {
Object.assign(opts, options);
opts.keys = keys.map(getId);
} else {
Object.assign(opts, keys);
opts.startkey = `${PREFIX}_0`;
opts.endkey = `${PREFIX}_\ufff0`;
}
return await db.allDocs(opts);
}
export async function getAttachment(id, attName) {
return await db.getAttachment(id, attName);
}
export async function remove(ids) {
const docs = await find(Array.isArray(ids) ? ids : [ids]);
const foundDocs = docs.rows.filter(r => !r.error);
const result = await db.bulkDocs(foundDocs.map(r => Object.assign(r.doc, { _deleted: true })));
foundDocs.filter((_, i) => result[i].ok).map(r => removed.fire(r.doc));
return result.reduce((a, r) => a && r.ok, true);
}
export async function update(id, properties) {
const results = await find([id]);
const doc = results.rows[0].doc;
deepAssign(doc, properties);
await db.put(doc);
return doc;
}
export async function addAttachment(doc, key, blob) {
return db.putAttachment(doc._id, key, doc._rev, blob, blob.type);
}
// Internal Functions
const processImportables = backgroundTask(async function _processImportables(importables) {
if (!importables.length) { if (!importables.length) {
return; return;
} }
const file = importables[0]; const image = importables[0];
const { _id, _rev } = file; const { _id, _rev } = image;
const imageData = await file.getAttachment('data'); const imageData = await FileType.getFromURL(image.sizes.full);
const ExifParser = await import('exif-parser'); const ExifParser = await import('exif-parser');
const buffer = await blobToArrayBuffer(imageData); const buffer = await blobToArrayBuffer(imageData);
// Check if this image already exists
// TODO - Create an image.digest index
const digestQuery = await db.find({
selector: { digest: file.digest },
fields: ['_id'],
limit: 1
});
if (digestQuery.docs.length) {
imported.fire(digestQuery.docs[0]._id, _id, false);
} else {
const exifData = ExifParser.create(buffer).parse(); const exifData = ExifParser.create(buffer).parse();
const { tags, imageSize } = exifData; const { tags, imageSize } = exifData;
const { width, height } = imageSize;
const originalDate = new Date( const originalDate = new Date(
tags.DateTimeOriginal ? new Date(tags.DateTimeOriginal * 1000).toISOString() : file.modifiedDate tags.DateTimeOriginal
); ? new Date(tags.DateTimeOriginal * 1000).toISOString()
const id = `${PREFIX}_${originalDate.getTime().toString(36)}_${file.digest.substr(0, 6)}`; : image.originalDate
).toISOString();
const newDoc = Object.assign( const img = await ImageType.getOrCreate({
{}, originalDate,
{ width,
_id: id, height,
originalDate: originalDate,
orientation: tags.Orientation, orientation: tags.Orientation,
digest: file.digest, digest: image.digest,
make: tags.Make, make: tags.Make,
model: tags.Model, model: tags.Model,
flash: !!tags.Flash, flash: !!tags.Flash,
ISO: tags.ISO, iso: tags.ISO,
fileId: file._id, sizes: image.sizes,
url: generateAttachmentUrl('file', file._id, 'data'),
gps: { gps: {
latitude: tags.GPSLatitude, latitude: tags.GPSLatitude,
longitude: tags.GPSLongitude, longitude: tags.GPSLongitude,
altitude: tags.GPSAltitude, altitude: tags.GPSAltitude,
heading: tags.GPSImgDirection heading: tags.GPSImgDirection
} }
},
imageSize // width & height
);
delete newDoc._rev; // assigned from doc but not desired.
try {
await db.put(newDoc);
file.update({ tags: { galleryImage: false } });
imported.fire(id, _id, true);
} catch (e) {
error(`Error processing Image ${id}`, e);
}
}
}, false);
FileType.find(
{
$and: [{ mimetype: { $in: ['image/jpeg'] } }, { $not: { ['tags.galleryImage']: false } }]
},
true
).then(fw => {
fw.subscribe((...props) => {
processImportables(...props);
}); });
image.delete();
const module = await import('../context/generateThumbnails');
await module.generateThumbnailForImage(img);
}, false)
}
}); });
ImageType.find({ importing: true }, true).then(fw => fw.subscribe(ImageType.processImportables));

View File

@ -1,37 +1,39 @@
import { defineElement as el } from 'domvm'; import { defineElement as el } from 'domvm';
import { prop, computed, bundle } from 'frptools'; import { prop, computed, bundle } from 'frptools';
import * as imageType from '../data/image.js'; import { ImageType } from '../data/image.js';
import { FileType } from '../data/file.js';
import { pouchDocComparator } from '../utils/comparators.js';
export function AttachmentImageView(vm, params) { export function AttachmentImageView(vm, doc) {
const model = bundle({ const model = bundle({
doc: prop(params.src), _id: prop(doc._id),
attachmentKey: prop(params.attachmentKey || 'image') _rev: prop(doc._rev),
sizes: prop(doc.sizes)
}); });
const blobURL = prop(''); const blobURL = prop('');
const imageURL = computed((sizes, bURL) => bURL || sizes.thumbnail || sizes.full, [
const imageID = computed(doc => doc._id, [model.doc]); model.sizes,
const imageURL = computed((doc, key, bURL) => bURL || doc.attachmentUrls[key], [
model.doc,
model.attachmentKey,
blobURL blobURL
]); ]);
const imageSignature = computed((id, key) => id + ' ' + key, [imageID, model.attachmentKey]); const _key = computed((id, rev) => id + rev, [model._id, model._rev]);
async function loadImageFromBlob() { async function loadImageFromBlob() {
const id = imageID(); const options = ['thumbnail', 'full'].filter(o => model.sizes().hasOwnProperty(o));
const key = model.attachmentKey();
for (let attempt of options) {
try { try {
const data = await imageType.getAttachment(id, key); const data = await FileType.getFromURL(model.sizes()[attempt]);
if (blobURL()) { if (blobURL()) {
URL.revokeObjectURL(blobURL()); URL.revokeObjectURL(blobURL());
} }
blobURL(URL.createObjectURL(data)); blobURL(URL.createObjectURL(data));
return;
} catch (err) { } catch (err) {
// Probably hasn't created the thumbnail yet. continue;
console.log("Probably hasn't created the thumbnail yet.", err); }
} }
} }
@ -42,18 +44,17 @@ export function AttachmentImageView(vm, params) {
const redrawOff = imageURL.subscribe(() => vm.redraw()); const redrawOff = imageURL.subscribe(() => vm.redraw());
return function render(vm, params) { return function render(vm, doc) {
const imgSig = imageSignature(); if (!pouchDocComparator(doc, { _id: model._id(), _rev: model._rev() })) {
model(params);
if (imgSig !== imageSignature()) {
URL.revokeObjectURL(blobURL()); URL.revokeObjectURL(blobURL());
blobURL(''); blobURL('');
} }
model(doc);
return el('img', { return el('img', {
src: imageURL(), src: imageURL(),
onerror: loadImageFromBlob, onerror: loadImageFromBlob,
_key: imageSignature(), _key: _key(),
_hooks: { _hooks: {
didRemove: cleanup didRemove: cleanup
} }

View File

@ -1,39 +1,51 @@
import * as image from '../data/image.js';
import * as index from '../data/indexType.js';
import { FileType } from '../data/file.js';
import * as imageTag from '../context/manageImageTags.js';
import { defineView as vw } from 'domvm'; import { defineView as vw } from 'domvm';
import { ImageType } from '../data/image.js';
// import * as index from '../data/indexType.js';
// import * as imageTag from '../context/manageImageTags.js';
import { ThumbnailView } from './thumbnail.js'; import { ThumbnailView } from './thumbnail.js';
import { AlbumView } from './album.js'; import { AlbumView } from './album.js';
import { router, routeChanged } from '../services/router.js'; import { router, routeChanged } from '../services/router.js';
import { styled, el } from '../services/style.js'; import { styled, el } from '../services/style.js';
import { LiveArray } from '../utils/livearray.js';
import { Watcher } from '../utils/watcher.js';
export function GalleryView(vm, model) { export function GalleryView(vm, model) {
const { db } = model; const { db } = model;
const NAV_OPTIONS = { const NAV_OPTIONS = {
images: { images: {
selector: image.SELECTOR, data: ImageType.find(
title: 'Images' {
importing: { $exists: false }
}, },
albums: { true
selector: index.SELECTOR, ),
title: 'Albums' title: 'Images'
} }
// albums: {
// selector: index.SELECTOR,
// title: 'Albums'
// }
}; };
let data = null; let data = null;
let laCleanup = null;
let title = ''; let title = '';
function uploadImages(evt) {
Array.from(evt.currentTarget.files).forEach(ImageType.upload);
}
routeChanged.subscribe(function onRouteChange(router, route) { routeChanged.subscribe(function onRouteChange(router, route) {
if (data) { if (laCleanup) {
data.cleanup(); laCleanup();
} }
const o = NAV_OPTIONS[route.name]; const o = NAV_OPTIONS[route.name];
data = LiveArray(db, o.selector);
title = o.title; title = o.title;
data.subscribe(() => vm.redraw()); return o.data.then(la => {
data = la;
laCleanup = data.subscribe(() => {
vm.redraw();
});
data.ready.subscribe(() => vm.redraw);
});
}); });
return function(vm, model, key, opts) { return function(vm, model, key, opts) {
@ -44,7 +56,7 @@ export function GalleryView(vm, model) {
type: 'file', type: 'file',
multiple: true, multiple: true,
accept: 'image/jpeg', accept: 'image/jpeg',
onchange: FileType.upload onchange: uploadImages
}) })
]), ]),
...(!data || !data.ready() ...(!data || !data.ready()
@ -60,9 +72,9 @@ export function GalleryView(vm, model) {
{ {
doc: i, doc: i,
showTags: true, showTags: true,
addTag: imageTag.add, // addTag: imageTag.add,
remove: image.remove, remove: i.delete.bind(i)
removeTag: imageTag.remove // removeTag: imageTag.remove
}, },
i._id + i._rev i._id + i._rev
); );
@ -72,9 +84,9 @@ export function GalleryView(vm, model) {
AlbumView, AlbumView,
{ {
doc: a, doc: a,
db, db
addTag: imageTag.add, // addTag: imageTag.add,
remove: imageTag.remove // remove: imageTag.remove
}, },
a._id + a._rev a._id + a._rev
); );

View File

@ -1,7 +1,7 @@
import { defineView as vw, defineElement as el } from 'domvm'; import { defineView as vw, defineElement as el } from 'domvm';
import { prop, computed } from 'frptools'; import { prop, computed } from 'frptools';
import { isObject } from '../utils/comparators.js';
import * as image from '../data/image.js';
import { AttachmentImageView } from './attachmentImage.js'; import { AttachmentImageView } from './attachmentImage.js';
export function ThumbnailView(vm, model) { export function ThumbnailView(vm, model) {
@ -15,7 +15,10 @@ export function ThumbnailView(vm, model) {
const { doc, showTags, remove, removeTag } = model; const { doc, showTags, remove, removeTag } = model;
const { _id: id, _rev: rev, tags } = doc; const { _id: id, _rev: rev, tags } = doc;
const _showTags = showTags !== undefined ? showTags : true; const _showTags = showTags !== undefined ? showTags : true;
const filteredTags = _showTags ? Object.entries(doc.tags).filter(([_, visible]) => visible) : []; const filteredTags =
_showTags && isObject(doc.tags)
? Object.entries(doc.tags).filter(([_, visible]) => visible)
: [];
return el('div', { _key: id }, [ return el('div', { _key: id }, [
el( el(
@ -24,10 +27,7 @@ export function ThumbnailView(vm, model) {
onclick: { img: [remove, id, rev] } onclick: { img: [remove, id, rev] }
}, },
[ [
vw(AttachmentImageView, { vw(AttachmentImageView, doc, doc._id + doc._rev),
src: doc,
attachmentKey: 'thumbnail'
}),
filteredTags.length filteredTags.length
? el( ? el(
'figcaption', 'figcaption',

View File

@ -105,44 +105,46 @@ export function PouchORM(PouchDB) {
return docOrResultSet; return docOrResultSet;
} }
async function find(idOrQuery, live = false, raw = false) { async function find(idOrQuery, live = false) {
let results = []; let results = [];
if (typeof idOrQuery === 'undefined') {
results = await db.find({ if (typeof idOrQuery === 'string') {
selector: { results = await db.get(idOrQuery);
_id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } } else {
} const selector = Object.assign(
}); { _deleted: { exists: false } },
} else if (typeof idOrQuery === 'string') { isObject(idOrQuery) ? idOrQuery : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } }
results = await db.find({ );
selector: { _id: idOrQuery }
});
} else if (isObject(idOrQuery)) {
if (live) { if (live) {
return LiveArray(db, idOrQuery, instantiate); return LiveArray(db, idOrQuery, instantiate);
} }
results = await db.find({ results = await db.find({ selector: idOrQuery });
selector: idOrQuery
});
} }
return raw ? results : instantiate(results); return instantiate(results);
} }
async function _delete() { async function _delete() {
try { return await this.update({ _deleted: true });
const { ok } = await db.remove(this);
this.update({ _id: undefined, _rev: undefined }, false);
return ok;
} catch (e) {
return false;
}
} }
function _new(props, save = false) { async function _new(props, save = true) {
const doc = instantiate(populateId(props)); const doc = instantiate(populateId(props));
if (save) { if (save) {
doc.save(); await doc.save();
}
return doc;
}
async function getOrCreate(props) {
let doc = await _new(props, false);
try {
await doc.save();
} catch (e) {
if (e.status !== 409) {
throw e;
}
doc = await find(doc._id);
} }
return doc; return doc;
} }
@ -150,7 +152,11 @@ export function PouchORM(PouchDB) {
return Object.assign( return Object.assign(
{ {
new: _new, new: _new,
find getOrCreate,
find,
prefix,
db
// delete: // FIXME
}, },
opts.methods || {} opts.methods || {}
); );

View File

@ -1,14 +1,18 @@
import { extractID } from './conversion.js'; import { pick } from './conversion.js';
import { equals } from './set.js';
const extractID = pick('_id');
const extractREV = pick('_rev');
export function pouchDocComparator(a, b) {
return extractID(a) === extractID(b) && extractREV(a) === extractREV(b);
}
export function pouchDocArrayComparator(a, b) { export function pouchDocArrayComparator(a, b) {
if (!Array.isArray(a)) { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
return false; return false;
} }
const aIDs = a.map(extractID);
const bIDs = b.map(extractID);
return equals(new Set(...aIDs), new Set(...bIDs)); return a.every((aRec, index) => pouchDocComparator(aRec, b[index]));
} }
export function isObject(obj) { export function isObject(obj) {

View File

@ -36,6 +36,4 @@ export function deepAssign(to, ...rest) {
return to; return to;
} }
export function extractID(doc) { export const pick = id => doc => doc[id];
return doc._id;
}

View File

@ -1,66 +1,7 @@
import { prop, computed } from 'frptools'; import { prop, computed } from 'frptools';
import { matchesSelector } from 'pouchdb-selector-core';
import { getDatabase } from '../services/db.js';
import { Watcher } from './watcher.js'; import { Watcher } from './watcher.js';
import { pouchDocArrayComparator } from './comparators.js'; import { pouchDocArrayComparator } from './comparators.js';
import { difference } from './set.js';
// The point of the watcher 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 watcherMap = new Map();
const watchingIDs = new Map();
const dbIDs = new Map();
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(db, id, selector, refresher) {
if (!watcherMap.has(db)) {
watcherMap.set(db, Watcher(db, {}, true)(checkDocs));
}
if (!dbIDs.has(db)) {
dbIDs.set(db, new Set());
}
dbIDs.get(db).add(id);
if (!watchingIDs.has(id)) {
watchingIDs.set(id, new Map());
}
watchingIDs.get(id).set(selector, refresher);
}
function removeID(db, id, selector) {
if (watchingIDs.has(id)) {
const idSet = watchingIDs.get(id);
idSet.delete(selector);
if (idSet.size === 0) {
watchingIDs.delete(selector);
}
const dbIDMap = dbIDs.get(db);
dbIDMap.delete(id);
if (dbIDMap.size === 0) {
// Unsubscribe from this watcher
watcherMap.get(db)();
dbIDs.delete(db);
}
}
}
// LiveArray is a subscribable property function that always returns the db results that match the provided selector and calls subscribers when the results change. // 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, mapper) { export function LiveArray(db, selector, mapper) {
@ -72,10 +13,6 @@ export function LiveArray(db, selector, mapper) {
const data = prop({ docs: [] }); const data = prop({ docs: [] });
const docs = computed(r => r.docs.map(_mapper), [data], pouchDocArrayComparator); const docs = computed(r => r.docs.map(_mapper), [data], pouchDocArrayComparator);
const idSet = () => docs().reduce((acc, d) => acc.add(d._id), new Set());
const addThisID = id => addID(db, id, selector, refresh);
const removeThisID = id => removeID(db, id, selector);
const cleanup = () => { const cleanup = () => {
docs.unsubscribeAll(); docs.unsubscribeAll();
ready.unsubscribeAll(); ready.unsubscribeAll();
@ -83,18 +20,11 @@ export function LiveArray(db, selector, mapper) {
changeSub(); changeSub();
changeSub = null; changeSub = null;
} }
[...idSet()].forEach(removeThisID);
data({ docs: [] }); data({ docs: [] });
}; };
const refresh = async function refresh() { const refresh = async function refresh(...args) {
const oldIdSet = idSet();
data(await db.find({ selector })); data(await db.find({ selector }));
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.ready = ready;