From 91a33ca33200293170f7b6e1e3ffd86a0b207c99 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 11 Jan 2022 12:09:49 -0600 Subject: [PATCH 1/9] feat: tag notes index --- .../protocol/collection/item_collection.ts | 7 +- .../protocol/collection/tag_notes_index.ts | 74 +++++++++++++++++++ .../snjs/lib/services/item_manager.spec.ts | 55 +++++++++++++- packages/snjs/lib/services/item_manager.ts | 18 +++++ 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 packages/snjs/lib/protocol/collection/tag_notes_index.ts diff --git a/packages/snjs/lib/protocol/collection/item_collection.ts b/packages/snjs/lib/protocol/collection/item_collection.ts index 0b5d31cd7..53d9f5ea5 100644 --- a/packages/snjs/lib/protocol/collection/item_collection.ts +++ b/packages/snjs/lib/protocol/collection/item_collection.ts @@ -134,9 +134,12 @@ export class ItemCollection extends MutableCollection { const previousElement = !isNullOrUndefined(previousIndex) ? sortedElements[previousIndex] : undefined; - /** If the element is deleted, or if it no longer exists in the primary map (because + + /** + * If the element is deleted, or if it no longer exists in the primary map (because * it was discarded without neccessarily being marked as deleted), it does not pass - * the filter. If no filter the element passes by default. */ + * the filter. If no filter the element passes by default. + */ const passes = element.deleted || !this.map[element.uuid] ? false diff --git a/packages/snjs/lib/protocol/collection/tag_notes_index.ts b/packages/snjs/lib/protocol/collection/tag_notes_index.ts new file mode 100644 index 000000000..7eb99a83c --- /dev/null +++ b/packages/snjs/lib/protocol/collection/tag_notes_index.ts @@ -0,0 +1,74 @@ +import { ContentType } from '@Models/content_types'; +import { ItemCollection } from './item_collection'; +import { SNNote } from '@Lib/models'; +import { SNTag } from '@Lib/index'; +import { UuidString } from '@Lib/types'; + +export class TagNotesIndex { + private tagToNotesMap: Record> = {}; + private allCountableNotes = new Set(); + + constructor(private collection: ItemCollection) {} + + private isNoteCountable = (note: SNNote) => { + return !note.archived && !note.trashed; + }; + + public allCountableNotesCount(): number { + return this.allCountableNotes.size; + } + + public countableNotesForTag(tag: SNTag): number { + return this.tagToNotesMap[tag.uuid]?.size; + } + + public receiveTagAndNoteChanges(items: (SNTag | SNNote)[]): void { + const notes = items.filter( + (item) => item.content_type === ContentType.Note + ) as SNNote[]; + this.recieveNoteChanges(notes); + + const tags = items.filter( + (item) => item.content_type === ContentType.Tag + ) as SNTag[]; + this.recieveTagChanges(tags); + } + + private recieveTagChanges(tags: SNTag[]): void { + for (const tag of tags) { + const uuids = tag.noteReferences.map((ref) => ref.uuid); + const countableUuids = uuids.filter((uuid) => + this.allCountableNotes.has(uuid) + ); + this.tagToNotesMap[tag.uuid] = new Set(countableUuids); + } + } + + private recieveNoteChanges(notes: SNNote[]): void { + for (const note of notes) { + const isCountable = this.isNoteCountable(note); + + if (isCountable) { + this.allCountableNotes.add(note.uuid); + } else { + this.allCountableNotes.delete(note.uuid); + } + + const associatedTagUuids = this.collection.uuidsThatReferenceUuid( + note.uuid + ); + const associatedTags = this.collection.findAll( + associatedTagUuids + ) as SNTag[]; + + for (const tag of associatedTags) { + const set = this.tagToNotesMap[tag.uuid]; + if (isCountable) { + set.add(note.uuid); + } else { + set.delete(note.uuid); + } + } + } + } +} diff --git a/packages/snjs/lib/services/item_manager.spec.ts b/packages/snjs/lib/services/item_manager.spec.ts index a7f7ad2e3..bb4fed17f 100644 --- a/packages/snjs/lib/services/item_manager.spec.ts +++ b/packages/snjs/lib/services/item_manager.spec.ts @@ -1,5 +1,5 @@ import { ItemManager, SNItem } from '@Lib/index'; -import { SNNote } from '@Lib/models'; +import { SNNote, NoteMutator } from '@Lib/models'; import { SmartTagPredicateContent, SNSmartTag } from '@Lib/models/app/smartTag'; import { Uuid } from '@Lib/uuid'; import { SNTag, TagMutator } from '@Models/app/tag'; @@ -334,6 +334,59 @@ describe('itemManager', () => { }); }); + describe('tags notes index', () => { + it('counts countable notes', async () => { + itemManager = createService(); + + const parentTag = createTag('parent'); + const childTag = createTag('child'); + await itemManager.insertItems([parentTag, childTag]); + await itemManager.setTagParent(parentTag, childTag); + + const parentNote = createNote('parentNote'); + const childNote = createNote('childNote'); + await itemManager.insertItems([parentNote, childNote]); + + await itemManager.addTagToNote(parentNote, parentTag); + await itemManager.addTagToNote(childNote, childTag); + + expect(itemManager.countableNotesForTag(parentTag)).toBe(1); + expect(itemManager.countableNotesForTag(childTag)).toBe(1); + expect(itemManager.allCountableNotesCount()).toBe(2); + }); + + it('archiving a note should update count index', async () => { + itemManager = createService(); + + const tag1 = createTag('tag 1'); + await itemManager.insertItems([tag1]); + + const note1 = createNote('note 1'); + const note2 = createNote('note 2'); + await itemManager.insertItems([note1, note2]); + + await itemManager.addTagToNote(note1, tag1); + await itemManager.addTagToNote(note2, tag1); + + expect(itemManager.countableNotesForTag(tag1)).toBe(2); + expect(itemManager.allCountableNotesCount()).toBe(2); + + await itemManager.changeItem(note1.uuid, (m) => { + m.archived = true; + }); + + expect(itemManager.allCountableNotesCount()).toBe(1); + expect(itemManager.countableNotesForTag(tag1)).toBe(1); + + await itemManager.changeItem(note1.uuid, (m) => { + m.archived = false; + }); + + expect(itemManager.allCountableNotesCount()).toBe(2); + expect(itemManager.countableNotesForTag(tag1)).toBe(2); + }); + }); + describe('tags and smart tags', () => { it('lets me create a smart tag', async () => { itemManager = createService(); diff --git a/packages/snjs/lib/services/item_manager.ts b/packages/snjs/lib/services/item_manager.ts index 8caf50e4f..92bdefd88 100644 --- a/packages/snjs/lib/services/item_manager.ts +++ b/packages/snjs/lib/services/item_manager.ts @@ -1,3 +1,4 @@ +import { TagNotesIndex } from './../protocol/collection/tag_notes_index'; import { createMutatorForItem } from '@Lib/models/mutator'; import { ItemCollectionNotesView } from '@Lib/protocol/collection/item_collection_notes_view'; import { NotesDisplayCriteria } from '@Lib/protocol/collection/notes_display_criteria'; @@ -85,6 +86,7 @@ export class ItemManager extends PureService { private collection!: ItemCollection; private notesView!: ItemCollectionNotesView; private systemSmartTags: SNSmartTag[]; + private tagNotesIndex!: TagNotesIndex; constructor(private payloadManager: PayloadManager) { super(); @@ -130,6 +132,7 @@ export class ItemManager extends PureService { 'asc' ); this.notesView = new ItemCollectionNotesView(this.collection); + this.tagNotesIndex = new TagNotesIndex(this.collection); } public setDisplayOptions( @@ -253,6 +256,14 @@ export class ItemManager extends PureService { ) as SNComponent[]; } + public allCountableNotesCount(): number { + return this.tagNotesIndex.allCountableNotesCount(); + } + + public countableNotesForTag(tag: SNTag): number { + return this.tagNotesIndex.countableNotesForTag(tag); + } + public addObserver( contentType: ContentType | ContentType[], callback: ObserverCallback @@ -313,6 +324,13 @@ export class ItemManager extends PureService { this.collection.discard(item); } this.notesView.setNeedsRebuilding(); + this.tagNotesIndex.receiveTagAndNoteChanges( + changedOrInserted.filter( + (item) => + item.content_type === ContentType.Tag || + item.content_type === ContentType.Note + ) as SNTag[] + ); this.notifyObservers( changedItems, insertedItems, From a5a10dc250b3a348b613acdc20a8a57850eecbd1 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 11 Jan 2022 12:14:04 -0600 Subject: [PATCH 2/9] fix: remove redundant tag lookup --- packages/snjs/lib/protocol/collection/tag_notes_index.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/snjs/lib/protocol/collection/tag_notes_index.ts b/packages/snjs/lib/protocol/collection/tag_notes_index.ts index 7eb99a83c..5b97c9245 100644 --- a/packages/snjs/lib/protocol/collection/tag_notes_index.ts +++ b/packages/snjs/lib/protocol/collection/tag_notes_index.ts @@ -47,7 +47,6 @@ export class TagNotesIndex { private recieveNoteChanges(notes: SNNote[]): void { for (const note of notes) { const isCountable = this.isNoteCountable(note); - if (isCountable) { this.allCountableNotes.add(note.uuid); } else { @@ -57,12 +56,8 @@ export class TagNotesIndex { const associatedTagUuids = this.collection.uuidsThatReferenceUuid( note.uuid ); - const associatedTags = this.collection.findAll( - associatedTagUuids - ) as SNTag[]; - - for (const tag of associatedTags) { - const set = this.tagToNotesMap[tag.uuid]; + for (const tagUuid of associatedTagUuids) { + const set = this.tagToNotesMap[tagUuid]; if (isCountable) { set.add(note.uuid); } else { From 7780747efeb9a27191576a1fc1771eeedd7c1d8a Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 11 Jan 2022 12:30:14 -0600 Subject: [PATCH 3/9] fix: PR review --- packages/snjs/lib/models/app/note.ts | 8 ++++++-- packages/snjs/lib/models/app/tag.ts | 5 ++++- .../protocol/collection/tag_notes_index.ts | 19 ++++++++----------- packages/snjs/lib/services/item_manager.ts | 9 ++++----- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/snjs/lib/models/app/note.ts b/packages/snjs/lib/models/app/note.ts index 6fb26702d..cc2263b84 100644 --- a/packages/snjs/lib/models/app/note.ts +++ b/packages/snjs/lib/models/app/note.ts @@ -1,7 +1,8 @@ -import { PayloadContent } from '@Payloads/generator'; -import { PayloadFormat } from './../../protocol/payloads/formats'; import { isNullOrUndefined } from '@Lib/utils'; +import { ContentType } from '@Models/content_types'; import { AppDataField, ItemMutator, SNItem } from '@Models/core/item'; +import { PayloadContent } from '@Payloads/generator'; +import { PayloadFormat } from './../../protocol/payloads/formats'; import { PurePayload } from './../../protocol/payloads/pure_payload'; export interface NoteContent extends PayloadContent { @@ -14,6 +15,9 @@ export interface NoteContent extends PayloadContent { spellcheck?: boolean; } +export const isNote = (x: SNItem): x is SNNote => + x.content_type === ContentType.Note; + /** A note item */ export class SNNote extends SNItem implements NoteContent { public readonly title!: string; diff --git a/packages/snjs/lib/models/app/tag.ts b/packages/snjs/lib/models/app/tag.ts index 68cd0f82d..8346d3e02 100644 --- a/packages/snjs/lib/models/app/tag.ts +++ b/packages/snjs/lib/models/app/tag.ts @@ -9,6 +9,9 @@ export interface TagContent extends ItemContent { title: string; } +export const isTag = (x: SNItem): x is SNTag => + x.content_type === ContentType.Tag; + /** * Allows organization of notes into groups. * A tag can have many notes, and a note can have many tags. @@ -23,7 +26,7 @@ export class SNTag extends SNItem implements TagContent { get noteReferences(): ContentReference[] { const references = this.payload.safeReferences; - return references.filter(ref => ref.content_type === ContentType.Note) + return references.filter((ref) => ref.content_type === ContentType.Note); } get noteCount(): number { diff --git a/packages/snjs/lib/protocol/collection/tag_notes_index.ts b/packages/snjs/lib/protocol/collection/tag_notes_index.ts index 5b97c9245..c506e7782 100644 --- a/packages/snjs/lib/protocol/collection/tag_notes_index.ts +++ b/packages/snjs/lib/protocol/collection/tag_notes_index.ts @@ -1,8 +1,9 @@ -import { ContentType } from '@Models/content_types'; import { ItemCollection } from './item_collection'; import { SNNote } from '@Lib/models'; import { SNTag } from '@Lib/index'; import { UuidString } from '@Lib/types'; +import { isNote } from '@Lib/models/app/note'; +import { isTag } from '@Lib/models/app/tag'; export class TagNotesIndex { private tagToNotesMap: Record> = {}; @@ -23,18 +24,14 @@ export class TagNotesIndex { } public receiveTagAndNoteChanges(items: (SNTag | SNNote)[]): void { - const notes = items.filter( - (item) => item.content_type === ContentType.Note - ) as SNNote[]; - this.recieveNoteChanges(notes); + const notes = items.filter(isNote); + this.receiveNoteChanges(notes); - const tags = items.filter( - (item) => item.content_type === ContentType.Tag - ) as SNTag[]; - this.recieveTagChanges(tags); + const tags = items.filter(isTag); + this.receiveTagChanges(tags); } - private recieveTagChanges(tags: SNTag[]): void { + private receiveTagChanges(tags: SNTag[]): void { for (const tag of tags) { const uuids = tag.noteReferences.map((ref) => ref.uuid); const countableUuids = uuids.filter((uuid) => @@ -44,7 +41,7 @@ export class TagNotesIndex { } } - private recieveNoteChanges(notes: SNNote[]): void { + private receiveNoteChanges(notes: SNNote[]): void { for (const note of notes) { const isCountable = this.isNoteCountable(note); if (isCountable) { diff --git a/packages/snjs/lib/services/item_manager.ts b/packages/snjs/lib/services/item_manager.ts index 92bdefd88..96e0b2af9 100644 --- a/packages/snjs/lib/services/item_manager.ts +++ b/packages/snjs/lib/services/item_manager.ts @@ -70,6 +70,9 @@ export type TransactionalMutation = { mutationType?: MutationType; }; +export const isTagOrNote = (x: SNItem): x is SNNote | SNTag => + x.content_type === ContentType.Note || x.content_type === ContentType.Tag; + /** * The item manager is backed by the Payload Manager. Think of the item manager as a * more user-friendly or item-specific interface to creating and updating data. @@ -325,11 +328,7 @@ export class ItemManager extends PureService { } this.notesView.setNeedsRebuilding(); this.tagNotesIndex.receiveTagAndNoteChanges( - changedOrInserted.filter( - (item) => - item.content_type === ContentType.Tag || - item.content_type === ContentType.Note - ) as SNTag[] + changedOrInserted.filter(isTagOrNote) ); this.notifyObservers( changedItems, From 0990b2f413b2dc553eb68a3c3f4e4a3f96cd6c07 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 11 Jan 2022 12:39:38 -0600 Subject: [PATCH 4/9] fix: find or create set --- .../lib/protocol/collection/tag_notes_index.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/snjs/lib/protocol/collection/tag_notes_index.ts b/packages/snjs/lib/protocol/collection/tag_notes_index.ts index c506e7782..f3ffe9b1c 100644 --- a/packages/snjs/lib/protocol/collection/tag_notes_index.ts +++ b/packages/snjs/lib/protocol/collection/tag_notes_index.ts @@ -6,7 +6,7 @@ import { isNote } from '@Lib/models/app/note'; import { isTag } from '@Lib/models/app/tag'; export class TagNotesIndex { - private tagToNotesMap: Record> = {}; + private tagToNotesMap: Partial>> = {}; private allCountableNotes = new Set(); constructor(private collection: ItemCollection) {} @@ -20,7 +20,7 @@ export class TagNotesIndex { } public countableNotesForTag(tag: SNTag): number { - return this.tagToNotesMap[tag.uuid]?.size; + return this.tagToNotesMap[tag.uuid]?.size || 0; } public receiveTagAndNoteChanges(items: (SNTag | SNNote)[]): void { @@ -54,7 +54,7 @@ export class TagNotesIndex { note.uuid ); for (const tagUuid of associatedTagUuids) { - const set = this.tagToNotesMap[tagUuid]; + const set = this.setForTag(tagUuid); if (isCountable) { set.add(note.uuid); } else { @@ -63,4 +63,13 @@ export class TagNotesIndex { } } } + + private setForTag(uuid: UuidString): Set { + let set = this.tagToNotesMap[uuid]; + if (!set) { + set = new Set(); + this.tagToNotesMap[uuid] = set; + } + return set; + } } From 0370d5895e5b7fe7be9d8ece211c0ac77c639112 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 11 Jan 2022 13:06:06 -0600 Subject: [PATCH 5/9] feat: note count change observer --- packages/snjs/lib/application.ts | 15 ++++++++ .../protocol/collection/tag_notes_index.ts | 36 +++++++++++++++++++ packages/snjs/lib/services/item_manager.ts | 16 ++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/snjs/lib/application.ts b/packages/snjs/lib/application.ts index 2aff2dadf..2a75b5e0c 100644 --- a/packages/snjs/lib/application.ts +++ b/packages/snjs/lib/application.ts @@ -1,3 +1,4 @@ +import { TagNoteCountChangeObserver } from './protocol/collection/tag_notes_index'; import { TransactionalMutation } from './services/item_manager'; import { FeatureStatus } from '@Lib/services/features_service'; import { Settings } from './services/settings_service'; @@ -857,6 +858,20 @@ export class SNApplication { return this.itemManager.notesMatchingSmartTag(smartTag); } + public addNoteCountChangeObserver( + observer: TagNoteCountChangeObserver + ): () => void { + return this.itemManager.addNoteCountChangeObserver(observer); + } + + public allCountableNotesCount(): number { + return this.itemManager.allCountableNotesCount(); + } + + public countableNotesForTag(tag: SNTag): number { + return this.itemManager.countableNotesForTag(tag); + } + /** Returns an item's direct references */ public referencesForItem(item: SNItem, contentType?: ContentType): SNItem[] { let references = this.itemManager.referencesForItem(item.uuid); diff --git a/packages/snjs/lib/protocol/collection/tag_notes_index.ts b/packages/snjs/lib/protocol/collection/tag_notes_index.ts index f3ffe9b1c..9df0c946c 100644 --- a/packages/snjs/lib/protocol/collection/tag_notes_index.ts +++ b/packages/snjs/lib/protocol/collection/tag_notes_index.ts @@ -1,3 +1,4 @@ +import { removeFromArray } from '@Lib/utils'; import { ItemCollection } from './item_collection'; import { SNNote } from '@Lib/models'; import { SNTag } from '@Lib/index'; @@ -5,9 +6,15 @@ import { UuidString } from '@Lib/types'; import { isNote } from '@Lib/models/app/note'; import { isTag } from '@Lib/models/app/tag'; +/** tagUuid undefined signifies all notes count change */ +export type TagNoteCountChangeObserver = ( + tagUuid: UuidString | undefined +) => void; + export class TagNotesIndex { private tagToNotesMap: Partial>> = {}; private allCountableNotes = new Set(); + private observers: TagNoteCountChangeObserver[] = []; constructor(private collection: ItemCollection) {} @@ -15,6 +22,22 @@ export class TagNotesIndex { return !note.archived && !note.trashed; }; + public addCountChangeObserver( + observer: TagNoteCountChangeObserver + ): () => void { + this.observers.push(observer); + + return () => { + removeFromArray(this.observers, observer); + }; + } + + private notifyObservers(tagUuid: UuidString | undefined) { + for (const observer of this.observers) { + observer(tagUuid); + } + } + public allCountableNotesCount(): number { return this.allCountableNotes.size; } @@ -37,29 +60,42 @@ export class TagNotesIndex { const countableUuids = uuids.filter((uuid) => this.allCountableNotes.has(uuid) ); + const previousSet = this.tagToNotesMap[tag.uuid]; this.tagToNotesMap[tag.uuid] = new Set(countableUuids); + + if (previousSet?.size !== countableUuids.length) { + this.notifyObservers(tag.uuid); + } } } private receiveNoteChanges(notes: SNNote[]): void { for (const note of notes) { const isCountable = this.isNoteCountable(note); + const previousAllCount = this.allCountableNotes.size; if (isCountable) { this.allCountableNotes.add(note.uuid); } else { this.allCountableNotes.delete(note.uuid); } + if (previousAllCount !== this.allCountableNotes.size) { + this.notifyObservers(undefined); + } const associatedTagUuids = this.collection.uuidsThatReferenceUuid( note.uuid ); for (const tagUuid of associatedTagUuids) { const set = this.setForTag(tagUuid); + const previousCount = set.size; if (isCountable) { set.add(note.uuid); } else { set.delete(note.uuid); } + if (previousCount !== set.size) { + this.notifyObservers(tagUuid); + } } } } diff --git a/packages/snjs/lib/services/item_manager.ts b/packages/snjs/lib/services/item_manager.ts index 96e0b2af9..6d5db7d17 100644 --- a/packages/snjs/lib/services/item_manager.ts +++ b/packages/snjs/lib/services/item_manager.ts @@ -1,4 +1,7 @@ -import { TagNotesIndex } from './../protocol/collection/tag_notes_index'; +import { + TagNotesIndex, + TagNoteCountChangeObserver, +} from './../protocol/collection/tag_notes_index'; import { createMutatorForItem } from '@Lib/models/mutator'; import { ItemCollectionNotesView } from '@Lib/protocol/collection/item_collection_notes_view'; import { NotesDisplayCriteria } from '@Lib/protocol/collection/notes_display_criteria'; @@ -259,11 +262,22 @@ export class ItemManager extends PureService { ) as SNComponent[]; } + public addNoteCountChangeObserver( + observer: TagNoteCountChangeObserver + ): () => void { + return this.tagNotesIndex.addCountChangeObserver(observer); + } + public allCountableNotesCount(): number { return this.tagNotesIndex.allCountableNotesCount(); } public countableNotesForTag(tag: SNTag): number { + if (tag.isSmartTag) { + throw Error( + 'countableNotesForTag is not meant to be used for smart tags.' + ); + } return this.tagNotesIndex.countableNotesForTag(tag); } From 9349222a0cd70f0295393a7e29dbec636481806a Mon Sep 17 00:00:00 2001 From: Laurent Senta Date: Fri, 14 Jan 2022 14:24:39 +0100 Subject: [PATCH 6/9] refactor: restructure the indexing internals --- .../snjs/lib/protocol/collection/indexes.ts | 12 ++++ .../protocol/collection/item_collection.ts | 15 ++++- .../collection/item_collection_notes_view.ts | 36 +++++++----- .../protocol/collection/tag_notes_index.ts | 12 ++-- packages/snjs/lib/services/item_manager.ts | 55 +++++++++---------- 5 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 packages/snjs/lib/protocol/collection/indexes.ts diff --git a/packages/snjs/lib/protocol/collection/indexes.ts b/packages/snjs/lib/protocol/collection/indexes.ts new file mode 100644 index 000000000..d51e67981 --- /dev/null +++ b/packages/snjs/lib/protocol/collection/indexes.ts @@ -0,0 +1,12 @@ +import { SNItem } from '../../models/core/item'; + +export interface ItemDelta { + changed: SNItem[]; + inserted: SNItem[]; + discarded: SNItem[]; + ignored: SNItem[]; +} + +export interface SNIndex { + onChange(delta: ItemDelta): void; +} diff --git a/packages/snjs/lib/protocol/collection/item_collection.ts b/packages/snjs/lib/protocol/collection/item_collection.ts index 53d9f5ea5..e8d28c251 100644 --- a/packages/snjs/lib/protocol/collection/item_collection.ts +++ b/packages/snjs/lib/protocol/collection/item_collection.ts @@ -3,6 +3,7 @@ import { compareValues, isNullOrUndefined, uniqueArrayByKey } from '@Lib/utils'; import { SNItem } from './../../models/core/item'; import { ContentType } from '@standardnotes/common'; import { UuidString } from './../../types'; +import { ItemDelta, SNIndex } from './indexes'; export enum CollectionSort { CreatedAt = 'created_at', @@ -13,7 +14,9 @@ export type SortDirection = 'asc' | 'dsc'; /** The item collection class builds on mutable collection by providing an option to keep * items sorted and filtered. */ -export class ItemCollection extends MutableCollection { +export class ItemCollection + extends MutableCollection + implements SNIndex { private displaySortBy: Partial< Record< ContentType, @@ -263,4 +266,14 @@ export class ItemCollection extends MutableCollection { } this.sortedMap[contentType] = cleaned; } + + public onChange(delta: ItemDelta): void { + const changedOrInserted = delta.changed.concat(delta.inserted); + + if (changedOrInserted.length > 0) { + this.set(changedOrInserted); + } + + this.discard(delta.discarded); + } } diff --git a/packages/snjs/lib/protocol/collection/item_collection_notes_view.ts b/packages/snjs/lib/protocol/collection/item_collection_notes_view.ts index b253b0b55..d03187f71 100644 --- a/packages/snjs/lib/protocol/collection/item_collection_notes_view.ts +++ b/packages/snjs/lib/protocol/collection/item_collection_notes_view.ts @@ -1,7 +1,8 @@ +import { ContentType } from '@Lib/index'; +import { SNNote, SNTag } from '../../models'; import { SNSmartTag } from './../../models/app/smartTag'; +import { ItemDelta, SNIndex } from './indexes'; import { ItemCollection } from './item_collection'; -import { SNNote, SNTag } from '../../models'; -import { ContentType } from '@standardnotes/common'; import { criteriaForSmartTag, NotesDisplayCriteria, @@ -11,7 +12,7 @@ import { /** * A view into ItemCollection that allows filtering by tag and smart tag. */ -export class ItemCollectionNotesView { +export class ItemCollectionNotesView implements SNIndex { private displayedNotes: SNNote[] = []; private needsRebuilding = true; @@ -35,7 +36,22 @@ export class ItemCollectionNotesView { return notesMatchingCriteria(criteria, this.collection); } + public displayElements(): SNNote[] { + if (this.needsRebuilding) { + this.rebuildList(); + } + return this.displayedNotes.slice(); + } + private rebuildList(): void { + this.displayedNotes = notesMatchingCriteria( + this.currentCriteria, + this.collection + ); + this.needsRebuilding = false; + } + + private get currentCriteria(): NotesDisplayCriteria { const mostRecentVersionOfTags = this.criteria.tags .map((tag) => { if (tag.isSystemSmartTag) { @@ -45,21 +61,15 @@ export class ItemCollectionNotesView { } }) .filter((tag) => tag != undefined); + const criteria = NotesDisplayCriteria.Copy(this.criteria, { tags: mostRecentVersionOfTags, }); - this.displayedNotes = notesMatchingCriteria(criteria, this.collection); - } - setNeedsRebuilding() { - this.needsRebuilding = true; + return criteria; } - displayElements() { - if (this.needsRebuilding) { - this.rebuildList(); - this.needsRebuilding = false; - } - return this.displayedNotes.slice(); + public onChange(_delta: ItemDelta): void { + this.needsRebuilding = true; } } diff --git a/packages/snjs/lib/protocol/collection/tag_notes_index.ts b/packages/snjs/lib/protocol/collection/tag_notes_index.ts index 9df0c946c..0a2c4798d 100644 --- a/packages/snjs/lib/protocol/collection/tag_notes_index.ts +++ b/packages/snjs/lib/protocol/collection/tag_notes_index.ts @@ -5,13 +5,14 @@ import { SNTag } from '@Lib/index'; import { UuidString } from '@Lib/types'; import { isNote } from '@Lib/models/app/note'; import { isTag } from '@Lib/models/app/tag'; +import { ItemDelta, SNIndex } from './indexes'; /** tagUuid undefined signifies all notes count change */ export type TagNoteCountChangeObserver = ( tagUuid: UuidString | undefined ) => void; -export class TagNotesIndex { +export class TagNotesIndex implements SNIndex { private tagToNotesMap: Partial>> = {}; private allCountableNotes = new Set(); private observers: TagNoteCountChangeObserver[] = []; @@ -46,11 +47,12 @@ export class TagNotesIndex { return this.tagToNotesMap[tag.uuid]?.size || 0; } - public receiveTagAndNoteChanges(items: (SNTag | SNNote)[]): void { - const notes = items.filter(isNote); - this.receiveNoteChanges(notes); + public onChange(delta: ItemDelta): void { + const changedOrInserted = delta.changed.concat(delta.inserted); + const notes = changedOrInserted.filter(isNote); + const tags = changedOrInserted.filter(isTag); - const tags = items.filter(isTag); + this.receiveNoteChanges(notes); this.receiveTagChanges(tags); } diff --git a/packages/snjs/lib/services/item_manager.ts b/packages/snjs/lib/services/item_manager.ts index 6d5db7d17..564514d05 100644 --- a/packages/snjs/lib/services/item_manager.ts +++ b/packages/snjs/lib/services/item_manager.ts @@ -1,8 +1,5 @@ -import { - TagNotesIndex, - TagNoteCountChangeObserver, -} from './../protocol/collection/tag_notes_index'; import { createMutatorForItem } from '@Lib/models/mutator'; +import { ItemDelta } from '@Lib/protocol/collection/indexes'; import { ItemCollectionNotesView } from '@Lib/protocol/collection/item_collection_notes_view'; import { NotesDisplayCriteria } from '@Lib/protocol/collection/notes_display_criteria'; import { PureService } from '@Lib/services/pure_service'; @@ -17,31 +14,34 @@ import { CreateMaxPayloadFromAnyObject } from '@Payloads/generator'; import { CollectionSort, ItemCollection, - SortDirection, + SortDirection } from '@Protocol/collection/item_collection'; import { ContentType } from '@standardnotes/common' import { ComponentMutator } from './../models/app/component'; import { ActionsExtensionMutator, - SNActionsExtension, + SNActionsExtension } from './../models/app/extension'; import { FeatureRepoMutator, - SNFeatureRepo, + SNFeatureRepo } from './../models/app/feature_repo'; import { ItemsKeyMutator } from './../models/app/items_key'; import { NoteMutator, SNNote } from './../models/app/note'; import { SmartTagPredicateContent, SMART_TAG_DSL_PREFIX, - SNSmartTag, + SNSmartTag } from './../models/app/smartTag'; import { TagMutator } from './../models/app/tag'; import { ItemMutator, MutationType, SNItem } from './../models/core/item'; import { SNPredicate } from './../models/core/predicate'; +import { + TagNoteCountChangeObserver, TagNotesIndex +} from './../protocol/collection/tag_notes_index'; import { PayloadContent, - PayloadOverride, + PayloadOverride } from './../protocol/payloads/generator'; import { PurePayload } from './../protocol/payloads/pure_payload'; import { PayloadSource } from './../protocol/payloads/sources'; @@ -329,26 +329,25 @@ export class ItemManager extends PureService { source: PayloadSource, sourceKey?: string ) { - const changedItems = changed.map((p) => CreateItemFromPayload(p)); - const insertedItems = inserted.map((p) => CreateItemFromPayload(p)); - const ignoredItems = ignored.map((p) => CreateItemFromPayload(p)); - const changedOrInserted = changedItems.concat(insertedItems); - if (changedOrInserted.length > 0) { - this.collection.set(changedOrInserted); - } - const discardedItems = discarded.map((p) => CreateItemFromPayload(p)); - for (const item of discardedItems) { - this.collection.discard(item); - } - this.notesView.setNeedsRebuilding(); - this.tagNotesIndex.receiveTagAndNoteChanges( - changedOrInserted.filter(isTagOrNote) - ); + const createItems = (items: PurePayload[]) => + items.map((item) => CreateItemFromPayload(item)); + + const delta: ItemDelta = { + changed: createItems(changed), + inserted: createItems(inserted), + discarded: createItems(discarded), + ignored: createItems(ignored), + }; + + this.collection.onChange(delta); + this.notesView.onChange(delta); + this.tagNotesIndex.onChange(delta); + this.notifyObservers( - changedItems, - insertedItems, - discardedItems, - ignoredItems, + delta.changed, + delta.inserted, + delta.discarded, + delta.ignored, source, sourceKey ); From 7945488cff4832d7964171e0860cdef10cae5e09 Mon Sep 17 00:00:00 2001 From: Laurent Senta Date: Fri, 14 Jan 2022 14:26:19 +0100 Subject: [PATCH 7/9] doc: add more intuitive e2e commands --- README.md | 4 ++-- package.json | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ba670616..b4a621dcc 100644 --- a/README.md +++ b/README.md @@ -138,10 +138,10 @@ From the root of the repository, run: ``` # Starts browser-navigable web page -yarn run start:test-server:dev +yarn run start:e2e:mocha # Starts backend servers -yarn run test:e2e:dev-setup +yarn run start:e2e:docker ``` Then choose between the following run options: diff --git a/package.json b/package.json index c4618ee33..1ff18c725 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "bootstrap": "lerna bootstrap", "start": "lerna run start --parallel", "test:unit": "lerna run test:unit --parallel", + "start:e2e:mocha": "yarn run start:test-server:dev", + "start:e2e:docker": "yarn run test:e2e:dev-setup", "start:test-server": "node e2e-server.js", "start:test-server:dev": "node e2e-server.js --dev", "test:e2e": "bash test.sh stable", From 2fda84d5ddb3df4be17ac2a7e5a188980bb6a042 Mon Sep 17 00:00:00 2001 From: Laurent Senta Date: Mon, 17 Jan 2022 10:33:12 +0100 Subject: [PATCH 8/9] refactor: allow countable over smarttags --- packages/snjs/lib/application.ts | 2 +- packages/snjs/lib/services/item_manager.ts | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/snjs/lib/application.ts b/packages/snjs/lib/application.ts index 2a75b5e0c..ce7c7a3cb 100644 --- a/packages/snjs/lib/application.ts +++ b/packages/snjs/lib/application.ts @@ -868,7 +868,7 @@ export class SNApplication { return this.itemManager.allCountableNotesCount(); } - public countableNotesForTag(tag: SNTag): number { + public countableNotesForTag(tag: SNTag | SNSmartTag): number { return this.itemManager.countableNotesForTag(tag); } diff --git a/packages/snjs/lib/services/item_manager.ts b/packages/snjs/lib/services/item_manager.ts index 564514d05..1ee4514bb 100644 --- a/packages/snjs/lib/services/item_manager.ts +++ b/packages/snjs/lib/services/item_manager.ts @@ -14,34 +14,35 @@ import { CreateMaxPayloadFromAnyObject } from '@Payloads/generator'; import { CollectionSort, ItemCollection, - SortDirection + SortDirection, } from '@Protocol/collection/item_collection'; -import { ContentType } from '@standardnotes/common' +import { ContentType } from '@standardnotes/common'; import { ComponentMutator } from './../models/app/component'; import { ActionsExtensionMutator, - SNActionsExtension + SNActionsExtension, } from './../models/app/extension'; import { FeatureRepoMutator, - SNFeatureRepo + SNFeatureRepo, } from './../models/app/feature_repo'; import { ItemsKeyMutator } from './../models/app/items_key'; import { NoteMutator, SNNote } from './../models/app/note'; import { SmartTagPredicateContent, SMART_TAG_DSL_PREFIX, - SNSmartTag + SNSmartTag, } from './../models/app/smartTag'; import { TagMutator } from './../models/app/tag'; import { ItemMutator, MutationType, SNItem } from './../models/core/item'; import { SNPredicate } from './../models/core/predicate'; import { - TagNoteCountChangeObserver, TagNotesIndex + TagNoteCountChangeObserver, + TagNotesIndex, } from './../protocol/collection/tag_notes_index'; import { PayloadContent, - PayloadOverride + PayloadOverride, } from './../protocol/payloads/generator'; import { PurePayload } from './../protocol/payloads/pure_payload'; import { PayloadSource } from './../protocol/payloads/sources'; @@ -272,8 +273,12 @@ export class ItemManager extends PureService { return this.tagNotesIndex.allCountableNotesCount(); } - public countableNotesForTag(tag: SNTag): number { + public countableNotesForTag(tag: SNTag | SNSmartTag): number { if (tag.isSmartTag) { + if (tag.isAllTag) { + return this.tagNotesIndex.allCountableNotesCount(); + } + throw Error( 'countableNotesForTag is not meant to be used for smart tags.' ); From a0cca74adc064dee38b5f6cca79175bfb80c38e9 Mon Sep 17 00:00:00 2001 From: Laurent Senta Date: Mon, 17 Jan 2022 10:35:20 +0100 Subject: [PATCH 9/9] fix: type import --- packages/snjs/lib/models/app/note.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snjs/lib/models/app/note.ts b/packages/snjs/lib/models/app/note.ts index cc2263b84..5d636984d 100644 --- a/packages/snjs/lib/models/app/note.ts +++ b/packages/snjs/lib/models/app/note.ts @@ -1,5 +1,5 @@ +import { ContentType } from '@Lib/index'; import { isNullOrUndefined } from '@Lib/utils'; -import { ContentType } from '@Models/content_types'; import { AppDataField, ItemMutator, SNItem } from '@Models/core/item'; import { PayloadContent } from '@Payloads/generator'; import { PayloadFormat } from './../../protocol/payloads/formats';