Skip to content

Commit

Permalink
refactor(electron): sqlite db data workflow (remove symlink & fs watc…
Browse files Browse the repository at this point in the history
…her) (#2491)
  • Loading branch information
pengx17 committed May 29, 2023
1 parent f3ac122 commit 20cf452
Show file tree
Hide file tree
Showing 58 changed files with 1,077 additions and 895 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import assert from 'node:assert';
import path from 'node:path';

import fs from 'fs-extra';
import type { Subscription } from 'rxjs';
import { v4 } from 'uuid';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import * as Y from 'yjs';

import type { MainIPCHandlerMap } from '../../../../constraints';
import type { MainIPCHandlerMap } from '../../../constraints';

const registeredHandlers = new Map<
string,
Expand Down Expand Up @@ -42,6 +41,7 @@ ReturnType<MainIPCHandlerMap[T][F]> {
}

const SESSION_DATA_PATH = path.join(__dirname, './tmp', 'affine-test');
const DOCUMENTS_PATH = path.join(__dirname, './tmp', 'affine-test-documents');

const browserWindow = {
isDestroyed: () => {
Expand Down Expand Up @@ -92,8 +92,12 @@ function compareBuffer(a: Uint8Array | null, b: Uint8Array | null) {
const electronModule = {
app: {
getPath: (name: string) => {
assert(name === 'sessionData');
return SESSION_DATA_PATH;
if (name === 'sessionData') {
return SESSION_DATA_PATH;
} else if (name === 'documents') {
return DOCUMENTS_PATH;
}
throw new Error('not implemented');
},
name: 'affine-test',
on: (name: string, callback: (...args: any[]) => any) => {
Expand Down Expand Up @@ -123,27 +127,23 @@ vi.doMock('electron', () => {
return electronModule;
});

let connectableSubscription: Subscription;

beforeEach(async () => {
const { registerHandlers } = await import('../register');
const { registerHandlers } = await import('../handlers');
registerHandlers();

// should also register events
const { registerEvents } = await import('../../events');
const { registerEvents } = await import('../events');
registerEvents();
await fs.mkdirp(SESSION_DATA_PATH);
const { database$ } = await import('../db/ensure-db');
await import('../db/ensure-db');

connectableSubscription = database$.connect();
registeredHandlers.get('ready')?.forEach(fn => fn());
});

afterEach(async () => {
// reset registered handlers
registeredHandlers.get('before-quit')?.forEach(fn => fn());

connectableSubscription.unsubscribe();

await fs.remove(SESSION_DATA_PATH);
});

Expand All @@ -157,55 +157,26 @@ describe('ensureSQLiteDB', () => {
expect(fileExists).toBe(true);
});

test('when db file is removed', async () => {
// stub webContents.send
const sendSpy = vi.spyOn(browserWindow.webContents, 'send');
const id = v4();
test('should emit the same db instance for the same id', async () => {
const [id1, id2] = [v4(), v4()];
const { ensureSQLiteDB } = await import('../db/ensure-db');
let workspaceDB = await ensureSQLiteDB(id);
const file = workspaceDB.path;
const fileExists = await fs.pathExists(file);
expect(fileExists).toBe(true);

// Can't remove file on Windows, because the sqlite is still holding the file handle
if (process.platform === 'win32') {
return;
}

await fs.remove(file);

// wait for 2000ms for file watcher to detect file removal
await delay(2000);

expect(sendSpy).toBeCalledWith('db:onDBFileMissing', id);

// ensureSQLiteDB should recreate the db file
workspaceDB = await ensureSQLiteDB(id);
const fileExists2 = await fs.pathExists(file);
expect(fileExists2).toBe(true);
sendSpy.mockRestore();
const workspaceDB1 = await ensureSQLiteDB(id1);
const workspaceDB2 = await ensureSQLiteDB(id2);
const workspaceDB3 = await ensureSQLiteDB(id1);
expect(workspaceDB1).toBe(workspaceDB3);
expect(workspaceDB1).not.toBe(workspaceDB2);
});

test('when db file is updated', async () => {
test('when app quit, db should be closed', async () => {
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
const { dbSubjects } = await import('../../events/db');
const workspaceDB = await ensureSQLiteDB(id);
const file = workspaceDB.path;
const fileExists = await fs.pathExists(file);
expect(fileExists).toBe(true);
const dbUpdateSpy = vi.spyOn(dbSubjects.dbFileUpdate, 'next');
registeredHandlers.get('before-quit')?.forEach(fn => fn());
await delay(100);
// writes some data to the db file
await fs.appendFile(file, 'random-data', { encoding: 'binary' });
// write again
await fs.appendFile(file, 'random-data', { encoding: 'binary' });

// wait for 2000ms for file watcher to detect file change
await delay(2000);

expect(dbUpdateSpy).toBeCalledWith(id);
dbUpdateSpy.mockRestore();
expect(workspaceDB.db?.open).toBe(false);
});
});

Expand All @@ -219,16 +190,14 @@ describe('workspace handlers', () => {
});

test('delete workspace', async () => {
// @TODO dispatch is hanging on Windows
if (process.platform === 'win32') {
return;
}
const ids = [v4(), v4()];
const { ensureSQLiteDB } = await import('../db/ensure-db');
await Promise.all(ids.map(id => ensureSQLiteDB(id)));
const dbs = await Promise.all(ids.map(id => ensureSQLiteDB(id)));
await dispatch('workspace', 'delete', ids[1]);
const list = await dispatch('workspace', 'list');
expect(list.map(([id]) => id)).toEqual([ids[0]]);
// deleted db should be closed
expect(dbs[1].db?.open).toBe(false);
});
});

Expand Down Expand Up @@ -290,7 +259,7 @@ describe('db handlers', () => {

test('list blobs (empty)', async () => {
const workspaceId = v4();
const list = await dispatch('db', 'getPersistedBlobs', workspaceId);
const list = await dispatch('db', 'getBlobKeys', workspaceId);
expect(list).toEqual([]);
});

Expand Down Expand Up @@ -320,14 +289,14 @@ describe('db handlers', () => {
).toBe(true);

// list blobs
let lists = await dispatch('db', 'getPersistedBlobs', workspaceId);
let lists = await dispatch('db', 'getBlobKeys', workspaceId);
expect(lists).toHaveLength(2);
expect(lists).toContain('testBin');
expect(lists).toContain('testBin2');

// delete blob
await dispatch('db', 'deleteBlob', workspaceId, 'testBin');
lists = await dispatch('db', 'getPersistedBlobs', workspaceId);
lists = await dispatch('db', 'getBlobKeys', workspaceId);
expect(lists).toEqual(['testBin2']);
});
});
Expand Down Expand Up @@ -409,10 +378,10 @@ describe('dialog handlers', () => {
expect(res.error).toBe('DB_FILE_PATH_INVALID');
});

test('loadDBFile (error, not a valid db file)', async () => {
test('loadDBFile (error, not a valid affine file)', async () => {
// create a random db file
const basePath = path.join(SESSION_DATA_PATH, 'random-path');
const dbPath = path.join(basePath, 'xxx.db');
const dbPath = path.join(basePath, 'xxx.affine');
await fs.ensureDir(basePath);
await fs.writeFile(dbPath, 'hello world');

Expand All @@ -428,73 +397,74 @@ describe('dialog handlers', () => {
electronModule.dialog = {};
});

test('loadDBFile', async () => {
test('loadDBFile (correct)', async () => {
// we use ensureSQLiteDB to create a valid db file
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
const db = await ensureSQLiteDB(id);

// copy db file to dbPath
const basePath = path.join(SESSION_DATA_PATH, 'random-path');
const originDBFilePath = path.join(basePath, 'xxx.db');
const clonedDBPath = path.join(basePath, 'xxx.affine');
await fs.ensureDir(basePath);
await fs.copyFile(db.path, originDBFilePath);

// on Windows, we skip this test because we can't delete the db file
if (process.platform === 'win32') {
return;
}
await fs.copyFile(db.path, clonedDBPath);

// remove db
await fs.remove(db.path);
// delete workspace
await dispatch('workspace', 'delete', id);

// try load originDBFilePath
const mockShowOpenDialog = vi.fn(() => {
return { filePaths: [originDBFilePath] };
return { filePaths: [clonedDBPath] };
}) as any;
electronModule.dialog.showOpenDialog = mockShowOpenDialog;

const res = await dispatch('dialog', 'loadDBFile');
expect(mockShowOpenDialog).toBeCalled();
expect(res.workspaceId).not.toBeUndefined();
const newId = res.workspaceId;

expect(newId).not.toBeUndefined();

assert(newId);

const meta = await dispatch('workspace', 'getMeta', newId);

const importedDb = await ensureSQLiteDB(res.workspaceId!);
expect(await fs.realpath(importedDb.path)).toBe(originDBFilePath);
expect(importedDb.path).not.toBe(originDBFilePath);
expect(meta.secondaryDBPath).toBe(clonedDBPath);

// try load it again, will trigger error (db file already loaded)
const res2 = await dispatch('dialog', 'loadDBFile');
expect(res2.error).toBe('DB_FILE_ALREADY_LOADED');
});

test('moveDBFile', async () => {
test('moveDBFile (valid)', async () => {
const newPath = path.join(SESSION_DATA_PATH, 'xxx');
const mockShowSaveDialog = vi.fn(() => {
return { filePath: newPath };
const showOpenDialog = vi.fn(() => {
return { filePaths: [newPath] };
}) as any;
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
electronModule.dialog.showOpenDialog = showOpenDialog;

const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
await ensureSQLiteDB(id);
const res = await dispatch('dialog', 'moveDBFile', id);
expect(mockShowSaveDialog).toBeCalled();
expect(res.filePath).toBe(newPath);
expect(showOpenDialog).toBeCalled();
assert(res.filePath);
expect(path.dirname(res.filePath)).toBe(newPath);
expect(res.filePath.endsWith('.affine')).toBe(true);
electronModule.dialog = {};
});

test('moveDBFile (skipped)', async () => {
const mockShowSaveDialog = vi.fn(() => {
return { filePath: null };
test('moveDBFile (canceled)', async () => {
const showOpenDialog = vi.fn(() => {
return { filePaths: null };
}) as any;
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
electronModule.dialog.showOpenDialog = showOpenDialog;

const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
await ensureSQLiteDB(id);

const res = await dispatch('dialog', 'moveDBFile', id);
expect(mockShowSaveDialog).toBeCalled();
expect(showOpenDialog).toBeCalled();
expect(res.filePath).toBe(undefined);
electronModule.dialog = {};
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { app, Menu } from 'electron';

import { isMacOS } from '../../utils';
import { subjects } from './events';
import { checkForUpdatesAndNotify } from './handlers/updater';
import { revealLogFile } from './logger';
import { isMacOS } from '../../../utils';
import { revealLogFile } from '../logger';
import { checkForUpdatesAndNotify } from '../updater';
import { applicationMenuSubjects } from './subject';

// Unique id for menuitems
const MENUITEM_NEW_PAGE = 'affine:new-page';
Expand Down Expand Up @@ -43,7 +43,7 @@ export function createApplicationMenu() {
label: 'New Page',
accelerator: isMac ? 'Cmd+N' : 'Ctrl+N',
click: () => {
subjects.applicationMenu.newPageAction.next();
applicationMenuSubjects.newPageAction.next();
},
},
{ type: 'separator' },
Expand Down Expand Up @@ -117,7 +117,7 @@ export function createApplicationMenu() {
},
},
{
label: 'Open logs folder',
label: 'Open log file',
click: async () => {
revealLogFile();
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Subject } from 'rxjs';
import type { MainEventListener } from '../type';
import { applicationMenuSubjects } from './subject';

import type { MainEventListener } from './type';

export const applicationMenuSubjects = {
newPageAction: new Subject<void>(),
};
export * from './create';
export * from './subject';

/**
* Events triggered by application menu
Expand Down
5 changes: 5 additions & 0 deletions apps/electron/layers/main/src/application-menu/subject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Subject } from 'rxjs';

export const applicationMenuSubjects = {
newPageAction: new Subject<void>(),
};

2 comments on commit 20cf452

@vercel
Copy link

@vercel vercel bot commented on 20cf452 May 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

affine-storybook – ./packages/component

affine-storybook.vercel.app
affine-storybook-git-master-toeverything.vercel.app
storybook.affine.pro
affine-storybook-toeverything.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 20cf452 May 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.