align customId handling of EphemeralCacheStorage with OfflineStorage

To sort customIds within the offline database we store customIds
as base64Ext id strings in the offline storage. We want to align
the EphemeralCacheStorage to behave the same, and likewise store
customIds with base64Ext encoding.
This commit is contained in:
jhm 2024-08-28 17:47:42 +02:00 committed by FajindraII
parent c0e6944fb2
commit 2b87bef35a
2 changed files with 69 additions and 34 deletions

View file

@ -298,8 +298,8 @@ AND NOT(${firstIdBigger("elementId", range.upper)})`
*/ */
async getRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Range | null> { async getRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Range | null> {
let range = await this.getRange(typeRef, listId) let range = await this.getRange(typeRef, listId)
const typeModel = await resolveTypeReference(typeRef)
if (range == null) return range if (range == null) return range
const typeModel = await resolveTypeReference(typeRef)
return { return {
lower: customIdToBase64Url(typeModel, range.lower), lower: customIdToBase64Url(typeModel, range.lower),
upper: customIdToBase64Url(typeModel, range.upper), upper: customIdToBase64Url(typeModel, range.upper),

View file

@ -1,12 +1,13 @@
import { BlobElementEntity, ElementEntity, ListElementEntity, SomeEntity, TypeModel } from "../../common/EntityTypes.js" import { BlobElementEntity, ElementEntity, ListElementEntity, SomeEntity, TypeModel } from "../../common/EntityTypes.js"
import { EntityRestClient, typeRefToPath } from "./EntityRestClient.js" import { EntityRestClient, typeRefToPath } from "./EntityRestClient.js"
import { firstBiggerThanSecond, getElementId, getListId } from "../../common/utils/EntityUtils.js" import { firstBiggerThanSecond } from "../../common/utils/EntityUtils.js"
import { CacheStorage, LastUpdateTime } from "./DefaultEntityRestCache.js" import { CacheStorage, expandId, LastUpdateTime } from "./DefaultEntityRestCache.js"
import { assertNotNull, clone, getFromMap, remove, TypeRef } from "@tutao/tutanota-utils" import { assertNotNull, clone, getFromMap, remove, TypeRef } from "@tutao/tutanota-utils"
import { CustomCacheHandlerMap } from "./CustomCacheHandler.js" import { CustomCacheHandlerMap } from "./CustomCacheHandler.js"
import { resolveTypeReference } from "../../common/EntityFunctions.js" import { resolveTypeReference } from "../../common/EntityFunctions.js"
import { Type as TypeId } from "../../common/EntityConstants.js" import { Type as TypeId } from "../../common/EntityConstants.js"
import { ProgrammingError } from "../../common/error/ProgrammingError.js" import { ProgrammingError } from "../../common/error/ProgrammingError.js"
import { customIdToBase64Url, ensureBase64Ext } from "../offline/OfflineStorage.js"
/** Cache for a single list. */ /** Cache for a single list. */
type ListCache = { type ListCache = {
@ -59,39 +60,41 @@ export class EphemeralCacheStorage implements CacheStorage {
/** /**
* Get a given entity from the cache, expects that you have already checked for existence * Get a given entity from the cache, expects that you have already checked for existence
*/ */
async get<T extends SomeEntity>(typeRef: TypeRef<T>, listId: Id | null, id: Id): Promise<T | null> { async get<T extends SomeEntity>(typeRef: TypeRef<T>, listId: Id | null, elementId: Id): Promise<T | null> {
// We downcast because we can't prove that map has correct entity on the type level // We downcast because we can't prove that map has correct entity on the type level
const path = typeRefToPath(typeRef) const path = typeRefToPath(typeRef)
const typeModel = await resolveTypeReference(typeRef) const typeModel = await resolveTypeReference(typeRef)
elementId = ensureBase64Ext(typeModel, elementId)
switch (typeModel.type) { switch (typeModel.type) {
case TypeId.Element: case TypeId.Element:
return clone((this.entities.get(path)?.get(id) as T | undefined) ?? null) return clone((this.entities.get(path)?.get(elementId) as T | undefined) ?? null)
case TypeId.ListElement: case TypeId.ListElement:
return clone((this.lists.get(path)?.get(assertNotNull(listId))?.elements.get(id) as T | undefined) ?? null) return clone((this.lists.get(path)?.get(assertNotNull(listId))?.elements.get(elementId) as T | undefined) ?? null)
case TypeId.BlobElement: case TypeId.BlobElement:
return clone((this.blobEntities.get(path)?.get(assertNotNull(listId))?.elements.get(id) as T | undefined) ?? null) return clone((this.blobEntities.get(path)?.get(assertNotNull(listId))?.elements.get(elementId) as T | undefined) ?? null)
default: default:
throw new ProgrammingError("must be a persistent type") throw new ProgrammingError("must be a persistent type")
} }
} }
async deleteIfExists<T>(typeRef: TypeRef<T>, listId: Id | null, id: Id): Promise<void> { async deleteIfExists<T>(typeRef: TypeRef<T>, listId: Id | null, elementId: Id): Promise<void> {
const path = typeRefToPath(typeRef) const path = typeRefToPath(typeRef)
let typeModel: TypeModel let typeModel: TypeModel
typeModel = await resolveTypeReference(typeRef) typeModel = await resolveTypeReference(typeRef)
elementId = ensureBase64Ext(typeModel, elementId)
switch (typeModel.type) { switch (typeModel.type) {
case TypeId.Element: case TypeId.Element:
this.entities.get(path)?.delete(id) this.entities.get(path)?.delete(elementId)
break break
case TypeId.ListElement: case TypeId.ListElement:
const cache = this.lists.get(path)?.get(assertNotNull(listId)) const cache = this.lists.get(path)?.get(assertNotNull(listId))
if (cache != null) { if (cache != null) {
cache.elements.delete(id) cache.elements.delete(elementId)
remove(cache.allRange, id) remove(cache.allRange, elementId)
} }
break break
case TypeId.BlobElement: case TypeId.BlobElement:
this.blobEntities.get(path)?.get(assertNotNull(listId))?.elements.delete(id) this.blobEntities.get(path)?.get(assertNotNull(listId))?.elements.delete(elementId)
break break
default: default:
throw new ProgrammingError("must be a persistent type") throw new ProgrammingError("must be a persistent type")
@ -102,38 +105,43 @@ export class EphemeralCacheStorage implements CacheStorage {
getFromMap(this.entities, typeRefToPath(typeRef), () => new Map()).set(id, entity) getFromMap(this.entities, typeRefToPath(typeRef), () => new Map()).set(id, entity)
} }
async isElementIdInCacheRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, id: Id): Promise<boolean> { async isElementIdInCacheRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, elementId: Id): Promise<boolean> {
const typeModel = await resolveTypeReference(typeRef)
elementId = ensureBase64Ext(typeModel, elementId)
const cache = this.lists.get(typeRefToPath(typeRef))?.get(listId) const cache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
return cache != null && !firstBiggerThanSecond(id, cache.upperRangeId) && !firstBiggerThanSecond(cache.lowerRangeId, id) return cache != null && !firstBiggerThanSecond(elementId, cache.upperRangeId) && !firstBiggerThanSecond(cache.lowerRangeId, elementId)
} }
async put(originalEntity: SomeEntity): Promise<void> { async put(originalEntity: SomeEntity): Promise<void> {
const entity = clone(originalEntity) const entity = clone(originalEntity)
const typeRef = entity._type const typeRef = entity._type
const typeModel = await resolveTypeReference(typeRef) const typeModel = await resolveTypeReference(typeRef)
let { listId, elementId } = expandId(originalEntity._id)
elementId = ensureBase64Ext(typeModel, elementId)
switch (typeModel.type) { switch (typeModel.type) {
case TypeId.Element: case TypeId.Element:
const elementEntity = entity as ElementEntity const elementEntity = entity as ElementEntity
this.addElementEntity(elementEntity._type, elementEntity._id, elementEntity) this.addElementEntity(elementEntity._type, elementId, elementEntity)
break break
case TypeId.ListElement: case TypeId.ListElement:
const listElementEntity = entity as ListElementEntity const listElementEntity = entity as ListElementEntity
const listElementTypeRef = typeRef as TypeRef<ListElementEntity> const listElementTypeRef = typeRef as TypeRef<ListElementEntity>
await this.putListElement(listElementEntity, listElementTypeRef) listId = listId as Id
await this.putListElement(listElementTypeRef, listId, elementId, listElementEntity)
break break
case TypeId.BlobElement: case TypeId.BlobElement:
const blobElementEntity = entity as BlobElementEntity const blobElementEntity = entity as BlobElementEntity
const blobTypeRef = typeRef as TypeRef<BlobElementEntity> const blobTypeRef = typeRef as TypeRef<BlobElementEntity>
await this.putBlobElement(blobElementEntity, blobTypeRef) listId = listId as Id
await this.putBlobElement(blobTypeRef, listId, elementId, blobElementEntity)
break break
default: default:
throw new ProgrammingError("must be a persistent type") throw new ProgrammingError("must be a persistent type")
} }
} }
private async putBlobElement(entity: BlobElementEntity, typeRef: TypeRef<BlobElementEntity>) { private async putBlobElement(typeRef: TypeRef<BlobElementEntity>, listId: Id, elementId: Id, entity: BlobElementEntity) {
const listId = getListId(entity)
const elementId = getElementId(entity)
const cache = this.blobEntities.get(typeRefToPath(typeRef))?.get(listId) const cache = this.blobEntities.get(typeRefToPath(typeRef))?.get(listId)
if (cache == null) { if (cache == null) {
// first element in this list // first element in this list
@ -147,9 +155,8 @@ export class EphemeralCacheStorage implements CacheStorage {
} }
} }
private async putListElement(entity: ListElementEntity, typeRef: TypeRef<ListElementEntity>) { /** prcondition: elementId is converted to base64ext if necessary */
const listId = getListId(entity) private async putListElement(typeRef: TypeRef<ListElementEntity>, listId: Id, elementId: Id, entity: ListElementEntity) {
const elementId = getElementId(entity)
const cache = this.lists.get(typeRefToPath(typeRef))?.get(listId) const cache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
if (cache == null) { if (cache == null) {
// first element in this list // first element in this list
@ -164,12 +171,14 @@ export class EphemeralCacheStorage implements CacheStorage {
// if the element already exists in the cache, overwrite it // if the element already exists in the cache, overwrite it
// add new element to existing list if necessary // add new element to existing list if necessary
cache.elements.set(elementId, entity) cache.elements.set(elementId, entity)
if (await this.isElementIdInCacheRange(typeRef, listId, elementId)) { const typeModel = await resolveTypeReference(typeRef)
if (await this.isElementIdInCacheRange(typeRef, listId, customIdToBase64Url(typeModel, elementId))) {
this.insertIntoRange(cache.allRange, elementId) this.insertIntoRange(cache.allRange, elementId)
} }
} }
} }
/** precondition: elementId is converted to base64ext if necessary */
private insertIntoRange(allRange: Array<Id>, elementId: Id) { private insertIntoRange(allRange: Array<Id>, elementId: Id) {
for (let i = 0; i < allRange.length; i++) { for (let i = 0; i < allRange.length; i++) {
const rangeElement = allRange[i] const rangeElement = allRange[i]
@ -184,7 +193,10 @@ export class EphemeralCacheStorage implements CacheStorage {
allRange.push(elementId) allRange.push(elementId)
} }
async provideFromRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, start: Id, count: number, reverse: boolean): Promise<T[]> { async provideFromRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, startElementId: Id, count: number, reverse: boolean): Promise<T[]> {
const typeModel = await resolveTypeReference(typeRef)
startElementId = ensureBase64Ext(typeModel, startElementId)
const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId) const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
if (listCache == null) { if (listCache == null) {
@ -196,14 +208,14 @@ export class EphemeralCacheStorage implements CacheStorage {
if (reverse) { if (reverse) {
let i let i
for (i = range.length - 1; i >= 0; i--) { for (i = range.length - 1; i >= 0; i--) {
if (firstBiggerThanSecond(start, range[i])) { if (firstBiggerThanSecond(startElementId, range[i])) {
break break
} }
} }
if (i >= 0) { if (i >= 0) {
let startIndex = i + 1 - count let startIndex = i + 1 - count
if (startIndex < 0) { if (startIndex < 0) {
// start index may be negative if more elements have been requested than available when getting elements reverse. // startElementId index may be negative if more elements have been requested than available when getting elements reverse.
startIndex = 0 startIndex = 0
} }
ids = range.slice(startIndex, i + 1) ids = range.slice(startIndex, i + 1)
@ -212,7 +224,7 @@ export class EphemeralCacheStorage implements CacheStorage {
ids = [] ids = []
} }
} else { } else {
const i = range.findIndex((id) => firstBiggerThanSecond(id, start)) const i = range.findIndex((id) => firstBiggerThanSecond(id, startElementId))
ids = range.slice(i, i + count) ids = range.slice(i, i + count)
} }
let result: T[] = [] let result: T[] = []
@ -225,6 +237,9 @@ export class EphemeralCacheStorage implements CacheStorage {
async provideMultiple<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, elementIds: Id[]): Promise<Array<T>> { async provideMultiple<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, elementIds: Id[]): Promise<Array<T>> {
const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId) const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
const typeModel = await resolveTypeReference(typeRef)
elementIds = elementIds.map((el) => ensureBase64Ext(typeModel, el))
if (listCache == null) { if (listCache == null) {
return [] return []
} }
@ -242,23 +257,31 @@ export class EphemeralCacheStorage implements CacheStorage {
return null return null
} }
return { lower: listCache.lowerRangeId, upper: listCache.upperRangeId } const typeModel = await resolveTypeReference(typeRef)
return {
lower: customIdToBase64Url(typeModel, listCache.lowerRangeId),
upper: customIdToBase64Url(typeModel, listCache.upperRangeId),
}
} }
async setUpperRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, id: Id): Promise<void> { async setUpperRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, upperId: Id): Promise<void> {
const typeModel = await resolveTypeReference(typeRef)
upperId = ensureBase64Ext(typeModel, upperId)
const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId) const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
if (listCache == null) { if (listCache == null) {
throw new Error("list does not exist") throw new Error("list does not exist")
} }
listCache.upperRangeId = id listCache.upperRangeId = upperId
} }
async setLowerRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, id: Id): Promise<void> { async setLowerRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, lowerId: Id): Promise<void> {
const typeModel = await resolveTypeReference(typeRef)
lowerId = ensureBase64Ext(typeModel, lowerId)
const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId) const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
if (listCache == null) { if (listCache == null) {
throw new Error("list does not exist") throw new Error("list does not exist")
} }
listCache.lowerRangeId = id listCache.lowerRangeId = lowerId
} }
/** /**
@ -269,6 +292,10 @@ export class EphemeralCacheStorage implements CacheStorage {
* @param upper * @param upper
*/ */
async setNewRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, lower: Id, upper: Id): Promise<void> { async setNewRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, lower: Id, upper: Id): Promise<void> {
const typeModel = await resolveTypeReference(typeRef)
lower = ensureBase64Ext(typeModel, lower)
upper = ensureBase64Ext(typeModel, upper)
const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId) const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
if (listCache == null) { if (listCache == null) {
getFromMap(this.lists, typeRefToPath(typeRef), () => new Map()).set(listId, { getFromMap(this.lists, typeRefToPath(typeRef), () => new Map()).set(listId, {
@ -285,7 +312,15 @@ export class EphemeralCacheStorage implements CacheStorage {
} }
async getIdsInRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Array<Id>> { async getIdsInRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Array<Id>> {
return this.lists.get(typeRefToPath(typeRef))?.get(listId)?.allRange ?? [] const typeModel = await resolveTypeReference(typeRef)
return (
this.lists
.get(typeRefToPath(typeRef))
?.get(listId)
?.allRange.map((elementId) => {
return customIdToBase64Url(typeModel, elementId)
}) ?? []
)
} }
async getLastBatchIdForGroup(groupId: Id): Promise<Id | null> { async getLastBatchIdForGroup(groupId: Id): Promise<Id | null> {