Skip to content

Commit

Permalink
store current session in "database-session.bin" file
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Jun 24, 2019
1 parent 4ec3d87 commit 98f14e9
Show file tree
Hide file tree
Showing 18 changed files with 386 additions and 231 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import R from "ramda";

import {CONVERSATION_TYPE, ConversationEntry, FsDb, FsDbAccount, MAIL_FOLDER_TYPE, View} from "src/shared/model/database";
import {mailDateComparatorDefaultsToDesc, walkConversationNodesTree} from "src/shared/util";
import {resolveFsAccountFolders} from "src/electron-main/database/util";
import {resolveAccountFolders} from "src/electron-main/database/util";

export const FOLDER_UTILS: {
// TODO split "splitAndFormatAndFillSummaryFolders" function to pieces
Expand Down Expand Up @@ -155,7 +155,7 @@ export function buildFoldersAndRootNodePrototypes<T extends keyof FsDb["accounts
};
})();
const folders: View.Folder[] = Array.from(
resolveFsAccountFolders(account),
resolveAccountFolders(account),
(folder) => ({...folder, rootConversationNodes: [], size: 0, unread: 0}),
);
const resolveFolder = ((map = new Map(folders.reduce(
Expand Down
70 changes: 23 additions & 47 deletions src/electron-main/api/endpoints-builders/database/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import electronLog from "electron-log";
import sanitizeHtml from "sanitize-html";
import {equals, mergeDeepRight, omit} from "ramda";
import {omit} from "ramda";
import {v4 as uuid} from "uuid";

import {Context} from "src/electron-main/model";
import {DB_DATA_CONTAINER_FIELDS, FsDbAccount, IndexableMail} from "src/shared/model/database";
import {DB_DATA_CONTAINER_FIELDS, IndexableMail} from "src/shared/model/database";
import {IPC_MAIN_API_DB_INDEXER_NOTIFICATION$, IPC_MAIN_API_NOTIFICATION$} from "src/electron-main/api/constants";
import {IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS, IPC_MAIN_API_NOTIFICATION_ACTIONS, IpcMainApiEndpoints} from "src/shared/api/main";
import {buildDbExportEndpoints} from "./export";
import {buildDbIndexingEndpoints, narrowIndexActionPayload} from "./indexing";
import {buildDbSearchEndpoints, searchRootConversationNodes} from "./search";
import {curryFunctionMembers, isEntityUpdatesPatchNotEmpty} from "src/shared/util";
import {patchMetadata} from "src/electron-main/database/util";
import {prepareFoldersView} from "./folders-view";
import {validateEntity} from "src/electron-main/database/validation";

Expand Down Expand Up @@ -38,21 +39,27 @@ export async function buildEndpoints(ctx: Context): Promise<Pick<IpcMainApiEndpo

logger.info();

const {db, sessionDb} = ctx;
const key = {type, login} as const;
const account = ctx.db.getAccount(key) || ctx.db.initAccount(key);
const account = db.getAccount(key) || db.initAccount(key);
const sessionAccount = sessionDb.getAccount(key) || sessionDb.initAccount(key);

for (const entityType of DB_DATA_CONTAINER_FIELDS) {
const {remove, upsert} = entityUpdatesPatch[entityType];
const accountRecord = account[entityType];

// remove
remove.forEach(({pk}) => {
delete accountRecord[pk];
delete account[entityType][pk];
delete sessionAccount[entityType][pk];
sessionAccount.deletedPks[entityType].push(pk);
});

// add
for (const entity of upsert) {
accountRecord[entity.pk] = await validateEntity(entityType, entity);
const validatedEntity = await validateEntity(entityType, entity);
const {pk} = entity;
account[entityType][pk] = validatedEntity;
sessionAccount[entityType][pk] = validatedEntity;
}

if (entityType !== "mails") {
Expand All @@ -76,11 +83,11 @@ export async function buildEndpoints(ctx: Context): Promise<Pick<IpcMainApiEndpo
});
}

const metadataModified = patchMetadata(account.metadata, metadataPatch);
const metadataModified = patchMetadata(account.metadata, metadataPatch, "dbPatch");
const sessionMetadataModified = patchMetadata(sessionAccount.metadata, metadataPatch, "dbPatch");
const entitiesModified = isEntityUpdatesPatchNotEmpty(entityUpdatesPatch);
const modified = entitiesModified || metadataModified;

logger.verbose(JSON.stringify({entitiesModified, metadataModified, modified, forceFlush}));
logger.verbose(JSON.stringify({entitiesModified, metadataModified, sessionMetadataModified, forceFlush}));

setTimeout(async () => {
// TODO consider caching the config
Expand All @@ -91,12 +98,16 @@ export async function buildEndpoints(ctx: Context): Promise<Pick<IpcMainApiEndpo
key,
entitiesModified,
metadataModified,
stat: ctx.db.accountStat(account, includingSpam),
stat: db.accountStat(account, includingSpam),
}));
});

