2023-01-17 12:21:29 +01:00
import { ElementEntity , ListElementEntity , SomeEntity , TypeModel } from "../../common/EntityTypes.js"
2024-08-26 14:05:56 +02:00
import { CUSTOM_MIN_ID , firstBiggerThanSecond , GENERATED_MIN_ID , getElementId } from "../../common/utils/EntityUtils.js"
2022-12-27 15:37:40 +01:00
import { CacheStorage , expandId , ExposedCacheStorage , LastUpdateTime } from "../rest/DefaultEntityRestCache.js"
2022-01-12 14:43:01 +01:00
import * as cborg from "cborg"
2022-12-27 15:37:40 +01:00
import { EncodeOptions , Token , Type } from "cborg"
2023-08-16 10:47:09 +02:00
import {
assert ,
assertNotNull ,
2024-08-23 13:00:37 +02:00
base64ExtToBase64 ,
base64ToBase64Ext ,
base64ToBase64Url ,
base64UrlToBase64 ,
2023-08-16 10:47:09 +02:00
getTypeId ,
groupByAndMapUniquely ,
mapNullable ,
splitInChunks ,
TypeRef ,
} from "@tutao/tutanota-utils"
2022-12-27 15:37:40 +01:00
import { isDesktop , isOfflineStorageAvailable , isTest } from "../../common/Env.js"
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
import { modelInfos , resolveTypeReference } from "../../common/EntityFunctions.js"
2022-12-27 15:37:40 +01:00
import { DateProvider } from "../../common/DateProvider.js"
2024-01-08 17:14:09 +01:00
import { TokenOrNestedTokens } from "cborg/interface"
2024-12-17 15:46:05 +01:00
import { CalendarEventTypeRef , MailTypeRef } from "../../entities/tutanota/TypeRefs.js"
2022-12-27 15:37:40 +01:00
import { OfflineStorageMigrator } from "./OfflineStorageMigrator.js"
2024-12-17 15:46:05 +01:00
import { CustomCacheHandlerMap , CustomCalendarEventCacheHandler , CustomMailEventCacheHandler } from "../rest/CustomCacheHandler.js"
2022-12-27 15:37:40 +01:00
import { EntityRestClient } from "../rest/EntityRestClient.js"
import { InterWindowEventFacadeSendDispatcher } from "../../../native/common/generatedipc/InterWindowEventFacadeSendDispatcher.js"
import { SqlCipherFacade } from "../../../native/common/generatedipc/SqlCipherFacade.js"
2024-03-11 16:25:43 +01:00
import { FormattedQuery , SqlValue , TaggedSqlValue , untagSqlObject } from "./SqlValue.js"
2024-08-20 14:31:57 +02:00
import { AssociationType , Cardinality , Type as TypeId , ValueType } from "../../common/EntityConstants.js"
2023-01-04 10:54:28 +01:00
import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
2024-03-11 16:25:43 +01:00
import { sql , SqlFragment } from "./Sql.js"
2022-01-12 14:43:01 +01:00
2023-08-16 10:47:09 +02:00
/ * *
* this is the value of SQLITE_MAX_VARIABLE_NUMBER in sqlite3 . c
* it may change if the sqlite version is updated .
* * /
const MAX_SAFE_SQL_VARS = 32766
2022-01-12 14:43:01 +01:00
function dateEncoder ( data : Date , typ : string , options : EncodeOptions ) : TokenOrNestedTokens | null {
2022-10-13 13:29:00 +02:00
const time = data . getTime ( )
2022-01-12 14:43:01 +01:00
return [
// https://datatracker.ietf.org/doc/rfc8943/
new Token ( Type . tag , 100 ) ,
2022-12-27 15:37:40 +01:00
new Token ( time < 0 ? Type.negint : Type.uint , time ) ,
2022-01-12 14:43:01 +01:00
]
}
function dateDecoder ( bytes : number ) : Date {
return new Date ( bytes )
}
2022-12-27 15:37:40 +01:00
export const customTypeEncoders : { [ typeName : string ] : typeof dateEncoder } = Object . freeze ( {
Date : dateEncoder ,
2022-01-12 14:43:01 +01:00
} )
type TypeDecoder = ( _ : any ) = > any
export const customTypeDecoders : Array < TypeDecoder > = ( ( ) = > {
const tags : Array < TypeDecoder > = [ ]
tags [ 100 ] = dateDecoder
return tags
} ) ( )
2022-10-21 15:53:39 +02:00
/ * *
* For each of these keys we track the current version in the database .
* The keys are different model versions ( because we need to migrate the data with certain model version changes ) and "offline" key which is used to track
* migrations that are needed for other reasons e . g . if DB structure changes or if we need to invalidate some tables .
* /
export type VersionMetadataBaseKey = keyof typeof modelInfos | "offline"
2022-04-20 10:39:52 +02:00
2022-10-21 15:53:39 +02:00
type VersionMetadataEntries = {
2022-04-20 10:39:52 +02:00
// Yes this is cursed, give me a break
2022-10-21 15:53:39 +02:00
[ P in VersionMetadataBaseKey as ` ${ P } -version ` ] : number
2022-04-20 10:39:52 +02:00
}
2022-10-21 15:53:39 +02:00
export interface OfflineDbMeta extends VersionMetadataEntries {
2022-12-27 15:37:40 +01:00
lastUpdateTime : number
2022-04-20 10:39:52 +02:00
timeRangeDays : number
}
2022-08-11 16:38:53 +02:00
const TableDefinitions = Object . freeze ( {
2022-10-21 15:53:39 +02:00
// plus ownerGroup added in a migration
2022-12-27 15:37:40 +01:00
list_entities :
"type TEXT NOT NULL, listId TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, listId, elementId)" ,
2022-10-21 15:53:39 +02:00
// plus ownerGroup added in a migration
element_entities : "type TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, elementId)" ,
2022-08-11 16:38:53 +02:00
ranges : "type TEXT NOT NULL, listId TEXT NOT NULL, lower TEXT NOT NULL, upper TEXT NOT NULL, PRIMARY KEY (type, listId)" ,
lastUpdateBatchIdPerGroupId : "groupId TEXT NOT NULL, batchId TEXT NOT NULL, PRIMARY KEY (groupId)" ,
metadata : "key TEXT NOT NULL, value BLOB, PRIMARY KEY (key)" ,
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
blob_element_entities :
"type TEXT NOT NULL, listId TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, listId, elementId)" ,
2022-08-11 16:38:53 +02:00
} as const )
2024-08-23 13:00:37 +02:00
type Range = { lower : Id ; upper : Id }
2022-08-11 16:38:53 +02:00
2022-10-21 15:53:39 +02:00
export interface OfflineStorageInitArgs {
2022-12-27 15:37:40 +01:00
userId : Id
databaseKey : Uint8Array
timeRangeDays : number | null
2022-10-21 15:53:39 +02:00
forceNewDatabase : boolean
}
2022-05-12 17:06:57 +02:00
export class OfflineStorage implements CacheStorage , ExposedCacheStorage {
2022-07-04 14:55:17 +02:00
private customCacheHandler : CustomCacheHandlerMap | null = null
2022-10-21 15:53:39 +02:00
private userId : Id | null = null
2022-11-28 17:38:17 +01:00
private timeRangeDays : number | null = null
2022-02-10 16:32:47 +01:00
2022-01-12 14:43:01 +01:00
constructor (
2022-08-11 16:38:53 +02:00
private readonly sqlCipherFacade : SqlCipherFacade ,
2022-07-20 15:28:38 +02:00
private readonly interWindowEventSender : InterWindowEventFacadeSendDispatcher ,
2022-04-20 10:39:52 +02:00
private readonly dateProvider : DateProvider ,
2022-05-17 17:40:44 +02:00
private readonly migrator : OfflineStorageMigrator ,
2024-08-15 16:11:44 +02:00
private readonly cleaner : OfflineStorageCleaner ,
2022-01-12 14:43:01 +01:00
) {
2022-02-10 16:32:47 +01:00
assert ( isOfflineStorageAvailable ( ) || isTest ( ) , "Offline storage is not available." )
}
2022-05-17 17:40:44 +02:00
/ * *
* @return { boolean } whether the database was newly created or not
* /
2022-12-27 15:37:40 +01:00
async init ( { userId , databaseKey , timeRangeDays , forceNewDatabase } : OfflineStorageInitArgs ) : Promise < boolean > {
2022-10-21 15:53:39 +02:00
this . userId = userId
2022-11-28 17:38:17 +01:00
this . timeRangeDays = timeRangeDays
2022-07-20 15:28:38 +02:00
if ( forceNewDatabase ) {
2022-08-11 16:38:53 +02:00
if ( isDesktop ( ) ) {
await this . interWindowEventSender . localUserDataInvalidated ( userId )
}
await this . sqlCipherFacade . deleteDb ( userId )
2022-07-20 15:28:38 +02:00
}
2022-11-28 17:38:17 +01:00
// We open database here, and it is closed in the native side when the window is closed or the page is reloaded
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . openDb ( userId , databaseKey )
await this . createTables ( )
2024-02-01 13:54:19 +01:00
2023-01-04 10:54:28 +01:00
try {
await this . migrator . migrate ( this , this . sqlCipherFacade )
} catch ( e ) {
2023-01-04 16:54:58 +01:00
if ( e instanceof OutOfSyncError ) {
2023-02-02 14:17:02 +01:00
console . warn ( "Offline db is out of sync!" , e )
2023-01-04 16:54:58 +01:00
await this . recreateDbFile ( userId , databaseKey )
2023-02-02 14:17:02 +01:00
await this . migrator . migrate ( this , this . sqlCipherFacade )
2023-01-04 16:54:58 +01:00
} else {
2023-01-04 10:54:28 +01:00
throw e
}
}
2022-05-17 17:40:44 +02:00
// if nothing is written here, it means it's a new database
2024-01-22 10:42:11 +01:00
return ( await this . getLastUpdateTime ( ) ) . type === "never"
2023-01-04 10:54:28 +01:00
}
2022-11-28 17:38:17 +01:00
2023-01-04 10:54:28 +01:00
private async recreateDbFile ( userId : string , databaseKey : Uint8Array ) : Promise < void > {
console . log ( ` recreating DB file for userId ${ userId } ` )
await this . sqlCipherFacade . closeDb ( )
await this . sqlCipherFacade . deleteDb ( userId )
await this . sqlCipherFacade . openDb ( userId , databaseKey )
await this . createTables ( )
2022-02-10 16:32:47 +01:00
}
2022-08-15 14:22:44 +02:00
/ * *
* currently , we close DBs from the native side ( mainly on things like reload and on android ' s onDestroy )
* /
2022-07-20 15:28:38 +02:00
async deinit() {
2022-10-21 15:53:39 +02:00
this . userId = null
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . closeDb ( )
2022-07-20 15:28:38 +02:00
}
2022-08-11 16:38:53 +02:00
async deleteIfExists ( typeRef : TypeRef < SomeEntity > , listId : Id | null , elementId : Id ) : Promise < void > {
const type = getTypeId ( typeRef )
2025-02-05 13:00:51 +01:00
const typeModel : TypeModel = await resolveTypeReference ( typeRef )
const encodedElementId = ensureBase64Ext ( typeModel , elementId )
2023-08-15 16:43:50 +02:00
let formattedQuery
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
switch ( typeModel . type ) {
2023-01-12 16:48:28 +01:00
case TypeId . Element :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` DELETE
FROM element_entities
WHERE type = $ { type }
2025-02-05 13:00:51 +01:00
AND elementId = $ { encodedElementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . ListElement :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` DELETE
FROM list_entities
WHERE type = $ { type }
AND listId = $ { listId }
2025-02-05 13:00:51 +01:00
AND elementId = $ { encodedElementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . BlobElement :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` DELETE
FROM blob_element_entities
WHERE type = $ { type }
AND listId = $ { listId }
2025-02-05 13:00:51 +01:00
AND elementId = $ { encodedElementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
default :
throw new Error ( "must be a persistent type" )
2022-02-10 16:32:47 +01:00
}
2023-08-15 16:43:50 +02:00
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
2022-01-12 14:43:01 +01:00
}
2023-02-02 17:21:34 +01:00
async deleteAllOfType ( typeRef : TypeRef < SomeEntity > ) : Promise < void > {
const type = getTypeId ( typeRef )
let typeModel : TypeModel
2024-07-01 14:54:13 +02:00
typeModel = await resolveTypeReference ( typeRef )
2023-08-15 16:43:50 +02:00
let formattedQuery
2023-02-02 17:21:34 +01:00
switch ( typeModel . type ) {
case TypeId . Element :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` DELETE
FROM element_entities
WHERE type = $ { type } `
2023-02-02 17:21:34 +01:00
break
case TypeId . ListElement :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` DELETE
FROM list_entities
WHERE type = $ { type } `
2023-08-15 16:43:50 +02:00
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
2023-03-29 14:46:04 +02:00
await this . deleteAllRangesForType ( type )
return
2023-02-02 17:21:34 +01:00
case TypeId . BlobElement :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` DELETE
FROM blob_element_entities
WHERE type = $ { type } `
2023-02-02 17:21:34 +01:00
break
default :
throw new Error ( "must be a persistent type" )
}
2023-08-15 16:43:50 +02:00
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
2023-02-02 17:21:34 +01:00
}
2025-02-25 10:48:12 +01:00
/ * *
* Remove all ranges ( and only ranges , without associated data ) for the specified { @param typeRef } .
* Does not lock the ranges .
* /
async deleteAllRangesOfType ( typeRef : TypeRef < SomeEntity > ) : Promise < void > {
const type = getTypeId ( typeRef )
await this . deleteAllRangesForType ( type )
}
2023-03-29 14:46:04 +02:00
private async deleteAllRangesForType ( type : string ) : Promise < void > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` DELETE
FROM ranges
WHERE type = $ { type } `
2023-03-29 14:46:04 +02:00
await this . sqlCipherFacade . run ( query , params )
}
2022-08-11 16:38:53 +02:00
async get < T extends SomeEntity > ( typeRef : TypeRef < T > , listId : Id | null , elementId : Id ) : Promise < T | null > {
const type = getTypeId ( typeRef )
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
const typeModel = await resolveTypeReference ( typeRef )
2025-02-05 13:00:51 +01:00
const encodedElementId = ensureBase64Ext ( typeModel , elementId )
2023-08-15 16:43:50 +02:00
let formattedQuery
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
switch ( typeModel . type ) {
2023-01-12 16:48:28 +01:00
case TypeId . Element :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` SELECT entity
from element_entities
WHERE type = $ { type }
2025-02-05 13:00:51 +01:00
AND elementId = $ { encodedElementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . ListElement :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` SELECT entity
from list_entities
WHERE type = $ { type }
AND listId = $ { listId }
2025-02-05 13:00:51 +01:00
AND elementId = $ { encodedElementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . BlobElement :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` SELECT entity
from blob_element_entities
WHERE type = $ { type }
AND listId = $ { listId }
2025-02-05 13:00:51 +01:00
AND elementId = $ { encodedElementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
default :
throw new Error ( "must be a persistent type" )
2022-08-11 16:38:53 +02:00
}
2023-08-15 16:43:50 +02:00
const result = await this . sqlCipherFacade . get ( formattedQuery . query , formattedQuery . params )
2024-09-04 16:19:59 +02:00
return result ? . entity ? await this . deserialize ( typeRef , result . entity . value as Uint8Array ) : null
2022-01-12 14:43:01 +01:00
}
2024-08-07 08:38:58 +02:00
async provideMultiple < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , elementIds : Id [ ] ) : Promise < Array < T > > {
if ( elementIds . length === 0 ) return [ ]
2024-08-23 13:00:37 +02:00
const typeModel = await resolveTypeReference ( typeRef )
2025-02-05 13:00:51 +01:00
const encodedElementIds = elementIds . map ( ( elementId ) = > ensureBase64Ext ( typeModel , elementId ) )
2024-08-23 13:00:37 +02:00
2024-08-07 08:38:58 +02:00
const type = getTypeId ( typeRef )
const serializedList : ReadonlyArray < Record < string , TaggedSqlValue > > = await this . allChunked (
MAX_SAFE_SQL_VARS - 2 ,
2025-02-05 13:00:51 +01:00
encodedElementIds ,
2024-12-17 15:46:05 +01:00
( c ) = > sql ` SELECT entity
FROM list_entities
WHERE type = $ { type }
AND listId = $ { listId }
AND elementId IN $ { paramList ( c ) } ` ,
2024-08-07 08:38:58 +02:00
)
2024-09-04 16:19:59 +02:00
return await this . deserializeList (
2024-08-07 08:38:58 +02:00
typeRef ,
serializedList . map ( ( r ) = > r . entity . value as Uint8Array ) ,
)
}
2022-01-12 14:43:01 +01:00
async getIdsInRange < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id ) : Promise < Array < Id > > {
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2024-08-28 17:20:32 +02:00
const typeModel = await resolveTypeReference ( typeRef )
2024-08-23 13:00:37 +02:00
const range = await this . getRange ( typeRef , listId )
2022-08-11 16:38:53 +02:00
if ( range == null ) {
throw new Error ( ` no range exists for ${ type } and list ${ listId } ` )
}
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT elementId
FROM list_entities
WHERE type = $ { type }
AND listId = $ { listId }
AND ( elementId = $ { range . lower }
OR $ { firstIdBigger ( "elementId" , range . lower ) } )
AND NOT ( $ { firstIdBigger ( "elementId" , range . upper ) } ) `
2022-08-11 16:38:53 +02:00
const rows = await this . sqlCipherFacade . all ( query , params )
2024-08-28 17:20:32 +02:00
return rows . map ( ( row ) = > customIdToBase64Url ( typeModel , row . elementId . value as string ) )
2022-01-12 14:43:01 +01:00
}
2024-08-23 13:00:37 +02:00
/ * * d o n ' t u s e t h i s i n t e r n a l l y i n t h i s c l a s s , u s e O f f l i n e S t o r a g e : : g e t R a n g e i n s t e a d . O f f l i n e S t o r a g e i s
* using converted custom IDs internally which is undone when using this to access the range .
* /
2022-08-11 16:38:53 +02:00
async getRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id ) : Promise < Range | null > {
2024-08-23 13:00:37 +02:00
let range = await this . getRange ( typeRef , listId )
if ( range == null ) return range
2024-08-28 17:47:42 +02:00
const typeModel = await resolveTypeReference ( typeRef )
2024-08-23 13:00:37 +02:00
return {
lower : customIdToBase64Url ( typeModel , range . lower ) ,
upper : customIdToBase64Url ( typeModel , range . upper ) ,
}
2022-01-12 14:43:01 +01:00
}
2024-08-23 13:00:37 +02:00
async isElementIdInCacheRange < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , elementId : Id ) : Promise < boolean > {
const typeModel = await resolveTypeReference ( typeRef )
2025-02-05 13:00:51 +01:00
const encodedElementId = ensureBase64Ext ( typeModel , elementId )
2024-08-23 13:00:37 +02:00
const range = await this . getRange ( typeRef , listId )
2025-02-05 13:00:51 +01:00
return range != null && ! firstBiggerThanSecond ( encodedElementId , range . upper ) && ! firstBiggerThanSecond ( range . lower , encodedElementId )
2022-01-12 14:43:01 +01:00
}
async provideFromRange < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , start : Id , count : number , reverse : boolean ) : Promise < T [ ] > {
2024-08-23 13:00:37 +02:00
const typeModel = await resolveTypeReference ( typeRef )
2025-02-05 13:00:51 +01:00
const encodedStartId = ensureBase64Ext ( typeModel , start )
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2023-08-15 16:43:50 +02:00
let formattedQuery
2022-08-11 16:38:53 +02:00
if ( reverse ) {
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` SELECT entity
FROM list_entities
WHERE type = $ { type }
AND listId = $ { listId }
2025-02-05 13:00:51 +01:00
AND $ { firstIdBigger ( encodedStartId , "elementId" ) }
2024-12-17 15:46:05 +01:00
ORDER BY LENGTH ( elementId ) DESC , elementId DESC LIMIT $ { count } `
2022-08-11 16:38:53 +02:00
} else {
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` SELECT entity
FROM list_entities
WHERE type = $ { type }
AND listId = $ { listId }
2025-02-05 13:00:51 +01:00
AND $ { firstIdBigger ( "elementId" , encodedStartId ) }
2024-12-17 15:46:05 +01:00
ORDER BY LENGTH ( elementId ) ASC , elementId ASC LIMIT $ { count } `
2022-08-11 16:38:53 +02:00
}
2023-08-15 16:43:50 +02:00
const { query , params } = formattedQuery
2022-08-11 16:38:53 +02:00
const serializedList : ReadonlyArray < Record < string , TaggedSqlValue > > = await this . sqlCipherFacade . all ( query , params )
2024-09-04 16:19:59 +02:00
return await this . deserializeList (
2022-12-27 15:37:40 +01:00
typeRef ,
serializedList . map ( ( r ) = > r . entity . value as Uint8Array ) ,
)
2022-01-12 14:43:01 +01:00
}
async put ( originalEntity : SomeEntity ) : Promise < void > {
const serializedEntity = this . serialize ( originalEntity )
2025-02-05 13:00:51 +01:00
const { listId , elementId } = expandId ( originalEntity . _id )
2022-08-11 16:38:53 +02:00
const type = getTypeId ( originalEntity . _type )
2022-10-21 15:53:39 +02:00
const ownerGroup = originalEntity . _ownerGroup
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
const typeModel = await resolveTypeReference ( originalEntity . _type )
2025-02-05 13:00:51 +01:00
const encodedElementId = ensureBase64Ext ( typeModel , elementId )
2023-08-15 16:43:50 +02:00
let formattedQuery : FormattedQuery
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
switch ( typeModel . type ) {
2023-01-12 16:48:28 +01:00
case TypeId . Element :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` INSERT
OR REPLACE INTO element_entities ( type , elementId , ownerGroup , entity ) VALUES (
$ { type } ,
2025-02-05 13:00:51 +01:00
$ { encodedElementId } ,
2024-12-17 15:46:05 +01:00
$ { ownerGroup } ,
$ { serializedEntity }
) `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . ListElement :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` INSERT
OR REPLACE INTO list_entities ( type , listId , elementId , ownerGroup , entity ) VALUES (
$ { type } ,
$ { listId } ,
2025-02-05 13:00:51 +01:00
$ { encodedElementId } ,
2024-12-17 15:46:05 +01:00
$ { ownerGroup } ,
$ { serializedEntity }
) `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . BlobElement :
2024-12-17 15:46:05 +01:00
formattedQuery = sql ` INSERT
OR REPLACE INTO blob_element_entities ( type , listId , elementId , ownerGroup , entity ) VALUES (
$ { type } ,
$ { listId } ,
2025-02-05 13:00:51 +01:00
$ { encodedElementId } ,
2024-12-17 15:46:05 +01:00
$ { ownerGroup } ,
$ { serializedEntity }
) `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
default :
throw new Error ( "must be a persistent type" )
}
2023-08-15 16:43:50 +02:00
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
2022-01-12 14:43:01 +01:00
}
2024-08-23 13:00:37 +02:00
async setLowerRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , lowerId : Id ) : Promise < void > {
lowerId = ensureBase64Ext ( await resolveTypeReference ( typeRef ) , lowerId )
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` UPDATE ranges
SET lower = $ { lowerId }
WHERE type = $ { type }
AND listId = $ { listId } `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
2024-08-23 13:00:37 +02:00
async setUpperRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , upperId : Id ) : Promise < void > {
upperId = ensureBase64Ext ( await resolveTypeReference ( typeRef ) , upperId )
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` UPDATE ranges
SET upper = $ { upperId }
WHERE type = $ { type }
AND listId = $ { listId } `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
async setNewRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , lower : Id , upper : Id ) : Promise < void > {
2024-08-23 13:00:37 +02:00
const typeModel = await resolveTypeReference ( typeRef )
lower = ensureBase64Ext ( typeModel , lower )
upper = ensureBase64Ext ( typeModel , upper )
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` INSERT
OR REPLACE INTO ranges VALUES (
$ { type } ,
$ { listId } ,
$ { lower } ,
$ { upper }
) `
2022-08-11 16:38:53 +02:00
return this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async getLastBatchIdForGroup ( groupId : Id ) : Promise < Id | null > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT batchId
from lastUpdateBatchIdPerGroupId
WHERE groupId = $ { groupId } `
2022-12-27 15:37:40 +01:00
const row = ( await this . sqlCipherFacade . get ( query , params ) ) as { batchId : TaggedSqlValue } | null
2022-08-11 16:38:53 +02:00
return ( row ? . batchId ? . value ? ? null ) as Id | null
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async putLastBatchIdForGroup ( groupId : Id , batchId : Id ) : Promise < void > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` INSERT
OR REPLACE INTO lastUpdateBatchIdPerGroupId VALUES (
$ { groupId } ,
$ { batchId }
) `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
2022-09-07 17:09:25 +02:00
async getLastUpdateTime ( ) : Promise < LastUpdateTime > {
const time = await this . getMetadata ( "lastUpdateTime" )
2022-12-27 15:37:40 +01:00
return time ? { type : "recorded" , time } : { type : "never" }
2022-04-20 10:39:52 +02:00
}
2022-08-11 16:38:53 +02:00
async putLastUpdateTime ( ms : number ) : Promise < void > {
await this . putMetadata ( "lastUpdateTime" , ms )
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async purgeStorage ( ) : Promise < void > {
for ( let name of Object . keys ( TableDefinitions ) ) {
2024-12-17 15:46:05 +01:00
await this . sqlCipherFacade . run (
` DELETE
2025-01-03 10:16:07 +01:00
FROM $ { name } ` ,
2024-12-17 15:46:05 +01:00
[ ] ,
)
2022-08-11 16:38:53 +02:00
}
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async deleteRange ( typeRef : TypeRef < unknown > , listId : string ) : Promise < void > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` DELETE
FROM ranges
WHERE type = $ { getTypeId ( typeRef ) }
AND listId = $ { listId } `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
2024-09-04 16:51:12 +02:00
async getRawListElementsOfType ( typeRef : TypeRef < ListElementEntity > ) : Promise < Array < ListElementEntity > > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT entity
from list_entities
WHERE type = $ { getTypeId ( typeRef ) } `
2022-12-27 15:37:40 +01:00
const items = ( await this . sqlCipherFacade . all ( query , params ) ) ? ? [ ]
2024-09-04 16:51:12 +02:00
return items . map ( ( item ) = > this . decodeCborEntity ( item . entity . value as Uint8Array ) as Record < string , unknown > & ListElementEntity )
}
async getRawElementsOfType ( typeRef : TypeRef < ElementEntity > ) : Promise < Array < ElementEntity > > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT entity
from element_entities
WHERE type = $ { getTypeId ( typeRef ) } `
2024-09-04 16:51:12 +02:00
const items = ( await this . sqlCipherFacade . all ( query , params ) ) ? ? [ ]
return items . map ( ( item ) = > this . decodeCborEntity ( item . entity . value as Uint8Array ) as Record < string , unknown > & ElementEntity )
2022-02-10 16:32:47 +01:00
}
2024-08-15 16:11:44 +02:00
async getElementsOfType < T extends ElementEntity > ( typeRef : TypeRef < T > ) : Promise < Array < T > > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT entity
from element_entities
WHERE type = $ { getTypeId ( typeRef ) } `
2022-12-27 15:37:40 +01:00
const items = ( await this . sqlCipherFacade . all ( query , params ) ) ? ? [ ]
2024-09-04 16:19:59 +02:00
return await this . deserializeList (
2022-12-27 15:37:40 +01:00
typeRef ,
items . map ( ( row ) = > row . entity . value as Uint8Array ) ,
)
2022-06-16 17:23:48 +02:00
}
2022-08-11 16:38:53 +02:00
async getWholeList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id ) : Promise < Array < T > > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT entity
FROM list_entities
WHERE type = $ { getTypeId ( typeRef ) }
AND listId = $ { listId } `
2022-12-27 15:37:40 +01:00
const items = ( await this . sqlCipherFacade . all ( query , params ) ) ? ? [ ]
2024-09-04 16:19:59 +02:00
return await this . deserializeList (
2022-12-27 15:37:40 +01:00
typeRef ,
items . map ( ( row ) = > row . entity . value as Uint8Array ) ,
)
2022-02-10 16:32:47 +01:00
}
2022-08-11 16:38:53 +02:00
async dumpMetadata ( ) : Promise < Partial < OfflineDbMeta > > {
const query = "SELECT * from metadata"
2022-12-27 15:37:40 +01:00
const stored = ( await this . sqlCipherFacade . all ( query , [ ] ) ) . map ( ( row ) = > [ row . key . value as string , row . value . value as Uint8Array ] as const )
2022-08-11 16:38:53 +02:00
return Object . fromEntries ( stored . map ( ( [ key , value ] ) = > [ key , cborg . decode ( value ) ] ) ) as OfflineDbMeta
2022-01-12 14:43:01 +01:00
}
2022-10-21 15:53:39 +02:00
async setStoredModelVersion ( model : VersionMetadataBaseKey , version : number ) {
2022-08-11 16:38:53 +02:00
return this . putMetadata ( ` ${ model } -version ` , version )
}
getCustomCacheHandlerMap ( entityRestClient : EntityRestClient ) : CustomCacheHandlerMap {
if ( this . customCacheHandler == null ) {
2024-12-17 15:46:05 +01:00
this . customCacheHandler = new CustomCacheHandlerMap (
{
ref : CalendarEventTypeRef ,
handler : new CustomCalendarEventCacheHandler ( entityRestClient ) ,
} ,
{ ref : MailTypeRef , handler : new CustomMailEventCacheHandler ( ) } ,
)
2022-08-11 16:38:53 +02:00
}
return this . customCacheHandler
}
2022-10-21 15:53:39 +02:00
getUserId ( ) : Id {
return assertNotNull ( this . userId , "No user id, not initialized?" )
}
async deleteAllOwnedBy ( owner : Id ) : Promise < void > {
{
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` DELETE
FROM element_entities
WHERE ownerGroup = $ { owner } `
2022-10-21 15:53:39 +02:00
await this . sqlCipherFacade . run ( query , params )
}
{
2022-11-02 11:37:37 +01:00
// first, check which list Ids contain entities owned by the lost group
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT listId, type
FROM list_entities
WHERE ownerGroup = $ { owner } `
2022-11-02 11:37:37 +01:00
const rangeRows = await this . sqlCipherFacade . all ( query , params )
2022-12-27 15:37:40 +01:00
const rows = rangeRows . map ( ( row ) = > untagSqlObject ( row ) as { listId : string ; type : string } )
const listIdsByType : Map < string , Set < Id > > = groupByAndMapUniquely (
rows ,
( row ) = > row . type ,
( row ) = > row . listId ,
)
2023-08-16 10:47:09 +02:00
// delete the ranges for those listIds
2022-11-02 11:37:37 +01:00
for ( const [ type , listIds ] of listIdsByType . entries ( ) ) {
2023-08-16 10:47:09 +02:00
// this particular query uses one other SQL var for the type.
const safeChunkSize = MAX_SAFE_SQL_VARS - 1
const listIdArr = Array . from ( listIds )
2024-12-17 15:46:05 +01:00
await this . runChunked (
safeChunkSize ,
listIdArr ,
( c ) = > sql ` DELETE
2025-01-03 10:16:07 +01:00
FROM ranges
WHERE type = $ { type }
AND listId IN $ { paramList ( c ) } ` ,
2024-12-17 15:46:05 +01:00
)
await this . runChunked (
safeChunkSize ,
listIdArr ,
( c ) = > sql ` DELETE
2025-01-03 10:16:07 +01:00
FROM list_entities
WHERE type = $ { type }
AND listId IN $ { paramList ( c ) } ` ,
2024-12-17 15:46:05 +01:00
)
2022-11-02 11:37:37 +01:00
}
2022-11-03 11:04:26 +01:00
}
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
{
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` DELETE
FROM blob_element_entities
WHERE ownerGroup = $ { owner } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
await this . sqlCipherFacade . run ( query , params )
}
2022-11-03 11:04:26 +01:00
{
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` DELETE
FROM lastUpdateBatchIdPerGroupId
WHERE groupId = $ { owner } `
2022-11-03 11:04:26 +01:00
await this . sqlCipherFacade . run ( query , params )
2022-10-21 15:53:39 +02:00
}
}
2024-09-09 11:57:46 +02:00
async deleteWholeList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id ) : Promise < void > {
await this . lockRangesDbAccess ( listId )
await this . deleteRange ( typeRef , listId )
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` DELETE
FROM list_entities
WHERE listId = $ { listId } `
2024-09-09 11:57:46 +02:00
await this . sqlCipherFacade . run ( query , params )
await this . unlockRangesDbAccess ( listId )
}
2022-08-11 16:38:53 +02:00
private async putMetadata < K extends keyof OfflineDbMeta > ( key : K , value : OfflineDbMeta [ K ] ) : Promise < void > {
2023-02-07 10:18:22 +01:00
let encodedValue
try {
encodedValue = cborg . encode ( value )
} catch ( e ) {
console . log ( "[OfflineStorage] failed to encode metadata for key" , key , "with value" , value )
throw e
}
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` INSERT
OR REPLACE INTO metadata VALUES (
$ { key } ,
$ { encodedValue }
) `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
}
private async getMetadata < K extends keyof OfflineDbMeta > ( key : K ) : Promise < OfflineDbMeta [ K ] | null > {
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT value
from metadata
WHERE key = $ { key } `
2022-08-11 16:38:53 +02:00
const encoded = await this . sqlCipherFacade . get ( query , params )
return encoded && cborg . decode ( encoded . value . value as Uint8Array )
2022-01-12 14:43:01 +01:00
}
2022-02-10 16:32:47 +01:00
2022-05-06 14:39:31 +02:00
/ * *
2022-11-28 17:38:17 +01:00
* Clear out unneeded data from the offline database ( i . e . trash and spam lists , old data ) .
* This will be called after login ( CachePostLoginActions . ts ) to ensure fast login time .
* @param timeRangeDays : the maximum age of days that mails should be to be kept in the database . if null , will use a default value
* @param userId id of the current user . default , last stored userId
2022-05-06 14:39:31 +02:00
* /
2022-11-28 17:38:17 +01:00
async clearExcludedData ( timeRangeDays : number | null = this . timeRangeDays , userId : Id = this . getUserId ( ) ) : Promise < void > {
2024-08-15 16:11:44 +02:00
await this . cleaner . cleanOfflineDb ( this , timeRangeDays , userId , this . dateProvider . now ( ) )
2022-08-11 16:38:53 +02:00
}
private async createTables() {
for ( let [ name , definition ] of Object . entries ( TableDefinitions ) ) {
2024-12-17 15:46:05 +01:00
await this . sqlCipherFacade . run (
` CREATE TABLE IF NOT EXISTS ${ name }
2025-01-03 10:16:07 +01:00
(
$ { definition }
) ` ,
2024-12-17 15:46:05 +01:00
[ ] ,
)
2022-08-11 16:38:53 +02:00
}
}
2025-02-05 13:00:51 +01:00
private async getRange ( typeRef : TypeRef < ElementEntity | ListElementEntity > , listId : Id ) : Promise < Range | null > {
2024-08-23 13:00:37 +02:00
const type = getTypeId ( typeRef )
2024-12-17 15:46:05 +01:00
const { query , params } = sql ` SELECT upper, lower
FROM ranges
WHERE type = $ { type }
AND listId = $ { listId } `
2022-12-27 15:37:40 +01:00
const row = ( await this . sqlCipherFacade . get ( query , params ) ) ? ? null
2024-08-23 13:00:37 +02:00
2022-08-11 16:38:53 +02:00
return mapNullable ( row , untagSqlObject ) as Range | null
2022-04-20 10:39:52 +02:00
}
2024-08-15 16:11:44 +02:00
async deleteIn ( typeRef : TypeRef < unknown > , listId : Id | null , elementIds : Id [ ] ) : Promise < void > {
2023-09-18 12:07:46 +02:00
if ( elementIds . length === 0 ) return
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
const typeModel = await resolveTypeReference ( typeRef )
2025-02-05 13:00:51 +01:00
const encodedElementIds = elementIds . map ( ( elementIds ) = > ensureBase64Ext ( typeModel , elementIds ) )
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
switch ( typeModel . type ) {
2023-01-12 16:48:28 +01:00
case TypeId . Element :
2023-08-16 10:47:09 +02:00
return await this . runChunked (
MAX_SAFE_SQL_VARS - 1 ,
2025-02-05 13:00:51 +01:00
encodedElementIds ,
2024-12-17 15:46:05 +01:00
( c ) = > sql ` DELETE
FROM element_entities
WHERE type = $ { getTypeId ( typeRef ) }
AND elementId IN $ { paramList ( c ) } ` ,
2023-08-16 10:47:09 +02:00
)
2023-01-12 16:48:28 +01:00
case TypeId . ListElement :
2023-08-16 10:47:09 +02:00
return await this . runChunked (
MAX_SAFE_SQL_VARS - 2 ,
2025-02-05 13:00:51 +01:00
encodedElementIds ,
2024-12-17 15:46:05 +01:00
( c ) = > sql ` DELETE
FROM list_entities
WHERE type = $ { getTypeId ( typeRef ) }
AND listId = $ { listId }
AND elementId IN $ { paramList ( c ) } ` ,
2023-08-16 10:47:09 +02:00
)
2023-01-12 16:48:28 +01:00
case TypeId . BlobElement :
2023-08-16 10:47:09 +02:00
return await this . runChunked (
MAX_SAFE_SQL_VARS - 2 ,
2025-02-05 13:00:51 +01:00
encodedElementIds ,
2024-12-17 15:46:05 +01:00
( c ) = > sql ` DELETE
FROM blob_element_entities
WHERE type = $ { getTypeId ( typeRef ) }
AND listId = $ { listId }
AND elementId IN $ { paramList ( c ) } ` ,
2023-08-16 10:47:09 +02:00
)
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
default :
throw new Error ( "must be a persistent type" )
}
2022-04-20 10:39:52 +02:00
}
2022-11-30 17:15:08 +01:00
/ * *
* We want to lock the access to the "ranges" db when updating / reading the
2024-08-26 15:07:37 +02:00
* offline available mail list / mailset ranges for each mail list ( referenced using the listId ) .
* @param listId the mail list or mail set entry list that we want to lock
2022-11-30 17:15:08 +01:00
* /
async lockRangesDbAccess ( listId : Id ) {
await this . sqlCipherFacade . lockRangesDbAccess ( listId )
}
/ * *
* This is the counterpart to the function "lockRangesDbAccess(listId)" .
* @param listId the mail list that we want to unlock
* /
async unlockRangesDbAccess ( listId : Id ) {
await this . sqlCipherFacade . unlockRangesDbAccess ( listId )
}
2025-02-19 11:57:08 +01:00
async updateRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , rawCutoffId : Id ) : Promise < void > {
2024-08-23 13:00:37 +02:00
const typeModel = await resolveTypeReference ( typeRef )
const isCustomId = isCustomIdType ( typeModel )
2025-02-05 13:00:51 +01:00
const encodedCutoffId = ensureBase64Ext ( typeModel , rawCutoffId )
2022-04-20 10:39:52 +02:00
2024-08-23 13:00:37 +02:00
const range = await this . getRange ( typeRef , listId )
2022-04-20 10:39:52 +02:00
if ( range == null ) {
return
}
2025-02-19 11:57:08 +01:00
// If the range for a given list is complete from the beginning (starts at GENERATED_MIN_ID or CUSTOM_MIN_ID), then we only want to actually modify the
2022-04-20 10:39:52 +02:00
// saved range if we would be removing elements from the list, in order to not lose the information that the range is complete in storage.
// So we have to check how old the oldest element in said range is. If it is newer than cutoffId, then we will not modify the range,
// otherwise we will just modify it normally
2024-08-23 13:00:37 +02:00
const expectedMinId = isCustomId ? CUSTOM_MIN_ID : GENERATED_MIN_ID
if ( range . lower === expectedMinId ) {
const entities = await this . provideFromRange ( typeRef , listId , expectedMinId , 1 , false )
2022-04-20 10:39:52 +02:00
const id = mapNullable ( entities [ 0 ] , getElementId )
2025-02-19 11:57:08 +01:00
// !!IMPORTANT!!
// Ids on entities with a customId are always base64Url encoded,
// !!however ids for entities with a customId used to QUERY the offline database
// MUST always be base64Ext encoded
// Therefore, we need to compare against the rawCutoffId here!
const rangeWontBeModified = id != null && ( firstBiggerThanSecond ( id , rawCutoffId ) || id === rawCutoffId )
2022-04-20 10:39:52 +02:00
if ( rangeWontBeModified ) {
return
}
}
2025-02-05 13:00:51 +01:00
if ( firstBiggerThanSecond ( encodedCutoffId , range . lower ) ) {
2022-04-20 10:39:52 +02:00
// If the upper id of the range is below the cutoff, then the entire range will be deleted from the storage
// so we just delete the range as well
// Otherwise, we only want to modify
2025-02-05 13:00:51 +01:00
if ( firstBiggerThanSecond ( encodedCutoffId , range . upper ) ) {
2022-08-11 16:38:53 +02:00
await this . deleteRange ( typeRef , listId )
2022-04-20 10:39:52 +02:00
} else {
2024-08-26 15:07:37 +02:00
await this . setLowerRangeForList ( typeRef , listId , rawCutoffId )
2022-04-20 10:39:52 +02:00
}
}
}
2022-08-11 16:38:53 +02:00
private serialize ( originalEntity : SomeEntity ) : Uint8Array {
2023-02-07 10:18:22 +01:00
try {
return cborg . encode ( originalEntity , { typeEncoders : customTypeEncoders } )
} catch ( e ) {
console . log ( "[OfflineStorage] failed to encode entity of type" , originalEntity . _type , "with id" , originalEntity . _id )
throw e
}
2022-04-20 10:39:52 +02:00
}
2024-08-20 14:31:57 +02:00
/ * *
* Convert the type from CBOR representation to the runtime type
* /
2024-09-20 16:10:37 +02:00
private async deserialize < T extends SomeEntity > ( typeRef : TypeRef < T > , loaded : Uint8Array ) : Promise < T | null > {
let deserialized
try {
deserialized = this . decodeCborEntity ( loaded )
} catch ( e ) {
console . log ( e )
console . log ( ` Error with CBOR decode. Trying to decode (of type: ${ typeof loaded } ): ${ loaded } ` )
return null
}
2024-08-20 14:31:57 +02:00
const typeModel = await resolveTypeReference ( typeRef )
return ( await this . fixupTypeRefs ( typeModel , deserialized ) ) as T
}
2024-09-04 16:51:12 +02:00
private decodeCborEntity ( loaded : Uint8Array ) : Record < string , unknown > {
return cborg . decode ( loaded , { tags : customTypeDecoders } )
}
2024-08-20 14:31:57 +02:00
private async fixupTypeRefs ( typeModel : TypeModel , deserialized : any ) : Promise < unknown > {
// TypeRef cannot be deserialized back automatically. We could write a codec for it but we don't actually need to store it so we just "patch" it.
2022-08-11 16:38:53 +02:00
// Some places rely on TypeRef being a class and not a plain object.
2024-08-20 14:31:57 +02:00
// We also have to update all aggregates, recursively.
deserialized . _type = new TypeRef ( typeModel . app , typeModel . name )
for ( const [ associationName , associationModel ] of Object . entries ( typeModel . associations ) ) {
if ( associationModel . type === AssociationType . Aggregation ) {
const aggregateTypeRef = new TypeRef ( associationModel . dependency ? ? typeModel . app , associationModel . refType )
const aggregateTypeModel = await resolveTypeReference ( aggregateTypeRef )
switch ( associationModel . cardinality ) {
case Cardinality . One :
2025-01-03 10:16:07 +01:00
case Cardinality . ZeroOrOne : {
2024-08-20 14:31:57 +02:00
const aggregate = deserialized [ associationName ]
if ( aggregate ) {
await this . fixupTypeRefs ( aggregateTypeModel , aggregate )
}
break
2025-01-03 10:16:07 +01:00
}
case Cardinality . Any : {
2024-08-20 14:31:57 +02:00
const aggregateList = deserialized [ associationName ]
for ( const aggregate of aggregateList ) {
await this . fixupTypeRefs ( aggregateTypeModel , aggregate )
}
break
2025-01-03 10:16:07 +01:00
}
2024-08-20 14:31:57 +02:00
}
}
}
2022-08-11 16:38:53 +02:00
return deserialized
2022-04-20 10:39:52 +02:00
}
2022-05-17 17:40:44 +02:00
2024-09-04 16:19:59 +02:00
private async deserializeList < T extends SomeEntity > ( typeRef : TypeRef < T > , loaded : Array < Uint8Array > ) : Promise < Array < T > > {
// manually reimplementing promiseMap to make sure we don't hit the scheduler since there's nothing actually async happening
2024-09-04 16:51:12 +02:00
const result : Array < T > = [ ]
2024-09-04 16:19:59 +02:00
for ( const entity of loaded ) {
2024-09-20 16:10:37 +02:00
const deserialized = await this . deserialize ( typeRef , entity )
if ( deserialized != null ) {
result . push ( deserialized )
}
2024-09-04 16:19:59 +02:00
}
return result
2022-05-17 17:40:44 +02:00
}
2023-08-16 10:47:09 +02:00
/ * *
* convenience method to run a potentially too large query over several chunks .
* chunkSize must be chosen such that the total number of SQL variables in the final query does not exceed MAX_SAFE_SQL_VARS
* * /
private async runChunked ( chunkSize : number , originalList : SqlValue [ ] , formatter : ( chunk : SqlValue [ ] ) = > FormattedQuery ) : Promise < void > {
for ( const chunk of splitInChunks ( chunkSize , originalList ) ) {
const formattedQuery = formatter ( chunk )
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
}
}
2024-08-07 08:38:58 +02:00
/ * *
* convenience method to execute a potentially too large query over several chunks .
* chunkSize must be chosen such that the total number of SQL variables in the final query does not exceed MAX_SAFE_SQL_VARS
* * /
private async allChunked (
chunkSize : number ,
originalList : SqlValue [ ] ,
formatter : ( chunk : SqlValue [ ] ) = > FormattedQuery ,
) : Promise < Array < Record < string , TaggedSqlValue > > > {
const result : Array < Record < string , TaggedSqlValue > > = [ ]
for ( const chunk of splitInChunks ( chunkSize , originalList ) ) {
const formattedQuery = formatter ( chunk )
result . push ( . . . ( await this . sqlCipherFacade . all ( formattedQuery . query , formattedQuery . params ) ) )
}
return result
}
2022-08-11 16:38:53 +02:00
}
2022-07-04 14:55:17 +02:00
2022-08-11 16:38:53 +02:00
/ *
2023-08-16 10:47:09 +02:00
* used to automatically create the right amount of SQL variables for selecting ids from a dynamic list .
* must be used within sql ` <query> ` template string to inline the logic into the query .
*
* It is very important that params is kept to a size such that the total amount of SQL variables is
* less than MAX_SAFE_SQL_VARS .
2022-08-11 16:38:53 +02:00
* /
function paramList ( params : SqlValue [ ] ) : SqlFragment {
2022-12-27 15:37:40 +01:00
const qs = params . map ( ( ) = > "?" ) . join ( "," )
2022-08-11 16:38:53 +02:00
return new SqlFragment ( ` ( ${ qs } ) ` , params )
}
/ * *
2022-08-15 14:22:44 +02:00
* comparison to select ids that are bigger or smaller than a parameter id
2023-08-16 10:47:09 +02:00
* must be used within sql ` <query> ` template string to inline the logic into the query .
*
* will always insert 3 constants and 3 SQL variables into the query .
2022-08-11 16:38:53 +02:00
* /
function firstIdBigger ( . . . args : [ string , "elementId" ] | [ "elementId" , string ] ) : SqlFragment {
let [ l , r ] : [ string , string ] = args
let v
if ( l === "elementId" ) {
v = r
r = "?"
} else {
v = l
l = "?"
}
2022-12-27 15:37:40 +01:00
return new SqlFragment ( ` (CASE WHEN length( ${ l } ) > length( ${ r } ) THEN 1 WHEN length( ${ l } ) < length( ${ r } ) THEN 0 ELSE ${ l } > ${ r } END) ` , [ v , v , v ] )
2022-08-11 16:38:53 +02:00
}
2024-08-23 13:00:37 +02:00
export function isCustomIdType ( typeModel : TypeModel ) : boolean {
return typeModel . values . _id . type === ValueType . CustomId
}
/ * *
* We store customIds as base64ext in the db to make them sortable , but we get them as base64url from the server .
* /
export function ensureBase64Ext ( typeModel : TypeModel , elementId : Id ) : Id {
if ( isCustomIdType ( typeModel ) ) {
return base64ToBase64Ext ( base64UrlToBase64 ( elementId ) )
}
return elementId
}
export function customIdToBase64Url ( typeModel : TypeModel , elementId : Id ) : Id {
if ( isCustomIdType ( typeModel ) ) {
return base64ToBase64Url ( base64ExtToBase64 ( elementId ) )
}
return elementId
}
2024-08-15 16:11:44 +02:00
export interface OfflineStorageCleaner {
cleanOfflineDb ( offlineStorage : OfflineStorage , timeRangeDays : number | null , userId : Id , now : number ) : Promise < void >
}