feat: improve idb perf (#14159)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Performance**
* Optimized database operations through improved batch processing to
accelerate data retrieval, updates, and deletion operations for better
efficiency.

* **Reliability**
* Enhanced transaction durability handling to strengthen data
consistency and ensure more reliable persistence of database changes and
updates.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-12-27 08:22:37 +08:00
committed by GitHub
parent 4eed92cebf
commit 78949044ec
3 changed files with 104 additions and 34 deletions

View File

@@ -1,9 +1,5 @@
import { share } from '../../connection';
import {
type BlobRecord,
BlobStorageBase,
type ListedBlobRecord,
} from '../../storage';
import { type BlobRecord, BlobStorageBase } from '../../storage';
import { IDBConnection, type IDBConnectionOptions } from './db';
export class IndexedDBBlobStorage extends BlobStorageBase {
@@ -36,7 +32,9 @@ export class IndexedDBBlobStorage extends BlobStorageBase {
}
override async set(blob: BlobRecord) {
const trx = this.db.transaction(['blobs', 'blobData'], 'readwrite');
const trx = this.db.transaction(['blobs', 'blobData'], 'readwrite', {
durability: 'relaxed',
});
await trx.objectStore('blobs').put({
key: blob.key,
mime: blob.mime,
@@ -52,11 +50,15 @@ export class IndexedDBBlobStorage extends BlobStorageBase {
override async delete(key: string, permanently: boolean) {
if (permanently) {
const trx = this.db.transaction(['blobs', 'blobData'], 'readwrite');
const trx = this.db.transaction(['blobs', 'blobData'], 'readwrite', {
durability: 'relaxed',
});
await trx.objectStore('blobs').delete(key);
await trx.objectStore('blobData').delete(key);
} else {
const trx = this.db.transaction('blobs', 'readwrite');
const trx = this.db.transaction('blobs', 'readwrite', {
durability: 'relaxed',
});
const blob = await trx.store.get(key);
if (blob) {
await trx.store.put({
@@ -68,29 +70,37 @@ export class IndexedDBBlobStorage extends BlobStorageBase {
}
override async release() {
const trx = this.db.transaction(['blobs', 'blobData'], 'readwrite');
const trx = this.db.transaction(['blobs', 'blobData'], 'readwrite', {
durability: 'relaxed',
});
const it = trx.objectStore('blobs').iterate();
const store = trx.objectStore('blobs');
const getAllRecords = store.getAllRecords?.bind(store);
const blobs =
typeof getAllRecords === 'function'
? (await getAllRecords()).map(record => record.value)
: await store.getAll();
for await (const item of it) {
if (item.value.deletedAt) {
await item.delete();
await trx.objectStore('blobData').delete(item.value.key);
}
}
const deleted = blobs.filter(blob => blob.deletedAt);
await Promise.all(
deleted.map(blob =>
Promise.all([
store.delete(blob.key),
trx.objectStore('blobData').delete(blob.key),
])
)
);
}
override async list() {
const trx = this.db.transaction('blobs', 'readonly');
const it = trx.store.iterate();
const getAllRecords = trx.store.getAllRecords?.bind(trx.store);
const blobs =
typeof getAllRecords === 'function'
? (await getAllRecords()).map(record => record.value)
: await trx.store.getAll();
const blobs: ListedBlobRecord[] = [];
for await (const item of it) {
if (!item.value.deletedAt) {
blobs.push(item.value);
}
}
return blobs;
return blobs.filter(blob => !blob.deletedAt);
}
}

View File

@@ -4,6 +4,38 @@ import { AutoReconnectConnection } from '../../connection';
import type { SpaceType } from '../../utils/universal-id';
import { type DocStorageSchema, migrator } from './schema';
declare module 'idb' {
interface IDBPObjectStore {
getAllRecords?(
query?: IDBValidKey | IDBKeyRange | null,
count?: number | { direction?: IDBCursorDirection; count?: number }
): Promise<IDBRecord[]>;
}
interface IDBPIndex {
getAllRecords?(
query?: IDBValidKey | IDBKeyRange | null,
count?: number | { direction?: IDBCursorDirection; count?: number }
): Promise<IDBRecord[]>;
}
interface IDBObjectStore {
getAllRecords?(
query?: IDBValidKey | IDBKeyRange | null,
count?: number | { direction?: IDBCursorDirection; count?: number }
): Promise<IDBRecord[]>;
}
interface IDBIndex {
getAllRecords?(
query?: IDBValidKey | IDBKeyRange | null,
count?: number | { direction?: IDBCursorDirection; count?: number }
): Promise<IDBRecord[]>;
}
interface IDBRecord {
key: IDBValidKey;
primaryKey: IDBValidKey;
value: any;
}
}
export interface IDBConnectionOptions {
flavour: string;
type: SpaceType;

View File

@@ -37,7 +37,9 @@ export class IndexedDBDocStorage extends DocStorageBase<IDBConnectionOptions> {
while (true) {
try {
const trx = this.db.transaction(['updates', 'clocks'], 'readwrite');
const trx = this.db.transaction(['updates', 'clocks'], 'readwrite', {
durability: 'relaxed',
});
await trx.objectStore('updates').add({
...update,
@@ -103,15 +105,15 @@ export class IndexedDBDocStorage extends DocStorageBase<IDBConnectionOptions> {
override async deleteDoc(docId: string) {
const trx = this.db.transaction(
['snapshots', 'updates', 'clocks'],
'readwrite'
'readwrite',
{ durability: 'relaxed' }
);
const idx = trx.objectStore('updates').index('docId');
const iter = idx.iterate(IDBKeyRange.only(docId));
const updates = trx.objectStore('updates');
const idx = updates.index('docId');
const keys = await idx.getAllKeys(IDBKeyRange.only(docId));
for await (const { value } of iter) {
await trx.objectStore('updates').delete([value.docId, value.createdAt]);
}
await Promise.all(keys.map(key => updates.delete(key)));
await trx.objectStore('snapshots').delete(docId);
await trx.objectStore('clocks').delete(docId);
@@ -120,6 +122,18 @@ export class IndexedDBDocStorage extends DocStorageBase<IDBConnectionOptions> {
override async getDocTimestamps(after: Date = new Date(0)) {
const trx = this.db.transaction('clocks', 'readonly');
const getAllRecords = trx.store.getAllRecords?.bind(trx.store);
if (typeof getAllRecords === 'function') {
const records = await getAllRecords();
return records.reduce((ret, cur) => {
if (cur.value.timestamp > after) {
ret[cur.value.docId] = cur.value.timestamp;
}
return ret;
}, {} as DocClocks);
}
const clocks = await trx.store.getAll();
return clocks.reduce((ret, cur) => {
@@ -157,7 +171,19 @@ export class IndexedDBDocStorage extends DocStorageBase<IDBConnectionOptions> {
protected override async getDocUpdates(docId: string): Promise<DocRecord[]> {
const trx = this.db.transaction('updates', 'readonly');
const updates = await trx.store.index('docId').getAll(docId);
const idx = trx.store.index('docId');
const getAllRecords = idx.getAllRecords?.bind(idx);
if (typeof getAllRecords === 'function') {
const records = await getAllRecords(IDBKeyRange.only(docId));
return records.map(record => ({
docId,
bin: record.value.bin,
timestamp: record.value.createdAt,
}));
}
const updates = await idx.getAll(docId);
return updates.map(update => ({
docId,
@@ -170,7 +196,9 @@ export class IndexedDBDocStorage extends DocStorageBase<IDBConnectionOptions> {
docId: string,
updates: DocRecord[]
): Promise<number> {
const trx = this.db.transaction('updates', 'readwrite');
const trx = this.db.transaction('updates', 'readwrite', {
durability: 'relaxed',
});
await Promise.all(
updates.map(update => trx.store.delete([docId, update.timestamp]))