feat: add models package
This commit is contained in:
263
packages/models/src/Domain/Runtime/Collection/Collection.ts
Normal file
263
packages/models/src/Domain/Runtime/Collection/Collection.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { extendArray, isObject, isString, UuidMap } from '@standardnotes/utils'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { remove } from 'lodash'
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { ContentReference } from '../../Abstract/Item'
|
||||
|
||||
export interface CollectionElement {
|
||||
uuid: Uuid
|
||||
content_type: ContentType
|
||||
dirty?: boolean
|
||||
deleted?: boolean
|
||||
}
|
||||
|
||||
export interface DecryptedCollectionElement<C extends ItemContent = ItemContent> extends CollectionElement {
|
||||
content: C
|
||||
references: ContentReference[]
|
||||
}
|
||||
|
||||
export interface DeletedCollectionElement extends CollectionElement {
|
||||
content: undefined
|
||||
deleted: true
|
||||
}
|
||||
|
||||
export interface EncryptedCollectionElement extends CollectionElement {
|
||||
content: string
|
||||
errorDecrypting: boolean
|
||||
}
|
||||
|
||||
export abstract class Collection<
|
||||
Element extends Decrypted | Encrypted | Deleted,
|
||||
Decrypted extends DecryptedCollectionElement,
|
||||
Encrypted extends EncryptedCollectionElement,
|
||||
Deleted extends DeletedCollectionElement,
|
||||
> {
|
||||
readonly map: Partial<Record<Uuid, Element>> = {}
|
||||
readonly typedMap: Partial<Record<ContentType, Element[]>> = {}
|
||||
|
||||
/** An array of uuids of items that are dirty */
|
||||
dirtyIndex: Set<Uuid> = new Set()
|
||||
|
||||
/** An array of uuids of items that are not marked as deleted */
|
||||
nondeletedIndex: Set<Uuid> = new Set()
|
||||
|
||||
/** An array of uuids of items that are errorDecrypting or waitingForKey */
|
||||
invalidsIndex: Set<Uuid> = new Set()
|
||||
|
||||
readonly referenceMap: UuidMap
|
||||
|
||||
/** Maintains an index for each item uuid where the value is an array of uuids that are
|
||||
* conflicts of that item. So if Note B and C are conflicts of Note A,
|
||||
* conflictMap[A.uuid] == [B.uuid, C.uuid] */
|
||||
readonly conflictMap: UuidMap
|
||||
|
||||
isDecryptedElement = (e: Decrypted | Encrypted | Deleted): e is Decrypted => {
|
||||
return isObject(e.content)
|
||||
}
|
||||
|
||||
isEncryptedElement = (e: Decrypted | Encrypted | Deleted): e is Encrypted => {
|
||||
return 'content' in e && isString(e.content)
|
||||
}
|
||||
|
||||
isErrorDecryptingElement = (e: Decrypted | Encrypted | Deleted): e is Encrypted => {
|
||||
return this.isEncryptedElement(e) && e.errorDecrypting === true
|
||||
}
|
||||
|
||||
isDeletedElement = (e: Decrypted | Encrypted | Deleted): e is Deleted => {
|
||||
return 'deleted' in e && e.deleted === true
|
||||
}
|
||||
|
||||
isNonDeletedElement = (e: Decrypted | Encrypted | Deleted): e is Decrypted | Encrypted => {
|
||||
return !this.isDeletedElement(e)
|
||||
}
|
||||
|
||||
constructor(
|
||||
copy = false,
|
||||
mapCopy?: Partial<Record<Uuid, Element>>,
|
||||
typedMapCopy?: Partial<Record<ContentType, Element[]>>,
|
||||
referenceMapCopy?: UuidMap,
|
||||
conflictMapCopy?: UuidMap,
|
||||
) {
|
||||
if (copy) {
|
||||
this.map = mapCopy!
|
||||
this.typedMap = typedMapCopy!
|
||||
this.referenceMap = referenceMapCopy!
|
||||
this.conflictMap = conflictMapCopy!
|
||||
} else {
|
||||
this.referenceMap = new UuidMap()
|
||||
this.conflictMap = new UuidMap()
|
||||
}
|
||||
}
|
||||
|
||||
public uuids(): Uuid[] {
|
||||
return Object.keys(this.map)
|
||||
}
|
||||
|
||||
public all(contentType?: ContentType | ContentType[]): Element[] {
|
||||
if (contentType) {
|
||||
if (Array.isArray(contentType)) {
|
||||
const elements: Element[] = []
|
||||
for (const type of contentType) {
|
||||
extendArray(elements, this.typedMap[type] || [])
|
||||
}
|
||||
return elements
|
||||
} else {
|
||||
return this.typedMap[contentType]?.slice() || []
|
||||
}
|
||||
} else {
|
||||
return Object.keys(this.map).map((uuid: Uuid) => {
|
||||
return this.map[uuid]
|
||||
}) as Element[]
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns all elements that are not marked as deleted */
|
||||
public nondeletedElements(): Element[] {
|
||||
const uuids = Array.from(this.nondeletedIndex)
|
||||
return this.findAll(uuids).filter(this.isNonDeletedElement)
|
||||
}
|
||||
|
||||
/** Returns all elements that are errorDecrypting or waitingForKey */
|
||||
public invalidElements(): Encrypted[] {
|
||||
const uuids = Array.from(this.invalidsIndex)
|
||||
return this.findAll(uuids) as Encrypted[]
|
||||
}
|
||||
|
||||
/** Returns all elements that are marked as dirty */
|
||||
public dirtyElements(): Element[] {
|
||||
const uuids = Array.from(this.dirtyIndex)
|
||||
return this.findAll(uuids)
|
||||
}
|
||||
|
||||
public findAll(uuids: Uuid[]): Element[] {
|
||||
const results: Element[] = []
|
||||
|
||||
for (const id of uuids) {
|
||||
const element = this.map[id]
|
||||
if (element) {
|
||||
results.push(element)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
public find(uuid: Uuid): Element | undefined {
|
||||
return this.map[uuid]
|
||||
}
|
||||
|
||||
public has(uuid: Uuid): boolean {
|
||||
return this.find(uuid) != undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* If an item is not found, an `undefined` element
|
||||
* will be inserted into the array.
|
||||
*/
|
||||
public findAllIncludingBlanks<E extends Element>(uuids: Uuid[]): (E | Deleted | undefined)[] {
|
||||
const results: (E | Deleted | undefined)[] = []
|
||||
|
||||
for (const id of uuids) {
|
||||
const element = this.map[id] as E | Deleted | undefined
|
||||
results.push(element)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
public set(elements: Element | Element[]): void {
|
||||
elements = Array.isArray(elements) ? elements : [elements]
|
||||
|
||||
if (elements.length === 0) {
|
||||
console.warn('Attempting to set 0 elements onto collection')
|
||||
return
|
||||
}
|
||||
|
||||
for (const element of elements) {
|
||||
this.map[element.uuid] = element
|
||||
this.setToTypedMap(element)
|
||||
|
||||
if (this.isErrorDecryptingElement(element)) {
|
||||
this.invalidsIndex.add(element.uuid)
|
||||
} else {
|
||||
this.invalidsIndex.delete(element.uuid)
|
||||
}
|
||||
|
||||
if (this.isDecryptedElement(element)) {
|
||||
const conflictOf = element.content.conflict_of
|
||||
if (conflictOf) {
|
||||
this.conflictMap.establishRelationship(conflictOf, element.uuid)
|
||||
}
|
||||
|
||||
this.referenceMap.setAllRelationships(
|
||||
element.uuid,
|
||||
element.references.map((r) => r.uuid),
|
||||
)
|
||||
}
|
||||
|
||||
if (element.dirty) {
|
||||
this.dirtyIndex.add(element.uuid)
|
||||
} else {
|
||||
this.dirtyIndex.delete(element.uuid)
|
||||
}
|
||||
|
||||
if (element.deleted) {
|
||||
this.nondeletedIndex.delete(element.uuid)
|
||||
} else {
|
||||
this.nondeletedIndex.add(element.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public discard(elements: Element | Element[]): void {
|
||||
elements = Array.isArray(elements) ? elements : [elements]
|
||||
for (const element of elements) {
|
||||
this.deleteFromTypedMap(element)
|
||||
delete this.map[element.uuid]
|
||||
this.conflictMap.removeFromMap(element.uuid)
|
||||
this.referenceMap.removeFromMap(element.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
public uuidReferencesForUuid(uuid: Uuid): Uuid[] {
|
||||
return this.referenceMap.getDirectRelationships(uuid)
|
||||
}
|
||||
|
||||
public uuidsThatReferenceUuid(uuid: Uuid): Uuid[] {
|
||||
return this.referenceMap.getInverseRelationships(uuid)
|
||||
}
|
||||
|
||||
public referencesForElement(element: Decrypted): Element[] {
|
||||
const uuids = this.referenceMap.getDirectRelationships(element.uuid)
|
||||
return this.findAll(uuids)
|
||||
}
|
||||
|
||||
public conflictsOf(uuid: Uuid): Element[] {
|
||||
const uuids = this.conflictMap.getDirectRelationships(uuid)
|
||||
return this.findAll(uuids)
|
||||
}
|
||||
|
||||
public elementsReferencingElement(element: Decrypted, contentType?: ContentType): Element[] {
|
||||
const uuids = this.uuidsThatReferenceUuid(element.uuid)
|
||||
const items = this.findAll(uuids)
|
||||
|
||||
if (!contentType) {
|
||||
return items
|
||||
}
|
||||
|
||||
return items.filter((item) => item.content_type === contentType)
|
||||
}
|
||||
|
||||
private setToTypedMap(element: Element): void {
|
||||
const array = this.typedMap[element.content_type] || []
|
||||
remove(array, { uuid: element.uuid as never })
|
||||
array.push(element)
|
||||
this.typedMap[element.content_type] = array
|
||||
}
|
||||
|
||||
private deleteFromTypedMap(element: Element): void {
|
||||
const array = this.typedMap[element.content_type] || []
|
||||
remove(array, { uuid: element.uuid as never })
|
||||
this.typedMap[element.content_type] = array
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { UuidMap } from '@standardnotes/utils'
|
||||
|
||||
export interface CollectionInterface {
|
||||
/** Maintains an index where the direct map for each item id is an array
|
||||
* of item ids that the item references. This is essentially equivalent to
|
||||
* item.content.references, but keeps state even when the item is deleted.
|
||||
* So if tag A references Note B, referenceMap.directMap[A.uuid] == [B.uuid].
|
||||
* The inverse map for each item is an array of item ids where the items reference the
|
||||
* key item. So if tag A references Note B, referenceMap.inverseMap[B.uuid] == [A.uuid].
|
||||
* This allows callers to determine for a given item, who references it?
|
||||
* It would be prohibitive to look this up on demand */
|
||||
readonly referenceMap: UuidMap
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Uuid, ContentType } from '@standardnotes/common'
|
||||
|
||||
export interface SortableItem {
|
||||
uuid: Uuid
|
||||
content_type: ContentType
|
||||
created_at: Date
|
||||
userModifiedDate: Date
|
||||
title?: string
|
||||
pinned: boolean
|
||||
}
|
||||
|
||||
export const CollectionSort: Record<string, keyof SortableItem> = {
|
||||
CreatedAt: 'created_at',
|
||||
UpdatedAt: 'userModifiedDate',
|
||||
Title: 'title',
|
||||
}
|
||||
|
||||
export type CollectionSortDirection = 'asc' | 'dsc'
|
||||
|
||||
export type CollectionSortProperty = keyof SortableItem
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NoteContent } from './../../../Syncable/Note/NoteContent'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedItem } from '../../../Abstract/Item'
|
||||
import { DecryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload'
|
||||
import { ItemCollection } from './ItemCollection'
|
||||
import { FillItemContent, ItemContent } from '../../../Abstract/Content/ItemContent'
|
||||
|
||||
describe('item collection', () => {
|
||||
const createDecryptedPayload = (uuid?: string): DecryptedPayload => {
|
||||
return new DecryptedPayload({
|
||||
uuid: uuid || String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: FillItemContent<NoteContent>({
|
||||
title: 'foo',
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
}
|
||||
|
||||
it('setting same item twice should not result in doubles', () => {
|
||||
const collection = new ItemCollection()
|
||||
|
||||
const decryptedItem = new DecryptedItem(createDecryptedPayload())
|
||||
collection.set(decryptedItem)
|
||||
|
||||
const updatedItem = new DecryptedItem(
|
||||
decryptedItem.payload.copy({
|
||||
content: { foo: 'bar' } as unknown as jest.Mocked<ItemContent>,
|
||||
}),
|
||||
)
|
||||
|
||||
collection.set(updatedItem)
|
||||
|
||||
expect(collection.all()).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ItemContent } from './../../../Abstract/Content/ItemContent'
|
||||
import { EncryptedItemInterface } from './../../../Abstract/Item/Interfaces/EncryptedItem'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { SNIndex } from '../../Index/SNIndex'
|
||||
import { isDecryptedItem } from '../../../Abstract/Item/Interfaces/TypeCheck'
|
||||
import { DecryptedItemInterface } from '../../../Abstract/Item/Interfaces/DecryptedItem'
|
||||
import { CollectionInterface } from '../CollectionInterface'
|
||||
import { DeletedItemInterface } from '../../../Abstract/Item'
|
||||
import { Collection } from '../Collection'
|
||||
import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes'
|
||||
import { ItemDelta } from '../../Index/ItemDelta'
|
||||
|
||||
export class ItemCollection
|
||||
extends Collection<AnyItemInterface, DecryptedItemInterface, EncryptedItemInterface, DeletedItemInterface>
|
||||
implements SNIndex, CollectionInterface
|
||||
{
|
||||
public onChange(delta: ItemDelta): void {
|
||||
const changedOrInserted = delta.changed.concat(delta.inserted)
|
||||
|
||||
if (changedOrInserted.length > 0) {
|
||||
this.set(changedOrInserted)
|
||||
}
|
||||
|
||||
this.discard(delta.discarded)
|
||||
}
|
||||
|
||||
public findDecrypted<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: Uuid): T | undefined {
|
||||
const result = this.find(uuid)
|
||||
|
||||
if (!result) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return isDecryptedItem(result) ? (result as T) : undefined
|
||||
}
|
||||
|
||||
public findAllDecrypted<T extends DecryptedItemInterface = DecryptedItemInterface>(uuids: Uuid[]): T[] {
|
||||
return this.findAll(uuids).filter(isDecryptedItem) as T[]
|
||||
}
|
||||
|
||||
public findAllDecryptedWithBlanks<C extends ItemContent = ItemContent>(
|
||||
uuids: Uuid[],
|
||||
): (DecryptedItemInterface<C> | undefined)[] {
|
||||
const results = this.findAllIncludingBlanks(uuids)
|
||||
const mapped = results.map((i) => {
|
||||
if (i == undefined || isDecryptedItem(i)) {
|
||||
return i
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
return mapped as (DecryptedItemInterface<C> | undefined)[]
|
||||
}
|
||||
|
||||
public allDecrypted<T extends DecryptedItemInterface>(contentType: ContentType | ContentType[]): T[] {
|
||||
return this.all(contentType).filter(isDecryptedItem) as T[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NoteContent } from './../../../Syncable/Note/NoteContent'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedItem, EncryptedItem } from '../../../Abstract/Item'
|
||||
import { DecryptedPayload, EncryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload'
|
||||
import { ItemCollection } from './ItemCollection'
|
||||
import { FillItemContent } from '../../../Abstract/Content/ItemContent'
|
||||
import { TagNotesIndex } from './TagNotesIndex'
|
||||
import { ItemDelta } from '../../Index/ItemDelta'
|
||||
import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes'
|
||||
|
||||
describe('tag notes index', () => {
|
||||
const createEncryptedItem = (uuid?: string) => {
|
||||
const payload = new EncryptedPayload({
|
||||
uuid: uuid || String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: '004:...',
|
||||
enc_item_key: '004:...',
|
||||
items_key_id: '123',
|
||||
waitingForKey: true,
|
||||
errorDecrypting: true,
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
return new EncryptedItem(payload)
|
||||
}
|
||||
|
||||
const createDecryptedItem = (uuid?: string) => {
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: uuid || String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: FillItemContent<NoteContent>({
|
||||
title: 'foo',
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return new DecryptedItem(payload)
|
||||
}
|
||||
|
||||
const createChangeDelta = (item: AnyItemInterface): ItemDelta => {
|
||||
return {
|
||||
changed: [item],
|
||||
inserted: [],
|
||||
discarded: [],
|
||||
ignored: [],
|
||||
unerrored: [],
|
||||
}
|
||||
}
|
||||
|
||||
it('should decrement count after decrypted note becomes errored', () => {
|
||||
const collection = new ItemCollection()
|
||||
const index = new TagNotesIndex(collection)
|
||||
|
||||
const decryptedItem = createDecryptedItem()
|
||||
collection.set(decryptedItem)
|
||||
index.onChange(createChangeDelta(decryptedItem))
|
||||
|
||||
expect(index.allCountableNotesCount()).toEqual(1)
|
||||
|
||||
const encryptedItem = createEncryptedItem(decryptedItem.uuid)
|
||||
collection.set(encryptedItem)
|
||||
index.onChange(createChangeDelta(encryptedItem))
|
||||
|
||||
expect(index.allCountableNotesCount()).toEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { isTag, SNTag } from '../../../Syncable/Tag/Tag'
|
||||
import { SNIndex } from '../../Index/SNIndex'
|
||||
import { ItemCollection } from './ItemCollection'
|
||||
import { ItemDelta } from '../../Index/ItemDelta'
|
||||
import { isDecryptedItem, ItemInterface } from '../../../Abstract/Item'
|
||||
|
||||
type AllNotesUuidSignifier = undefined
|
||||
export type TagNoteCountChangeObserver = (tagUuid: Uuid | AllNotesUuidSignifier) => void
|
||||
|
||||
export class TagNotesIndex implements SNIndex {
|
||||
private tagToNotesMap: Partial<Record<Uuid, Set<Uuid>>> = {}
|
||||
private allCountableNotes = new Set<Uuid>()
|
||||
|
||||
constructor(private collection: ItemCollection, public observers: TagNoteCountChangeObserver[] = []) {}
|
||||
|
||||
private isNoteCountable = (note: ItemInterface) => {
|
||||
if (isDecryptedItem(note)) {
|
||||
return !note.archived && !note.trashed
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public addCountChangeObserver(observer: TagNoteCountChangeObserver): () => void {
|
||||
this.observers.push(observer)
|
||||
|
||||
const thislessEventObservers = this.observers
|
||||
return () => {
|
||||
removeFromArray(thislessEventObservers, observer)
|
||||
}
|
||||
}
|
||||
|
||||
private notifyObservers(tagUuid: Uuid | undefined) {
|
||||
for (const observer of this.observers) {
|
||||
observer(tagUuid)
|
||||
}
|
||||
}
|
||||
|
||||
public allCountableNotesCount(): number {
|
||||
return this.allCountableNotes.size
|
||||
}
|
||||
|
||||
public countableNotesForTag(tag: SNTag): number {
|
||||
return this.tagToNotesMap[tag.uuid]?.size || 0
|
||||
}
|
||||
|
||||
public onChange(delta: ItemDelta): void {
|
||||
const notes = [...delta.changed, ...delta.inserted, ...delta.discarded].filter(
|
||||
(i) => i.content_type === ContentType.Note,
|
||||
)
|
||||
const tags = [...delta.changed, ...delta.inserted].filter(isDecryptedItem).filter(isTag)
|
||||
|
||||
this.receiveNoteChanges(notes)
|
||||
this.receiveTagChanges(tags)
|
||||
}
|
||||
|
||||
private receiveTagChanges(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))
|
||||
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: ItemInterface[]): void {
|
||||
const previousAllCount = this.allCountableNotes.size
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (previousAllCount !== this.allCountableNotes.size) {
|
||||
this.notifyObservers(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
private setForTag(uuid: Uuid): Set<Uuid> {
|
||||
let set = this.tagToNotesMap[uuid]
|
||||
if (!set) {
|
||||
set = new Set()
|
||||
this.tagToNotesMap[uuid] = set
|
||||
}
|
||||
return set
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { FullyFormedPayloadInterface } from './../../../Abstract/Payload/Interfaces/UnionTypes'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { UuidMap } from '@standardnotes/utils'
|
||||
import { PayloadCollection } from './PayloadCollection'
|
||||
|
||||
export class ImmutablePayloadCollection<
|
||||
P extends FullyFormedPayloadInterface = FullyFormedPayloadInterface,
|
||||
> extends PayloadCollection<P> {
|
||||
public get payloads(): P[] {
|
||||
return this.all()
|
||||
}
|
||||
|
||||
/** We don't use a constructor for this because we don't want the constructor to have
|
||||
* side-effects, such as calling collection.set(). */
|
||||
static WithPayloads<T extends FullyFormedPayloadInterface>(payloads: T[] = []): ImmutablePayloadCollection<T> {
|
||||
const collection = new ImmutablePayloadCollection<T>()
|
||||
if (payloads.length > 0) {
|
||||
collection.set(payloads)
|
||||
}
|
||||
|
||||
Object.freeze(collection)
|
||||
return collection
|
||||
}
|
||||
|
||||
static FromCollection<T extends FullyFormedPayloadInterface>(
|
||||
collection: PayloadCollection<T>,
|
||||
): ImmutablePayloadCollection<T> {
|
||||
const mapCopy = Object.freeze(Object.assign({}, collection.map))
|
||||
const typedMapCopy = Object.freeze(Object.assign({}, collection.typedMap))
|
||||
const referenceMapCopy = Object.freeze(collection.referenceMap.makeCopy()) as UuidMap
|
||||
const conflictMapCopy = Object.freeze(collection.conflictMap.makeCopy()) as UuidMap
|
||||
|
||||
const result = new ImmutablePayloadCollection<T>(
|
||||
true,
|
||||
mapCopy,
|
||||
typedMapCopy as Partial<Record<ContentType, T[]>>,
|
||||
referenceMapCopy,
|
||||
conflictMapCopy,
|
||||
)
|
||||
|
||||
Object.freeze(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
mutableCopy(): PayloadCollection<P> {
|
||||
const mapCopy = Object.assign({}, this.map)
|
||||
const typedMapCopy = Object.assign({}, this.typedMap)
|
||||
const referenceMapCopy = this.referenceMap.makeCopy()
|
||||
const conflictMapCopy = this.conflictMap.makeCopy()
|
||||
const result = new PayloadCollection(true, mapCopy, typedMapCopy, referenceMapCopy, conflictMapCopy)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { FullyFormedPayloadInterface } from './../../../Abstract/Payload/Interfaces/UnionTypes'
|
||||
import { EncryptedPayloadInterface } from '../../../Abstract/Payload/Interfaces/EncryptedPayload'
|
||||
import { CollectionInterface } from '../CollectionInterface'
|
||||
import { DecryptedPayloadInterface } from '../../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { IntegrityPayload } from '@standardnotes/responses'
|
||||
import { Collection } from '../Collection'
|
||||
import { DeletedPayloadInterface } from '../../../Abstract/Payload'
|
||||
|
||||
export class PayloadCollection<P extends FullyFormedPayloadInterface = FullyFormedPayloadInterface>
|
||||
extends Collection<P, DecryptedPayloadInterface, EncryptedPayloadInterface, DeletedPayloadInterface>
|
||||
implements CollectionInterface
|
||||
{
|
||||
public integrityPayloads(): IntegrityPayload[] {
|
||||
const nondeletedElements = this.nondeletedElements()
|
||||
|
||||
return nondeletedElements.map((item) => ({
|
||||
uuid: item.uuid,
|
||||
updated_at_timestamp: item.serverUpdatedAtTimestamp as number,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user