if (modified || forceFlush) {
await ctx.db.saveToFile();
if (
(entitiesModified || sessionMetadataModified)
||
forceFlush
) {
await sessionDb.saveToFile();
}

return account.metadata;
Expand Down Expand Up @@ -165,38 +176,3 @@ export async function buildEndpoints(ctx: Context): Promise<Pick<IpcMainApiEndpo
},
};
}

function patchMetadata(
dest: FsDbAccount["metadata"],
// TODO TS: use patch: Arguments<IpcMainApiEndpoints["dbPatch"]>[0]["metadata"],
patch: Omit<FsDbAccount<"protonmail">["metadata"], "type"> | Omit<FsDbAccount<"tutanota">["metadata"], "type">,
logger = curryFunctionMembers(_logger, "patchMetadata()"),
): boolean {
logger.info();

if (
"latestEventId" in patch
&&
(
!patch.latestEventId
||
!patch.latestEventId.trim()
)
) {
return false;
}

const merged = mergeDeepRight(dest, patch);

// console.log(JSON.stringify({dest, patch, merged}, null, 2));

if (equals(dest, merged)) {
return false;
}

Object.assign(dest, merged);

logger.verbose(`metadata patched with ${JSON.stringify(Object.keys(patch))} properties`);

return true;
}
4 changes: 2 additions & 2 deletions src/electron-main/api/endpoints-builders/database/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function buildDbSearchEndpoints(
ctx: Context,
): Promise<Pick<IpcMainApiEndpoints, "dbFullTextSearch">> {
return {
dbFullTextSearch({type, login, query, folderPks}) {
async dbFullTextSearch({type, login, query, folderPks}) {
logger.info("dbFullTextSearch()");

const timeoutMs = DEFAULT_API_CALL_TIMEOUT;
Expand Down Expand Up @@ -100,7 +100,7 @@ export async function buildDbSearchEndpoints(
}),
);

return result$;
return result$.toPromise();
},
};
}
Expand Down
6 changes: 4 additions & 2 deletions src/electron-main/api/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ const tests: Record<keyof IpcMainApiEndpoints, (t: ExecutionContext<TestContext>
const {deletePassword: deletePasswordSpy} = t.context.mocks["src/electron-main/keytar"];
const {clearSessionsCache} = t.context.mocks["src/electron-main/session"];
const {endpoints} = t.context;
const resetSpy = sinon.spy(t.context.ctx.db, "reset");
const dbResetSpy = sinon.spy(t.context.ctx.db, "reset");
const sessionDbResetSpy = sinon.spy(t.context.ctx.sessionDb, "reset");
const updateOverlayIconSpy = sinon.spy(endpoints, "updateOverlayIcon");

await endpoints.logout();
Expand All @@ -310,7 +311,8 @@ const tests: Record<keyof IpcMainApiEndpoints, (t: ExecutionContext<TestContext>
t.falsy(t.context.ctx.settingsStore.adapter);
t.is(deletePasswordSpy.callCount, 3);

t.is(2, resetSpy.callCount);
t.is(2, dbResetSpy.callCount);
t.is(2, sessionDbResetSpy.callCount);
t.is(2, clearSessionsCache.callCount);
t.is(2, updateOverlayIconSpy.callCount);
},
Expand Down
89 changes: 81 additions & 8 deletions src/electron-main/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import electronLog from "electron-log";
import * as SpellCheck from "src/electron-main/spell-check/api";
import {Account, Database, FindInPage, General, TrayIcon} from "./endpoints-builders";
import {Context} from "src/electron-main/model";
import {DB_DATA_CONTAINER_FIELDS} from "src/shared/model/database";
import {IPC_MAIN_API, IPC_MAIN_API_NOTIFICATION_ACTIONS, IpcMainApiEndpoints} from "src/shared/api/main";
import {IPC_MAIN_API_NOTIFICATION$} from "src/electron-main/api/constants";
import {PACKAGE_NAME, PRODUCT_NAME} from "src/shared/constants";
Expand All @@ -11,6 +12,7 @@ import {buildSettingsAdapter} from "src/electron-main/util";
import {clearSessionsCache, initSessionByAccount} from "src/electron-main/session";
import {curryFunctionMembers} from "src/shared/util";
import {deletePassword, getPassword, setPassword} from "src/electron-main/keytar";
import {patchMetadata} from "src/electron-main/database/util";
import {upgradeConfig, upgradeDatabase, upgradeSettings} from "src/electron-main/storage-upgrade";

const logger = curryFunctionMembers(electronLog, "[src/electron-main/api/index]");
Expand Down Expand Up @@ -94,6 +96,7 @@ export const initApi = async (ctx: Context): Promise<IpcMainApiEndpoints> => {

ctx.settingsStore = ctx.settingsStore.clone({adapter: undefined});
ctx.db.reset();
ctx.sessionDb.reset();
delete ctx.selectedAccount; // TODO extend "logout" api test: "delete ctx.selectedAccount"

await clearSessionsCache(ctx);
Expand Down Expand Up @@ -189,22 +192,89 @@ export const initApi = async (ctx: Context): Promise<IpcMainApiEndpoints> => {

async reEncryptSettings({encryptionPreset, password}) {
await ctx.configStore.write({
...(await ctx.configStore.readExisting()),
...await ctx.configStore.readExisting(),
encryptionPreset,
});

return await endpoints.changeMasterPassword({password, newPassword: password});
return endpoints.changeMasterPassword({password, newPassword: password});
},

// TODO move to "src/electron-main/api/endpoints-builders/database"
async loadDatabase({accounts}) {
logger.info("loadDatabase() start");

if (await ctx.db.persisted()) {
await ctx.db.loadFromFile();
await upgradeDatabase(ctx.db, accounts);
const {db, sessionDb, configStore} = ctx;

if (await sessionDb.persisted()) {
await sessionDb.loadFromFile();
const upgraded = await upgradeDatabase(sessionDb, accounts);
logger.verbose("loadDatabase() session database upgraded:", upgraded);
// it will be reset and saved anyway
}

let needToSaveDb: boolean = false;

if (await db.persisted()) {
await db.loadFromFile();
const upgraded = await upgradeDatabase(db, accounts);
logger.verbose("loadDatabase() database upgraded:", upgraded);
if (upgraded) {
needToSaveDb = true;
}
}

// merging session database to the primary one
if (await sessionDb.persisted()) {
for (const {account: sourceAccount, pk: accountPk} of sessionDb.accountsIterator()) {
logger.verbose("loadDatabase() account processing iteration starts");
const targetAccount = db.getAccount(accountPk) || db.initAccount(accountPk);

// inserting new/updated entities
for (const entityType of DB_DATA_CONTAINER_FIELDS) {
const patch = sourceAccount[entityType];
const patchSize = Object.keys(patch).length;
logger.verbose(`loadDatabase() patch size (${entityType}):`, patchSize);
if (!patchSize) {
// skipping iteration as the patch is empty
continue;
}
Object.assign(
targetAccount[entityType],
patch,
);
needToSaveDb = true;
}

// removing entities
for (const entityType of DB_DATA_CONTAINER_FIELDS) {
const deletedPks = sourceAccount.deletedPks[entityType];
logger.verbose("loadDatabase() removing entities count:", deletedPks.length);
for (const pk of deletedPks) {
delete targetAccount[entityType][pk];
needToSaveDb = true;
}
}

// patching metadata
(() => {
const metadataPatched = patchMetadata(targetAccount.metadata, sourceAccount.metadata, "loadDatabase");
logger.verbose(`loadDatabase() metadata patched:`, metadataPatched);
if (metadataPatched) {
needToSaveDb = true;
}
})();
}
}

if (needToSaveDb) {
await db.saveToFile();
}

if ((await ctx.configStore.readExisting()).fullTextSearch) {
// resetting and saving the session database
sessionDb.reset();
await sessionDb.saveToFile();

if ((await configStore.readExisting()).fullTextSearch) {
await attachFullTextIndexWindow(ctx);
} else {
await detachFullTextIndexWindow(ctx);
Expand All @@ -220,11 +290,14 @@ export const initApi = async (ctx: Context): Promise<IpcMainApiEndpoints> => {
async toggleCompactLayout() {
const config = await ctx.configStore.readExisting();

return await ctx.configStore.write({...config, compactLayout: !config.compactLayout});
return ctx.configStore.write({
...config,
compactLayout: !config.compactLayout,
});
},
};

IPC_MAIN_API.register(endpoints);
IPC_MAIN_API.register(endpoints, {logger});

ctx.deferredEndpoints.resolve(endpoints);

Expand Down
Loading

0 comments on commit 98f14e9

Please sign in to comment.