perf(electron): add index for updates (#6951)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/cd2e982a-f78a-4cc3-b090-ee4c0090e19d.png)

Above image shows the performance on querying a 20k rows of updates table, which is super slow at 150+ms. After adding index for doc_id the performance should be greatly improved.

After:
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/45ea4389-1833-4dc5-bd64-84d8c99cd647.png)

fix TOV-866
This commit is contained in:
pengx17 2024-05-16 06:30:53 +00:00
parent 37cb5b86f4
commit 27af9b4d1a
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
10 changed files with 443 additions and 342 deletions

View File

@ -37,10 +37,7 @@ class Doc implements DocType {
if (!apis?.db) {
throw new Error('sqlite datasource is not available');
}
const update = await apis.db.getDocAsUpdates(
this.workspaceId,
this.workspaceId === docId ? undefined : docId
);
const update = await apis.db.getDocAsUpdates(this.workspaceId, docId);
if (update) {
if (
@ -60,19 +57,18 @@ class Doc implements DocType {
if (!apis?.db) {
throw new Error('sqlite datasource is not available');
}
await apis.db.applyDocUpdate(
this.workspaceId,
data,
this.workspaceId === docId ? undefined : docId
);
await apis.db.applyDocUpdate(this.workspaceId, data, docId);
}
clear(): void | Promise<void> {
return;
}
del(): void | Promise<void> {
return;
async del(docId: string) {
if (!apis?.db) {
throw new Error('sqlite datasource is not available');
}
await apis.db.deleteDoc(this.workspaceId, docId);
}
}

View File

@ -2,16 +2,14 @@ import type { InsertRow } from '@affine/native';
import { SqliteConnection, ValidationResult } from '@affine/native';
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
import { applyGuidCompatibilityFix, migrateToLatest } from '../db/migration';
import { logger } from '../logger';
import { applyGuidCompatibilityFix, migrateToLatest } from './migration';
/**
* A base class for SQLite DB adapter that provides basic methods around updates & blobs
*/
export abstract class BaseSQLiteAdapter {
export class SQLiteAdapter {
db: SqliteConnection | null = null;
abstract role: string;
constructor(public readonly path: string) {}
async connectIfNeeded() {
@ -27,7 +25,7 @@ export abstract class BaseSQLiteAdapter {
await migrateToLatest(this.path, WorkspaceVersion.Surface);
}
await applyGuidCompatibilityFix(this.db);
logger.info(`[SQLiteAdapter:${this.role}]`, 'connected:', this.path);
logger.info(`[SQLiteAdapter]`, 'connected:', this.path);
}
return this.db;
}
@ -36,7 +34,7 @@ export abstract class BaseSQLiteAdapter {
const { db } = this;
this.db = null;
// log after close will sometimes crash the app when quitting
logger.info(`[SQLiteAdapter:${this.role}]`, 'destroyed:', this.path);
logger.info(`[SQLiteAdapter]`, 'destroyed:', this.path);
await db?.close();
}
@ -128,7 +126,7 @@ export abstract class BaseSQLiteAdapter {
const start = performance.now();
await this.db.insertUpdates(updates);
logger.debug(
`[SQLiteAdapter][${this.role}] addUpdateToSQLite`,
`[SQLiteAdapter] addUpdateToSQLite`,
'length:',
updates.length,
'docids',
@ -140,4 +138,41 @@ export abstract class BaseSQLiteAdapter {
logger.error('addUpdateToSQLite', this.path, error);
}
}
async deleteUpdates(docId?: string) {
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return;
}
await this.db.deleteUpdates(docId);
} catch (error) {
logger.error('deleteUpdates', error);
}
}
async getUpdatesCount(docId?: string) {
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return 0;
}
return await this.db.getUpdatesCount(docId);
} catch (error) {
logger.error('getUpdatesCount', error);
return 0;
}
}
async replaceUpdates(docId: string | null | undefined, updates: InsertRow[]) {
try {
if (!this.db) {
logger.warn(`${this.path} is not connected`);
return;
}
await this.db.replaceUpdates(docId, updates);
} catch (error) {
logger.error('replaceUpdates', error);
}
}
}

View File

