feat: improve initial load performance on mobile (#2126)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { TransferPayload } from '@standardnotes/models'
|
||||
|
||||
export type DatabaseItemMetadata = Pick<TransferPayload, 'uuid' | 'updated_at' | 'content_type'>
|
||||
44
packages/services/src/Domain/Device/DatabaseLoadOptions.ts
Normal file
44
packages/services/src/Domain/Device/DatabaseLoadOptions.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FullyFormedTransferPayload } from '@standardnotes/models'
|
||||
|
||||
export type DatabaseKeysLoadChunk = {
|
||||
keys: string[]
|
||||
}
|
||||
|
||||
export type DatabaseFullEntryLoadChunk = {
|
||||
entries: FullyFormedTransferPayload[]
|
||||
}
|
||||
|
||||
export function isChunkFullEntry(
|
||||
x: DatabaseKeysLoadChunk | DatabaseFullEntryLoadChunk,
|
||||
): x is DatabaseFullEntryLoadChunk {
|
||||
return (x as DatabaseFullEntryLoadChunk).entries !== undefined
|
||||
}
|
||||
|
||||
export type DatabaseKeysLoadChunkResponse = {
|
||||
keys: {
|
||||
itemsKeys: DatabaseKeysLoadChunk
|
||||
remainingChunks: DatabaseKeysLoadChunk[]
|
||||
}
|
||||
remainingChunksItemCount: number
|
||||
}
|
||||
|
||||
export type DatabaseFullEntryLoadChunkResponse = {
|
||||
fullEntries: {
|
||||
itemsKeys: DatabaseFullEntryLoadChunk
|
||||
remainingChunks: DatabaseFullEntryLoadChunk[]
|
||||
}
|
||||
remainingChunksItemCount: number
|
||||
}
|
||||
|
||||
export function isFullEntryLoadChunkResponse(
|
||||
x: DatabaseKeysLoadChunkResponse | DatabaseFullEntryLoadChunkResponse,
|
||||
): x is DatabaseFullEntryLoadChunkResponse {
|
||||
return (x as DatabaseFullEntryLoadChunkResponse).fullEntries !== undefined
|
||||
}
|
||||
|
||||
export type DatabaseLoadOptions = {
|
||||
contentTypePriority: ContentType[]
|
||||
uuidPriority: string[]
|
||||
batchSize: number
|
||||
}
|
||||
150
packages/services/src/Domain/Device/DatabaseLoadSorter.spec.ts
Normal file
150
packages/services/src/Domain/Device/DatabaseLoadSorter.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FullyFormedPayloadInterface } from '@standardnotes/models'
|
||||
import { GetSortedPayloadsByPriority } from './DatabaseLoadSorter'
|
||||
|
||||
describe('GetSortedPayloadsByPriority', () => {
|
||||
let payloads: FullyFormedPayloadInterface[] = []
|
||||
const contentTypePriority = [ContentType.ItemsKey, ContentType.UserPrefs, ContentType.Component, ContentType.Theme]
|
||||
let launchPriorityUuids: string[] = []
|
||||
|
||||
it('should sort payloads based on content type priority', () => {
|
||||
payloads = [
|
||||
{
|
||||
content_type: ContentType.Theme,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.UserPrefs,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Component,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.ItemsKey,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Note,
|
||||
} as FullyFormedPayloadInterface,
|
||||
]
|
||||
|
||||
const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority(payloads, {
|
||||
contentTypePriority,
|
||||
uuidPriority: launchPriorityUuids,
|
||||
batchSize: 1000,
|
||||
})
|
||||
|
||||
expect(itemsKeyPayloads.length).toBe(1)
|
||||
expect(itemsKeyPayloads[0].content_type).toBe(ContentType.ItemsKey)
|
||||
|
||||
expect(contentTypePriorityPayloads.length).toBe(3)
|
||||
expect(contentTypePriorityPayloads[0].content_type).toBe(ContentType.UserPrefs)
|
||||
expect(contentTypePriorityPayloads[1].content_type).toBe(ContentType.Component)
|
||||
expect(contentTypePriorityPayloads[2].content_type).toBe(ContentType.Theme)
|
||||
|
||||
expect(remainingPayloads.length).toBe(1)
|
||||
expect(remainingPayloads[0].content_type).toBe(ContentType.Note)
|
||||
})
|
||||
|
||||
it('should sort payloads based on launch priority uuids', () => {
|
||||
const unprioritizedNoteUuid = 'unprioritized-note'
|
||||
const unprioritizedTagUuid = 'unprioritized-tag'
|
||||
|
||||
const prioritizedNoteUuid = 'prioritized-note'
|
||||
const prioritizedTagUuid = 'prioritized-tag'
|
||||
|
||||
payloads = [
|
||||
{
|
||||
content_type: ContentType.Theme,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.UserPrefs,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Component,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.ItemsKey,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Note,
|
||||
uuid: unprioritizedNoteUuid,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Tag,
|
||||
uuid: unprioritizedTagUuid,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Note,
|
||||
uuid: prioritizedNoteUuid,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Tag,
|
||||
uuid: prioritizedTagUuid,
|
||||
} as FullyFormedPayloadInterface,
|
||||
]
|
||||
|
||||
launchPriorityUuids = [prioritizedNoteUuid, prioritizedTagUuid]
|
||||
|
||||
const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority(payloads, {
|
||||
contentTypePriority,
|
||||
uuidPriority: launchPriorityUuids,
|
||||
batchSize: 1000,
|
||||
})
|
||||
|
||||
expect(itemsKeyPayloads.length).toBe(1)
|
||||
expect(itemsKeyPayloads[0].content_type).toBe(ContentType.ItemsKey)
|
||||
|
||||
expect(contentTypePriorityPayloads.length).toBe(3)
|
||||
expect(contentTypePriorityPayloads[0].content_type).toBe(ContentType.UserPrefs)
|
||||
expect(contentTypePriorityPayloads[1].content_type).toBe(ContentType.Component)
|
||||
expect(contentTypePriorityPayloads[2].content_type).toBe(ContentType.Theme)
|
||||
|
||||
expect(remainingPayloads.length).toBe(4)
|
||||
expect(remainingPayloads[0].uuid).toBe(prioritizedNoteUuid)
|
||||
expect(remainingPayloads[1].uuid).toBe(prioritizedTagUuid)
|
||||
expect(remainingPayloads[2].uuid).toBe(unprioritizedNoteUuid)
|
||||
expect(remainingPayloads[3].uuid).toBe(unprioritizedTagUuid)
|
||||
})
|
||||
|
||||
it('should sort payloads based on server updated date if same content type', () => {
|
||||
const unprioritizedNoteUuid = 'unprioritized-note'
|
||||
const unprioritizedTagUuid = 'unprioritized-tag'
|
||||
|
||||
const prioritizedNoteUuid = 'prioritized-note'
|
||||
const prioritizedTagUuid = 'prioritized-tag'
|
||||
|
||||
payloads = [
|
||||
{
|
||||
content_type: ContentType.Note,
|
||||
uuid: unprioritizedNoteUuid,
|
||||
updated_at: new Date(1),
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Tag,
|
||||
uuid: unprioritizedTagUuid,
|
||||
updated_at: new Date(2),
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Note,
|
||||
uuid: prioritizedNoteUuid,
|
||||
} as FullyFormedPayloadInterface,
|
||||
{
|
||||
content_type: ContentType.Tag,
|
||||
uuid: prioritizedTagUuid,
|
||||
} as FullyFormedPayloadInterface,
|
||||
]
|
||||
|
||||
launchPriorityUuids = [prioritizedNoteUuid, prioritizedTagUuid]
|
||||
|
||||
const { remainingPayloads } = GetSortedPayloadsByPriority(payloads, {
|
||||
contentTypePriority,
|
||||
uuidPriority: launchPriorityUuids,
|
||||
batchSize: 1000,
|
||||
})
|
||||
|
||||
expect(remainingPayloads.length).toBe(4)
|
||||
expect(remainingPayloads[0].uuid).toBe(prioritizedNoteUuid)
|
||||
expect(remainingPayloads[1].uuid).toBe(prioritizedTagUuid)
|
||||
expect(remainingPayloads[2].uuid).toBe(unprioritizedTagUuid)
|
||||
expect(remainingPayloads[3].uuid).toBe(unprioritizedNoteUuid)
|
||||
})
|
||||
})
|
||||
113
packages/services/src/Domain/Device/DatabaseLoadSorter.ts
Normal file
113
packages/services/src/Domain/Device/DatabaseLoadSorter.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { DatabaseItemMetadata } from './DatabaseItemMetadata'
|
||||
import { DatabaseLoadOptions } from './DatabaseLoadOptions'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
|
||||
/**
|
||||
* Sorts payloads according by most recently modified first, according to the priority,
|
||||
* whereby the earlier a content_type appears in the priorityList,
|
||||
* the earlier it will appear in the resulting sorted array.
|
||||
*/
|
||||
function SortPayloadsByRecentAndContentPriority<T extends DatabaseItemMetadata = DatabaseItemMetadata>(
|
||||
payloads: T[],
|
||||
contentTypePriorityList: ContentType[],
|
||||
): T[] {
|
||||
return payloads.sort((a, b) => {
|
||||
const dateResult = new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
|
||||
let aPriority = 0
|
||||
let bPriority = 0
|
||||
|
||||
aPriority = contentTypePriorityList.indexOf(a.content_type)
|
||||
bPriority = contentTypePriorityList.indexOf(b.content_type)
|
||||
|
||||
if (aPriority === -1) {
|
||||
aPriority = contentTypePriorityList.length
|
||||
}
|
||||
|
||||
if (bPriority === -1) {
|
||||
bPriority = contentTypePriorityList.length
|
||||
}
|
||||
|
||||
if (aPriority === bPriority) {
|
||||
return dateResult
|
||||
}
|
||||
|
||||
if (aPriority < bPriority) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts payloads according by most recently modified first, according to the priority,
|
||||
* whereby the earlier a uuid appears in the priorityList,
|
||||
* the earlier it will appear in the resulting sorted array.
|
||||
*/
|
||||
function SortPayloadsByRecentAndUuidPriority<T extends DatabaseItemMetadata = DatabaseItemMetadata>(
|
||||
payloads: T[],
|
||||
uuidPriorityList: Uuid[],
|
||||
): T[] {
|
||||
return payloads.sort((a, b) => {
|
||||
const dateResult = new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
|
||||
let aPriority = 0
|
||||
let bPriority = 0
|
||||
|
||||
aPriority = uuidPriorityList.indexOf(a.uuid)
|
||||
bPriority = uuidPriorityList.indexOf(b.uuid)
|
||||
|
||||
if (aPriority === -1) {
|
||||
aPriority = uuidPriorityList.length
|
||||
}
|
||||
|
||||
if (bPriority === -1) {
|
||||
bPriority = uuidPriorityList.length
|
||||
}
|
||||
|
||||
if (aPriority === bPriority) {
|
||||
return dateResult
|
||||
}
|
||||
|
||||
if (aPriority < bPriority) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function GetSortedPayloadsByPriority<T extends DatabaseItemMetadata = DatabaseItemMetadata>(
|
||||
payloads: T[],
|
||||
options: DatabaseLoadOptions,
|
||||
): {
|
||||
itemsKeyPayloads: T[]
|
||||
contentTypePriorityPayloads: T[]
|
||||
remainingPayloads: T[]
|
||||
} {
|
||||
const itemsKeyPayloads: T[] = []
|
||||
const contentTypePriorityPayloads: T[] = []
|
||||
const remainingPayloads: T[] = []
|
||||
|
||||
for (let index = 0; index < payloads.length; index++) {
|
||||
const payload = payloads[index]
|
||||
|
||||
if (payload.content_type === ContentType.ItemsKey) {
|
||||
itemsKeyPayloads.push(payload)
|
||||
} else if (options.contentTypePriority.includes(payload.content_type)) {
|
||||
contentTypePriorityPayloads.push(payload)
|
||||
} else {
|
||||
remainingPayloads.push(payload)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
itemsKeyPayloads,
|
||||
contentTypePriorityPayloads: SortPayloadsByRecentAndContentPriority(
|
||||
contentTypePriorityPayloads,
|
||||
options.contentTypePriority,
|
||||
),
|
||||
remainingPayloads: SortPayloadsByRecentAndUuidPriority(remainingPayloads, options.uuidPriority),
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,14 @@ import { ApplicationIdentifier } from '@standardnotes/common'
|
||||
import {
|
||||
FullyFormedTransferPayload,
|
||||
TransferPayload,
|
||||
LegacyRawKeychainValue,
|
||||
NamespacedRootKeyInKeychain,
|
||||
Environment,
|
||||
} from '@standardnotes/models'
|
||||
import {
|
||||
DatabaseLoadOptions,
|
||||
DatabaseKeysLoadChunkResponse,
|
||||
DatabaseFullEntryLoadChunkResponse,
|
||||
} from './DatabaseLoadOptions'
|
||||
|
||||
/**
|
||||
* Platforms must override this class to provide platform specific utilities
|
||||
@@ -21,8 +25,6 @@ export interface DeviceInterface {
|
||||
|
||||
getJsonParsedRawStorageValue(key: string): Promise<unknown | undefined>
|
||||
|
||||
getAllRawStorageKeyValues(): Promise<{ key: string; value: unknown }[]>
|
||||
|
||||
setRawStorageValue(key: string, value: string): Promise<void>
|
||||
|
||||
removeRawStorageValue(key: string): Promise<void>
|
||||
@@ -38,10 +40,10 @@ export interface DeviceInterface {
|
||||
*/
|
||||
openDatabase(identifier: ApplicationIdentifier): Promise<{ isNewDatabase?: boolean } | undefined>
|
||||
|
||||
/**
|
||||
* In a key/value database, this function returns just the keys.
|
||||
*/
|
||||
getDatabaseKeys(): Promise<string[]>
|
||||
getDatabaseLoadChunks(
|
||||
options: DatabaseLoadOptions,
|
||||
identifier: ApplicationIdentifier,
|
||||
): Promise<DatabaseKeysLoadChunkResponse | DatabaseFullEntryLoadChunkResponse>
|
||||
|
||||
/**
|
||||
* Remove all keychain and database data from device.
|
||||
@@ -52,17 +54,22 @@ export interface DeviceInterface {
|
||||
*/
|
||||
clearAllDataFromDevice(workspaceIdentifiers: ApplicationIdentifier[]): Promise<{ killsApplication: boolean }>
|
||||
|
||||
getAllRawDatabasePayloads<T extends FullyFormedTransferPayload = FullyFormedTransferPayload>(
|
||||
getAllDatabaseEntries<T extends FullyFormedTransferPayload = FullyFormedTransferPayload>(
|
||||
identifier: ApplicationIdentifier,
|
||||
): Promise<T[]>
|
||||
|
||||
saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier): Promise<void>
|
||||
getDatabaseEntries<T extends FullyFormedTransferPayload = FullyFormedTransferPayload>(
|
||||
identifier: ApplicationIdentifier,
|
||||
keys: string[],
|
||||
): Promise<T[]>
|
||||
|
||||
saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise<void>
|
||||
saveDatabaseEntry(payload: TransferPayload, identifier: ApplicationIdentifier): Promise<void>
|
||||
|
||||
removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier): Promise<void>
|
||||
saveDatabaseEntries(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise<void>
|
||||
|
||||
removeAllRawDatabasePayloads(identifier: ApplicationIdentifier): Promise<void>
|
||||
removeDatabaseEntry(id: string, identifier: ApplicationIdentifier): Promise<void>
|
||||
|
||||
removeAllDatabaseEntries(identifier: ApplicationIdentifier): Promise<void>
|
||||
|
||||
getNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise<NamespacedRootKeyInKeychain | undefined>
|
||||
|
||||
@@ -70,8 +77,6 @@ export interface DeviceInterface {
|
||||
|
||||
clearNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise<void>
|
||||
|
||||
setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise<void>
|
||||
|
||||
clearRawKeychainValue(): Promise<void>
|
||||
|
||||
openUrl(url: string): void
|
||||
|
||||
Reference in New Issue
Block a user