Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 64 additions & 12 deletions src/TemplateCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,61 @@ import Sortable from "./Util/Objects/Sortable.js";
import { isGlobMatch } from "./Util/GlobMatcher.js";

class TemplateCollection extends Sortable {
// Both caches are invalidated in `add`, the only entry point that mutates `items`
#allSortedCache;
#tagIndexCache;

constructor() {
super();

this._filteredByGlobsCache = new Map();
}

add(item) {
this.#allSortedCache = undefined;
this.#tagIndexCache = undefined;

super.add(item);
}

getAll() {
return this.items.slice();
}

// Sorted once per mutation instead of once per collection API call
#getAllSorted() {
if (!this.#allSortedCache) {
this.#allSortedCache = this.sort(Sortable.sortFunctionDateInputPath);
}

return this.#allSortedCache;
}

// Tag name to items (in sorted order), so tag lookups don’t walk the full collection
#getTagIndex() {
if (!this.#tagIndexCache) {
let index = new Map();

for (let item of this.#getAllSorted()) {
for (let tagName of TemplateData.getIncludedTagNames(item.data)) {
let tagItems = index.get(tagName);
if (!tagItems) {
tagItems = [];
index.set(tagName, tagItems);
}
tagItems.push(item);
}
}

this.#tagIndexCache = index;
}

return this.#tagIndexCache;
}

getAllSorted() {
return this.sort(Sortable.sortFunctionDateInputPath);
// Callers sort and reverse the result in place, so hand out a copy
return this.#getAllSorted().slice();
}

getSortedByDate() {
Expand Down Expand Up @@ -56,20 +99,29 @@ class TemplateCollection extends Sortable {
}

getFilteredByTag(tagName) {
return this.getAllSorted().filter((item) => {
if (!tagName || TemplateData.getIncludedTagNames(item.data).includes(tagName)) {
return true;
}
return false;
});
if (!tagName) {
return this.getAllSorted();
}

return this.#getTagIndex().get(tagName)?.slice() || [];
}

getFilteredByTags(...tags) {
return this.getAllSorted().filter((item) => {
let itemTags = new Set(TemplateData.getIncludedTagNames(item.data));
return tags.every((requiredTag) => {
return itemTags.has(requiredTag);
});
if (!tags.length) {
return this.getAllSorted();
}

let index = this.#getTagIndex();
let [firstTag, ...remainingTags] = tags;
let matches = index.get(firstTag) || [];
if (!remainingTags.length) {
return matches.slice();
}

// Intersect the first tag’s matches with the remaining tags
let remainingItems = remainingTags.map((tagName) => new Set(index.get(tagName)));
return matches.filter((item) => {
return remainingItems.every((tagItems) => tagItems.has(item));
});
}
}
Expand Down
73 changes: 73 additions & 0 deletions test/TemplateCollectionTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,79 @@ test("getFilteredByTag (added out of order, sorted)", async (t) => {
t.deepEqual(dogs[0].template, tmpl1);
});

test("getFilteredByTag (results are cached, cache is invalidated by add)", async (t) => {
let $config = await getTemplateConfigInstance();

let tmpl1 = await getNewTemplateByNumber(1, $config);
let tmpl2 = await getNewTemplateByNumber(2, $config);
let tmpl3 = await getNewTemplateByNumber(3, $config);

let c = new Collection();
await addTemplate(c, tmpl1);

let posts = c.getFilteredByTag("post");
t.is(posts.length, 1);

await addTemplate(c, tmpl2);
await addTemplate(c, tmpl3);

t.is(c.getFilteredByTag("post").length, 2);
t.is(c.getFilteredByTag("cat").length, 2);
t.is(c.getFilteredByTags("post", "cat").length, 1);
t.is(c.getAllSorted().length, 3);

// The result returned before the additions is unaffected
t.is(posts.length, 1);
});

test("getFilteredByTag (results are mutable copies)", async (t) => {
let $config = await getTemplateConfigInstance();

let tmpl1 = await getNewTemplateByNumber(1, $config);
let tmpl3 = await getNewTemplateByNumber(3, $config);

let c = new Collection();
await addTemplate(c, tmpl1);
await addTemplate(c, tmpl3);

let posts = c.getFilteredByTag("post");
posts.reverse();
posts.pop();

let posts2 = c.getFilteredByTag("post");
t.is(posts2.length, 2);
t.deepEqual(posts2[0].template, tmpl1);
t.deepEqual(posts2[1].template, tmpl3);

let postsPlural = c.getFilteredByTags("post");
postsPlural.pop();
t.is(c.getFilteredByTags("post").length, 2);
});

test("getFilteredByTag (no tag name returns everything, sorted)", async (t) => {
let $config = await getTemplateConfigInstance();

let tmpl1 = await getNewTemplateByNumber(1, $config);
let tmpl4 = await getNewTemplateByNumber(4, $config);
let tmpl5 = await getNewTemplateByNumber(5, $config);

let c = new Collection();
await addTemplate(c, tmpl1);
await addTemplate(c, tmpl4);
await addTemplate(c, tmpl5);

let all = c.getFilteredByTag();
t.is(all.length, 3);
t.deepEqual(all[0].template, tmpl4);
t.deepEqual(all[1].template, tmpl1);
t.deepEqual(all[2].template, tmpl5);

// `all` is not a tag name, it is filtered out of the included tag names
t.is(c.getFilteredByTag("all").length, 0);
t.is(c.getFilteredByTag("unknown-tag").length, 0);
t.is(c.getFilteredByTags().length, 3);
});

test("getFilteredByTags", async (t) => {
let $config = await getTemplateConfigInstance();

Expand Down