@ -5,22 +5,21 @@ import { ensureSQLiteDB } from './ensure-db';
export * from './ensure-db';
export const dbHandlers = {
getDocAsUpdates: async (workspaceId: string, subdocId?: string) => {
getDocAsUpdates: async (workspaceId: string, subdocId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.getDocAsUpdates(subdocId);
},
applyDocUpdate: async (
workspaceId: string,
update: Uint8Array,
subdocId?: string
subdocId: string
) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.addUpdateToSQLite([
{
data: update,
docId: subdocId,
},
]);
return workspaceDB.addUpdateToSQLite(update, subdocId);
},
deleteDoc: async (workspaceId: string, subdocId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.deleteUpdate(subdocId);
},
addBlob: async (workspaceId: string, key: string, data: Uint8Array) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);

View File

@ -1,36 +1,43 @@
import type { InsertRow } from '@affine/native';
import { AsyncLock } from '@toeverything/infra';
import { Subject } from 'rxjs';
import { applyUpdate, Doc as YDoc } from 'yjs';
import { logger } from '../logger';
import { getWorkspaceMeta } from '../workspace/meta';
import { BaseSQLiteAdapter } from './base-db-adapter';
import { SQLiteAdapter } from './db-adapter';
import { mergeUpdate } from './merge-update';
const TRIM_SIZE = 500;
export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
role = 'primary';
export class WorkspaceSQLiteDB {
lock = new AsyncLock();
update$ = new Subject<void>();
adapter = new SQLiteAdapter(this.path);
constructor(
public override path: string,
public path: string,
public workspaceId: string
) {
super(path);
) {}
async transaction<T>(cb: () => Promise<T>): Promise<T> {
using _lock = await this.lock.acquire();
return await cb();
}
override async destroy() {
await super.destroy();
async destroy() {
await this.adapter.destroy();
// when db is closed, we can safely remove it from ensure-db list
this.update$.complete();
}
toDBDocId = (docId: string) => {
return this.workspaceId === docId ? undefined : docId;
};
getWorkspaceName = async () => {
const ydoc = new YDoc();
const updates = await this.getUpdates();
const updates = await this.adapter.getUpdates();
updates.forEach(update => {
applyUpdate(ydoc, update.data);
});
@ -38,44 +45,75 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
};
async init() {
const db = await super.connectIfNeeded();
const db = await this.adapter.connectIfNeeded();
await this.tryTrim();
return db;
}
async get(docId: string) {
return this.adapter.getUpdates(docId);
}
// getUpdates then encode
getDocAsUpdates = async (docId?: string) => {
const updates = await this.getUpdates(docId);
return mergeUpdate(updates.map(row => row.data));
getDocAsUpdates = async (docId: string) => {
const dbID = this.toDBDocId(docId);
const update = await this.tryTrim(dbID);
if (update) {
return update;
} else {
const updates = await this.adapter.getUpdates(dbID);
return mergeUpdate(updates.map(row => row.data));
}
};
override async addBlob(key: string, value: Uint8Array) {
async addBlob(key: string, value: Uint8Array) {
this.update$.next();
const res = await super.addBlob(key, value);
const res = await this.adapter.addBlob(key, value);
return res;
}
override async deleteBlob(key: string) {
this.update$.next();
await super.deleteBlob(key);
async getBlob(key: string) {
return this.adapter.getBlob(key);
}
override async addUpdateToSQLite(data: InsertRow[]) {
this.update$.next();
await super.addUpdateToSQLite(data);
async getBlobKeys() {
return this.adapter.getBlobKeys();
}
private readonly tryTrim = async (docId?: string) => {
const count = (await this.db?.getUpdatesCount(docId)) ?? 0;
async deleteBlob(key: string) {
this.update$.next();
await this.adapter.deleteBlob(key);
}
async addUpdateToSQLite(update: Uint8Array, subdocId: string) {
this.update$.next();
await this.adapter.addUpdateToSQLite([
{
data: update,
docId: this.toDBDocId(subdocId),
},
]);
}
async deleteUpdate(subdocId: string) {
this.update$.next();
await this.adapter.deleteUpdates(this.toDBDocId(subdocId));
}
private readonly tryTrim = async (dbID?: string) => {
const count = (await this.adapter?.getUpdatesCount(dbID)) ?? 0;
if (count > TRIM_SIZE) {
logger.debug(`trim ${this.workspaceId}:${docId} ${count}`);
const update = await this.getDocAsUpdates(docId);
if (update) {
const insertRows = [{ data: update, docId }];
await this.db?.replaceUpdates(docId, insertRows);
logger.debug(`trim ${this.workspaceId}:${docId} successfully`);
}
return await this.transaction(async () => {
logger.debug(`trim ${this.workspaceId}:${dbID} ${count}`);
const updates = await this.adapter.getUpdates(dbID);
const update = mergeUpdate(updates.map(row => row.data));
const insertRows = [{ data: update, dbID }];
await this.adapter?.replaceUpdates(dbID, insertRows);
logger.debug(`trim ${this.workspaceId}:${dbID} successfully`);
return update;
});
}
return null;
};
}

View File

@ -77,16 +77,16 @@ test('db should be destroyed when app quits', async () => {
const db0 = await ensureSQLiteDB(workspaceId);
const db1 = await ensureSQLiteDB(v4());
expect(db0.db).not.toBeNull();
expect(db1.db).not.toBeNull();
expect(db0.adapter).not.toBeNull();
expect(db1.adapter).not.toBeNull();
existProcess();
// wait the async `db.destroy()` to be called
await setTimeout(100);
expect(db0.db).toBeNull();
expect(db1.db).toBeNull();
expect(db0.adapter.db).toBeNull();
expect(db1.adapter.db).toBeNull();
});
test('db should be removed in db$Map after destroyed', async () => {

View File

@ -51,6 +51,6 @@ test('on destroy, check if resources have been released', async () => {
};
db.update$ = updateSub as any;
await db.destroy();
expect(db.db).toBe(null);
expect(db.adapter.db).toBe(null);
expect(updateSub.complete).toHaveBeenCalled();
});

View File

@ -9,7 +9,7 @@
"experimentalDecorators": true,
"types": ["node", "affine__env"],
"outDir": "lib",
"moduleResolution": "node",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"noImplicitOverride": true,
"paths": {

View File

@ -1,6 +1,5 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
export class SqliteConnection {
constructor(path: string)
connect(): Promise<void>
@ -9,6 +8,7 @@ export class SqliteConnection {
deleteBlob(key: string): Promise<void>
getBlobKeys(): Promise<Array<string>>
getUpdates(docId?: string | undefined | null): Promise<Array<UpdateRow>>
deleteUpdates(docId?: string | undefined | null): Promise<void>
getUpdatesCount(docId?: string | undefined | null): Promise<number>
getAllUpdates(): Promise<Array<UpdateRow>>
insertUpdates(updates: Array<InsertRow>): Promise<void>

View File

@ -2,14 +2,10 @@
/* eslint-disable */
/* auto-generated by NAPI-RS */
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
const { readFileSync } = require('fs')
let nativeBinding = null
let localFileExisted = false
let loadError = null
const loadErrors = []
const isMusl = () => {
let musl = false
@ -60,281 +56,281 @@ const isMuslFromChildProcess = () => {
}
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'affine.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./affine.android-arm64.node')
} else {
nativeBinding = require('@affine/native-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'affine.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./affine.android-arm-eabi.node')
} else {
nativeBinding = require('@affine/native-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
loadError = new Error(`Unsupported architecture on Android ${arch}`)
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, 'affine.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.win32-x64-msvc.node')
} else {
nativeBinding = require('@affine/native-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'affine.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.win32-ia32-msvc.node')
} else {
nativeBinding = require('@affine/native-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'affine.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.win32-arm64-msvc.node')
} else {
nativeBinding = require('@affine/native-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
loadError = new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'affine.darwin-universal.node'))
try {
if (localFileExisted) {
nativeBinding = require('./affine.darwin-universal.node')
} else {
nativeBinding = require('@affine/native-darwin-universal')
function requireNative() {
if (process.platform === 'android') {
if (process.arch === 'arm64') {
try {
return require('./affine.android-arm64.node')
} catch (e) {
loadErrors.push(e)
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'affine.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./affine.darwin-x64.node')
} else {
nativeBinding = require('@affine/native-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'affine.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.darwin-arm64.node')
} else {
nativeBinding = require('@affine/native-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
loadError = new Error(`Unsupported architecture on macOS: ${arch}`)
try {
return require('@affine/native-android-arm64')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./affine.android-arm-eabi.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-android-arm-eabi')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
}
break
case 'freebsd':
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'affine.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./affine.freebsd-x64.node')
} else {
nativeBinding = require('@affine/native-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'affine.freebsd-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./affine.freebsd-arm64.node')
} else {
nativeBinding = require('@affine/native-freebsd-arm64')
}
} catch (e) {
loadError = e
}
break
default:
loadError = new Error(`Unsupported architecture on FreeBSD: ${arch}`)
} else if (process.platform === 'win32') {
if (process.arch === 'x64') {
try {
return require('./affine.win32-x64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-win32-x64-msvc')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'ia32') {
try {
return require('./affine.win32-ia32-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-win32-ia32-msvc')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./affine.win32-arm64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-win32-arm64-msvc')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'affine.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.linux-x64-musl.node')
} else {
nativeBinding = require('@affine/native-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'affine.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.linux-x64-gnu.node')
} else {
nativeBinding = require('@affine/native-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'affine.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.linux-arm64-musl.node')
} else {
nativeBinding = require('@affine/native-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'affine.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.linux-arm64-gnu.node')
} else {
nativeBinding = require('@affine/native-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
localFileExisted = existsSync(
join(__dirname, 'affine.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('@affine/native-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'affine.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.linux-riscv64-musl.node')
} else {
nativeBinding = require('@affine/native-linux-riscv64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'affine.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.linux-riscv64-gnu.node')
} else {
nativeBinding = require('@affine/native-linux-riscv64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 's390x':
localFileExisted = existsSync(
join(__dirname, 'affine.linux-s390x-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./affine.linux-s390x-gnu.node')
} else {
nativeBinding = require('@affine/native-linux-s390x-gnu')
}
} catch (e) {
loadError = e
}
break
default:
loadError = new Error(`Unsupported architecture on Linux: ${arch}`)
} else if (process.platform === 'darwin') {
try {
return require('./affine.darwin-universal.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-darwin-universal')
} catch (e) {
loadErrors.push(e)
}
if (process.arch === 'x64') {
try {
return require('./affine.darwin-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-darwin-x64')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./affine.darwin-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-darwin-arm64')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
}
break
default:
loadError = new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
} else if (process.platform === 'freebsd') {
if (process.arch === 'x64') {
try {
return require('./affine.freebsd-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-freebsd-x64')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./affine.freebsd-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-freebsd-arm64')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
}
} else if (process.platform === 'linux') {
if (process.arch === 'x64') {
if (isMusl()) {
try {
return require('./affine.linux-x64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-x64-musl')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./affine.linux-x64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-x64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm64') {
if (isMusl()) {
try {
return require('./affine.linux-arm64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-arm64-musl')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./affine.linux-arm64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-arm64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm') {
if (isMusl()) {
try {
return require('./affine.linux-arm-musleabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-arm-musleabihf')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./affine.linux-arm-gnueabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-arm-gnueabihf')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'riscv64') {
if (isMusl()) {
try {
return require('./affine.linux-riscv64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-riscv64-musl')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./affine.linux-riscv64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-riscv64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'ppc64') {
try {
return require('./affine.linux-ppc64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-ppc64-gnu')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 's390x') {
try {
return require('./affine.linux-s390x-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@affine/native-linux-s390x-gnu')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
}
} else {
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
}
}
nativeBinding = requireNative()
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
try {
nativeBinding = require('./affine.wasi.cjs')
@ -355,8 +351,12 @@ if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
}
if (!nativeBinding) {
if (loadError) {
throw loadError
if (loadErrors.length > 0) {
// TODO Link to documentation with potential fixes
// - The package owner could build/publish bindings for this arch
// - The user may need to bundle the correct files
// - The user may need to re-install node_modules to get new packages
throw new Error('Failed to load native binding', { cause: loadErrors })
}
throw new Error(`Failed to load native binding`)
}

View File

@ -73,6 +73,7 @@ impl SqliteConnection {
.await
.map_err(anyhow::Error::from)?;
self.migrate_add_doc_id().await?;
self.migrate_add_doc_id_index().await?;
connection.detach();
Ok(())
}
@ -145,6 +146,25 @@ impl SqliteConnection {
Ok(updates)
}
#[napi]
pub async fn delete_updates(&self, doc_id: Option<String>) -> napi::Result<()> {
match doc_id {
Some(doc_id) => {
sqlx::query!("DELETE FROM updates WHERE doc_id = ?", doc_id)
.execute(&self.pool)
.await
.map_err(anyhow::Error::from)?;
}
None => {
sqlx::query!("DELETE FROM updates WHERE doc_id is NULL")
.execute(&self.pool)
.await
.map_err(anyhow::Error::from)?;
}
};
Ok(())
}
#[napi]
pub async fn get_updates_count(&self, doc_id: Option<String>) -> napi::Result<i32> {
let count = match doc_id {
@ -361,4 +381,17 @@ impl SqliteConnection {
}
}
}
pub async fn migrate_add_doc_id_index(&self) -> napi::Result<()> {
// ignore errors
match sqlx::query("CREATE INDEX IF NOT EXISTS idx_doc_id ON updates(doc_id);")
.execute(&self.pool)
.await
{
Ok(_) => Ok(()),
Err(err) => {
Err(anyhow::Error::from(err).into()) // Propagate other errors
}
}
}
}