import { Subject, Observable } from 'rxjs'; import { now, ensureNotFalsy, } from '../utils/index.ts'; import type { RxStorageInstance, RxStorageChangeEvent, RxDocumentData, BulkWriteRow, RxStorageBulkWriteResponse, RxStorageQueryResult, RxJsonSchema, RxStorageInstanceCreationParams, EventBulk, StringKeys, RxStorageDefaultCheckpoint, CategorizeBulkWriteRowsOutput, RxStorageCountResult, PreparedQuery } from '../../types/index.d.ts'; import type { DexieSettings, DexieStorageInternals } from '../../types/plugins/dexie.d.ts'; import { RxStorageDexie } from './rx-storage-dexie.ts'; import { attachmentObjectId, closeDexieDb, fromStorageToDexie, getDexieDbWithTables, getDocsInDb, RX_STORAGE_NAME_DEXIE } from './dexie-helper.ts'; import { dexieCount, dexieQuery } from './dexie-query.ts'; import { getPrimaryFieldOfPrimaryKey } from '../../rx-schema-helper.ts'; import { categorizeBulkWriteRows, flatCloneDocWithMeta } from '../../rx-storage-helper.ts'; import { addRxStorageMultiInstanceSupport } from '../../rx-storage-multiinstance.ts'; import { newRxError } from '../../rx-error.ts'; let instanceId = now(); export class RxStorageInstanceDexie implements RxStorageInstance< RxDocType, DexieStorageInternals, DexieSettings, RxStorageDefaultCheckpoint > { public readonly primaryPath: StringKeys>; private changes$: Subject>, RxStorageDefaultCheckpoint>> = new Subject(); public readonly instanceId = instanceId++; public closed?: Promise; constructor( public readonly storage: RxStorageDexie, public readonly databaseName: string, public readonly collectionName: string, public readonly schema: Readonly>>, public readonly internals: DexieStorageInternals, public readonly options: Readonly, public readonly settings: DexieSettings, public readonly devMode: boolean ) { this.primaryPath = getPrimaryFieldOfPrimaryKey(this.schema.primaryKey); } async bulkWrite( documentWrites: BulkWriteRow[], context: string ): Promise> { ensureNotClosed(this); /** * Check some assumptions to ensure RxDB * does not call the storage with an invalid write. */ documentWrites.forEach(row => { // ensure revision is set if ( !row.document._rev || ( row.previous && !row.previous._rev ) ) { throw newRxError('SNH', { args: { row } }); } }); const state = await this.internals; const ret: RxStorageBulkWriteResponse = { error: [] }; /** * Some storages might add any _meta fields * internally. To ensure RxDB can work with that in the * test suite, we add a random field here. */ if (this.devMode) { documentWrites = documentWrites.map(row => { const doc = flatCloneDocWithMeta(row.document); return { previous: row.previous, document: doc } }) } const documentKeys: string[] = documentWrites.map(writeRow => writeRow.document[this.primaryPath] as any); let categorized: CategorizeBulkWriteRowsOutput | undefined; await state.dexieDb.transaction( 'rw', state.dexieTable, state.dexieAttachmentsTable, async () => { const docsInDbMap = new Map>(); const docsInDbWithInternals = await getDocsInDb(this.internals, documentKeys); docsInDbWithInternals.forEach(docWithDexieInternals => { const doc = docWithDexieInternals; if (doc) { docsInDbMap.set((doc as any)[this.primaryPath], doc as any); } return doc; }); categorized = categorizeBulkWriteRows( this, this.primaryPath as any, docsInDbMap, documentWrites, context ); ret.error = categorized.errors; /** * Batch up the database operations * so we can later run them in bulk. */ let bulkPutDocs: any[] = []; categorized.bulkInsertDocs.forEach(row => { bulkPutDocs.push(row.document); }); categorized.bulkUpdateDocs.forEach(row => { bulkPutDocs.push(row.document); }); bulkPutDocs = bulkPutDocs.map(d => fromStorageToDexie(state.booleanIndexes, d)); if (bulkPutDocs.length > 0) { await state.dexieTable.bulkPut(bulkPutDocs); } // handle attachments const putAttachments: { id: string, data: Blob }[] = []; categorized.attachmentsAdd.forEach(attachment => { putAttachments.push({ id: attachmentObjectId(attachment.documentId, attachment.attachmentId), data: attachment.attachmentData.data }); }); categorized.attachmentsUpdate.forEach(attachment => { putAttachments.push({ id: attachmentObjectId(attachment.documentId, attachment.attachmentId), data: attachment.attachmentData.data }); }); await state.dexieAttachmentsTable.bulkPut(putAttachments); await state.dexieAttachmentsTable.bulkDelete( categorized.attachmentsRemove.map(attachment => attachmentObjectId(attachment.documentId, attachment.attachmentId)) ); }); categorized = ensureNotFalsy(categorized); if (categorized.eventBulk.events.length > 0) { const lastState = ensureNotFalsy(categorized.newestRow).document; categorized.eventBulk.checkpoint = { id: lastState[this.primaryPath], lwt: lastState._meta.lwt }; this.changes$.next(categorized.eventBulk); } return ret; } async findDocumentsById( ids: string[], deleted: boolean ): Promise[]> { ensureNotClosed(this); const state = await this.internals; const ret: RxDocumentData[] = []; await state.dexieDb.transaction( 'r', state.dexieTable, async () => { const docsInDb = await getDocsInDb(this.internals, ids); docsInDb.forEach(documentInDb => { if ( documentInDb && (!documentInDb._deleted || deleted) ) { ret.push(documentInDb); } }); }); return ret; } query(preparedQuery: PreparedQuery): Promise> { ensureNotClosed(this); return dexieQuery( this, preparedQuery ); } async count( preparedQuery: PreparedQuery ): Promise { if (preparedQuery.queryPlan.selectorSatisfiedByIndex) { const result = await dexieCount(this, preparedQuery); return { count: result, mode: 'fast' }; } else { const result = await dexieQuery(this, preparedQuery); return { count: result.documents.length, mode: 'slow' }; } } changeStream(): Observable>, RxStorageDefaultCheckpoint>> { ensureNotClosed(this); return this.changes$.asObservable(); } async cleanup(minimumDeletedTime: number): Promise { ensureNotClosed(this); const state = await this.internals; await state.dexieDb.transaction( 'rw', state.dexieTable, async () => { const maxDeletionTime = now() - minimumDeletedTime; const toRemove = await state.dexieTable .where('_meta.lwt') .below(maxDeletionTime) .toArray(); const removeIds: string[] = []; toRemove.forEach((doc: any) => { if (doc._deleted === '1') { removeIds.push(doc[this.primaryPath]); } }); await state.dexieTable.bulkDelete(removeIds); } ); return true; } async getAttachmentData(documentId: string, attachmentId: string, _digest: string): Promise { ensureNotClosed(this); const state = await this.internals; const id = attachmentObjectId(documentId, attachmentId); return await state.dexieDb.transaction( 'r', state.dexieAttachmentsTable, async () => { const attachment = await state.dexieAttachmentsTable.get(id); if (attachment) { return attachment.data; } else { throw new Error('attachment missing documentId: ' + documentId + ' attachmentId: ' + attachmentId); } }); } async remove(): Promise { ensureNotClosed(this); const state = await this.internals; await state.dexieTable.clear() return this.close(); } close(): Promise { if (this.closed) { return this.closed; } this.closed = (async () => { this.changes$.complete(); await closeDexieDb(this.internals); })(); return this.closed; } } export async function createDexieStorageInstance( storage: RxStorageDexie, params: RxStorageInstanceCreationParams, settings: DexieSettings ): Promise> { const internals = getDexieDbWithTables( params.databaseName, params.collectionName, settings, params.schema ); const instance = new RxStorageInstanceDexie( storage, params.databaseName, params.collectionName, params.schema, internals, params.options, settings, params.devMode ); await addRxStorageMultiInstanceSupport( RX_STORAGE_NAME_DEXIE, params, instance ); return Promise.resolve(instance); } function ensureNotClosed( instance: RxStorageInstanceDexie ) { if (instance.closed) { throw new Error('RxStorageInstanceDexie is closed ' + instance.databaseName + '-' + instance.collectionName); } }