Web worker draft

This commit is contained in:
Mr. Stallion 2021-01-23 18:52:27 -06:00
parent 52bf27d4fc
commit e1adc05cae
18 changed files with 390 additions and 516 deletions

View File

@ -2,6 +2,7 @@
## Canary
* Fixed Gelbooru video previews
* Moved database queries to a web worker to gain more responsive UI
## 1.9.0

View File

@ -254,6 +254,7 @@
webPreferences: {
webviewTag: true,
nodeIntegration: true,
nodeIntegrationInWorker: true,
spellcheck: true,
enableRemoteModule: true
}

View File

@ -179,7 +179,7 @@ function createWindow(): Electron.BrowserWindow | undefined {
const lastState = windowState.getSavedWindowState();
const windowProperties: Electron.BrowserWindowConstructorOptions & {maximized: boolean} = {
...lastState, center: lastState.x === undefined, show: false,
webPreferences: { webviewTag: true, nodeIntegration: true, spellcheck: true, enableRemoteModule: true }
webPreferences: { webviewTag: true, nodeIntegration: true, nodeIntegrationInWorker: true, spellcheck: true, enableRemoteModule: true }
};
if(process.platform === 'darwin') {

View File

@ -1,3 +1,4 @@
const _ = require('lodash');
const path = require('path');
const fs = require('fs');
const ForkTsCheckerWebpackPlugin = require('@f-list/fork-ts-checker-webpack-plugin');
@ -145,6 +146,44 @@ const mainConfig = {
}
};
const storeWorkerEndpointConfig = _.assign(
_.cloneDeep(mainConfig),
{
entry: [path.join(__dirname, '..', 'learn', 'store', 'worker', 'store.worker.endpoint.ts')],
output: {
path: __dirname + '/app',
filename: 'storeWorkerEndpoint.js',
globalObject: 'this'
},
target: 'electron-renderer',
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
configFile: __dirname + '/tsconfig-renderer.json',
transpileOnly: true,
getCustomTransformers: () => ({before: [vueTransformer]})
}
},
]
},
plugins: [
new ForkTsCheckerWebpackPlugin({
async: false,
tslint: path.join(__dirname, '../tslint.json'),
tsconfig: './tsconfig-renderer.json',
vue: true
})
]
}
);
module.exports = function(mode) {
const themesDir = path.join(__dirname, '../scss/themes/chat');
const themes = fs.readdirSync(themesDir);
@ -185,12 +224,14 @@ module.exports = function(mode) {
process.env.NODE_ENV = 'production';
mainConfig.devtool = rendererConfig.devtool = 'source-map';
rendererConfig.plugins.push(new OptimizeCssAssetsPlugin());
storeWorkerEndpointConfig.devtool = 'source-map';
} else {
// mainConfig.devtool = rendererConfig.devtool = 'none';
mainConfig.devtool = 'inline-source-map';
rendererConfig.devtool = 'inline-source-map';
storeWorkerEndpointConfig.devtool = 'inline-source-map';
}
return [mainConfig, rendererConfig];
return [mainConfig, rendererConfig, storeWorkerEndpointConfig];
};

View File

@ -8,7 +8,6 @@ import { AdCache } from './ad-cache';
import { ChannelConversationCache } from './channel-conversation-cache';
import { CharacterProfiler } from './character-profiler';
import { CharacterCacheRecord, ProfileCache } from './profile-cache';
import { IndexedStore } from './store/indexed';
import Timer = NodeJS.Timer;
import ChannelConversation = Conversation.ChannelConversation;
import Message = Conversation.Message;
@ -17,6 +16,10 @@ import Bluebird from 'bluebird';
import ChatMessage = Conversation.ChatMessage;
import { GeneralSettings } from '../electron/common';
import { Gender } from './matcher-types';
import { WorkerStore } from './store/worker';
import { PermanentIndexedStore } from './store/types';
import * as path from 'path';
// import * as electron from 'electron';
export interface ProfileCacheQueueEntry {
@ -41,7 +44,7 @@ export class CacheManager {
protected profileTimer: Timer | null = null;
protected characterProfiler: CharacterProfiler | undefined;
protected profileStore?: IndexedStore;
protected profileStore?: PermanentIndexedStore;
protected lastPost: Date = new Date();
@ -175,7 +178,9 @@ export class CacheManager {
async start(settings: GeneralSettings, skipFlush: boolean): Promise<void> {
await this.stop();
this.profileStore = await IndexedStore.open();
this.profileStore = await WorkerStore.open(
path.join(/*electron.remote.app.getAppPath(),*/ 'storeWorkerEndpoint.js')
); // await IndexedStore.open();
this.profileCache.setStore(this.profileStore);

View File

@ -4,7 +4,7 @@ import core from '../chat/core';
import {Character as ComplexCharacter, CharacterGroup, Guestbook} from '../site/character_page/interfaces';
import { AsyncCache } from './async-cache';
import { Matcher, MatchReport, Scoring } from './matcher';
import { PermanentIndexedStore } from './store/sql-store';
import { PermanentIndexedStore } from './store/types';
import { CharacterImage, SimpleCharacter } from '../interfaces';

View File

@ -1,89 +0,0 @@
/**
* Do not use
*/
// // import { Database, Statement } from 'better-sqlite3';
//
// type Database = any;
// type Statement = any;
//
// import { ProfileRecord, SqlStore } from './sql-store';
// import { Character as ComplexCharacter } from '../../site/character_page/interfaces';
// import * as SQL from './sql';
//
//
// export class BetterSqliteStore extends SqlStore {
// protected static Sqlite: any;
//
// protected stmtGetProfile: Statement;
// protected stmtStoreProfile: Statement;
// protected stmtUpdateCounts: Statement;
//
// protected db: Database;
// protected checkpointTimer: NodeJS.Timer | null = null;
//
// constructor(name: string) {
// super(name);
//
// if (!BetterSqliteStore.Sqlite) {
// throw new Error('BetterSqliteStore.setSqlite() must be called before instantiation');
// }
//
// this.db = new BetterSqliteStore.Sqlite(this.dbFile, {});
//
// this.stmtGetProfile = this.db.prepare(SQL.ProfileGet);
// this.stmtStoreProfile = this.db.prepare(SQL.ProfileInsert);
// this.stmtUpdateCounts = this.db.prepare(SQL.ProfileUpdateCount);
// }
//
//
// static setSqlite(Sqlite: any) {
// BetterSqliteStore.Sqlite = Sqlite;
// }
//
//
// async getProfile(name: string): Promise<ProfileRecord | undefined> {
// const data = this.stmtGetProfile.get(this.toProfileId(name));
//
// if (!data) {
// return;
// }
//
// // tslint:disable-next-line: no-unsafe-any
// data.profileData = JSON.parse(data.profileData) as ComplexCharacter;
//
// return data as ProfileRecord;
// }
//
//
// protected async run(stmtName: 'stmtStoreProfile' | 'stmtUpdateCounts', data: any[]): Promise<void> {
// if (!(stmtName in this)) {
// throw new Error(`Unknown statement: ${stmtName}`);
// }
//
// this[stmtName].run(data);
// }
//
//
// async start(): Promise<void> {
// await this.stop();
//
// this.db.pragma('journal_mode = WAL');
// this.db.exec(SQL.DatabaseMigration);
//
// this.checkpointTimer = setInterval(
// () => this.db.checkpoint(),
// 10 * 60 * 1000
// );
// }
//
//
// async stop(): Promise<void> {
// if (this.checkpointTimer) {
// clearInterval(this.checkpointTimer);
//
// this.checkpointTimer = null;
// }
// }
// }
//

View File

@ -3,7 +3,7 @@ import * as _ from 'lodash';
import {Character as ComplexCharacter, CharacterGroup, Guestbook} from '../../site/character_page/interfaces';
import { CharacterAnalysis } from '../matcher';
import { PermanentIndexedStore, ProfileRecord } from './sql-store';
import { PermanentIndexedStore, ProfileRecord } from './types';
import { CharacterImage, SimpleCharacter } from '../../interfaces';
import Bluebird from 'bluebird';
@ -30,7 +30,7 @@ export class IndexedStore implements PermanentIndexedStore {
}
static async open(dbName: string = 'flist-ascending-profiles'): Promise<IndexedStore> {
const request = window.indexedDB.open(dbName, 2);
const request = indexedDB.open(dbName, 2);
request.onupgradeneeded = (event) => {
const db = request.result;
@ -85,7 +85,7 @@ export class IndexedStore implements PermanentIndexedStore {
}
async prepareProfileData(c: ComplexCharacter): Promise<ProfileRecord> {
private async prepareProfileData(c: ComplexCharacter): Promise<ProfileRecord> {
const existing = await this.getProfile(c.character.name);
const ca = new CharacterAnalysis(c.character);
@ -121,8 +121,8 @@ export class IndexedStore implements PermanentIndexedStore {
}
async storeProfile(c: ComplexCharacter): Promise<void> {
const data = await this.prepareProfileData(c);
async storeProfile(character: ComplexCharacter): Promise<void> {
const data = await this.prepareProfileData(character);
const tx = this.db.transaction(IndexedStore.STORE_NAME, 'readwrite');
const store = tx.objectStore(IndexedStore.STORE_NAME);
@ -135,37 +135,37 @@ export class IndexedStore implements PermanentIndexedStore {
}
async updateProfileCounts(
name: string,
guestbookCount: number | null,
friendCount: number | null,
groupCount: number | null
): Promise<void> {
const existing = await this.getProfile(name);
if (!existing) {
return;
}
const data = _.merge(
existing,
{
lastCounted: Math.round(Date.now() / 1000),
guestbookCount,
friendCount,
groupCount
}
);
const tx = this.db.transaction(IndexedStore.STORE_NAME, 'readwrite');
const store = tx.objectStore(IndexedStore.STORE_NAME);
const putRequest = store.put(data);
// tslint:disable-next-line no-any
await promisifyRequest<any>(putRequest);
// console.log('IDX update counts', name, data);
}
// async updateProfileCounts(
// name: string,
// guestbookCount: number | null,
// friendCount: number | null,
// groupCount: number | null
// ): Promise<void> {
// const existing = await this.getProfile(name);
//
// if (!existing) {
// return;
// }
//
// const data = _.merge(
// existing,
// {
// lastCounted: Math.round(Date.now() / 1000),
// guestbookCount,
// friendCount,
// groupCount
// }
// );
//
// const tx = this.db.transaction(IndexedStore.STORE_NAME, 'readwrite');
// const store = tx.objectStore(IndexedStore.STORE_NAME);
// const putRequest = store.put(data);
//
// // tslint:disable-next-line no-any
// await promisifyRequest<any>(putRequest);
//
// // console.log('IDX update counts', name, data);
// }
async updateProfileMeta(

View File

@ -1,123 +0,0 @@
// tslint:disable-next-line:no-duplicate-imports
// import * as path from 'path';
// import core from '../../chat/core';
import {Character as ComplexCharacter, CharacterGroup, Guestbook} from '../../site/character_page/interfaces';
import { CharacterImage, SimpleCharacter } from '../../interfaces';
import { FurryPreference, Gender, Orientation, Species } from '../matcher-types';
// This design should be refactored; it's bad
export interface ProfileRecord {
id: string;
name: string;
profileData: ComplexCharacter;
firstSeen: number;
lastFetched: number;
gender: Gender | null;
orientation: Orientation | null;
furryPreference: FurryPreference | null;
species: Species | null;
age: number | null;
domSubRole: number | null;
position: number | null;
// lastCounted: number | null;
// guestbookCount: number | null;
// friendCount: number | null;
// groupCount: number | null;
lastMetaFetched: number | null;
guestbook: Guestbook | null;
images: CharacterImage[] | null;
friends: SimpleCharacter[] | null;
groups: CharacterGroup[] | null;
}
// export type Statement = any;
// export type Database = any;
export interface PermanentIndexedStore {
getProfile(name: string): Promise<ProfileRecord | undefined>;
storeProfile(c: ComplexCharacter): Promise<void>;
updateProfileMeta(
name: string,
images: CharacterImage[] | null,
guestbook: Guestbook | null,
friends: SimpleCharacter[] | null,
groups: CharacterGroup[] | null
): Promise<void>;
flushProfiles(daysToExpire: number): Promise<void>;
start(): Promise<void>;
stop(): Promise<void>;
}
// export abstract class SqlStore implements PermanentIndexedStore {
// protected dbFile: string;
// protected checkpointTimer: NodeJS.Timer | null = null;
//
// constructor(dbName: string = 'fchat-ascending.sqlite') {
// this.dbFile = path.join(core.state.generalSettings!.logDirectory, dbName);
// }
//
// // tslint:disable-next-line: prefer-function-over-method
// protected toProfileId(name: string): string {
// return name.toLowerCase();
// }
//
// abstract getProfile(name: string): Promise<ProfileRecord | undefined>;
//
// abstract start(): Promise<void>;
// abstract stop(): Promise<void>;
//
// // tslint:disable-next-line no-any
// protected abstract run(statementName: 'stmtStoreProfile' | 'stmtUpdateCounts', data: any[]): Promise<void>;
//
//
// async storeProfile(c: ComplexCharacter): Promise<void> {
// const ca = new CharacterAnalysis(c.character);
//
// const data = [
// this.toProfileId(c.character.name),
// c.character.name,
// JSON.stringify(c),
// Math.round(Date.now() / 1000),
// Math.round(Date.now() / 1000),
// ca.gender,
// ca.orientation,
// ca.furryPreference,
// ca.species,
// ca.age,
// null, // domSubRole
// null // position
// ];
//
// await this.run('stmtStoreProfile', data);
// }
//
// async updateProfileMeta(
// name: string,
// images: CharacterImage[] | null,
// guestbook: GuestbookState | null,
// friends: CharacterFriend[] | null,
// groups: CharacterGroup[] | null
// ): Promise<void> {
// throw new Error('Not implemented');
// }
//
// // async updateProfileCounts(
// // name: string,
// // guestbookCount: number | null,
// // friendCount: number | null,
// // groupCount: number | null
// // ): Promise<void> {
// // await this.run(
// // 'stmtUpdateCounts',
// // [Math.round(Date.now() / 1000), guestbookCount, friendCount, groupCount, this.toProfileId(name)]
// // );
// // }
// }
//

View File

@ -1,55 +0,0 @@
export const ProfileInsert =
`INSERT INTO profiles
(id, name, profileData, firstSeen, lastFetched, gender, orientation, furryPreference,
species, age, domSubRole, position)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
profileData=excluded.profileData,
lastFetched=excluded.lastFetched,
gender=excluded.gender,
orientation=excluded.orientation,
furryPreference=excluded.furryPreference,
species=excluded.species,
age=excluded.age,
domSubRole=excluded.domSubRole,
position=excluded.position
`;
export const ProfileGet =
'SELECT * FROM profiles WHERE id = ?';
export const ProfileUpdateCount =
'UPDATE profiles SET lastCounted = ?, guestbookCount = ?, friendCount = ?, groupCount = ? WHERE id = ?';
export const DatabaseMigration =
`CREATE TABLE IF NOT EXISTS "migration" (
"version" INTEGER NOT NULL
, UNIQUE("version")
);
CREATE TABLE IF NOT EXISTS "profiles" (
"id" TEXT NOT NULL PRIMARY KEY
, "name" TEXT NOT NULL
, "profileData" TEXT NOT NULL
, "firstSeen" INTEGER NOT NULL
, "lastFetched" INTEGER NOT NULL
, "lastCounted" INTEGER
, "gender" INTEGER
, "orientation" INTEGER
, "furryPreference" INTEGER
, "species" INTEGER
, "age" INTEGER
, "domSubRole" INTEGER
, "position" INTEGER
, "guestbookCount" INTEGER
, "friendCount" INTEGER
, "groupCount" INTEGER
, UNIQUE("id")
);
INSERT OR IGNORE INTO migration(version) VALUES(1);
`;

View File

@ -1,63 +0,0 @@
/**
* Do not use
*/
// import * as Sqlite from 'sqlite';
// import { Character as ComplexCharacter } from '../../site/character_page/interfaces';
//
// import * as SQL from './sql';
// import { ProfileRecord, SqlStore } from './sql-store';
//
// export class SqliteStore extends SqlStore {
// protected stmtGetProfile: Promise<Sqlite.Statement>;
// protected stmtStoreProfile: Promise<Sqlite.Statement>;
// protected stmtUpdateCounts: Promise<Sqlite.Statement>;
//
// protected db: Promise<Sqlite.Database>;
//
// constructor(dbName: string = 'fchat-ascending.sqlite') {
// super(dbName);
//
// this.db = Sqlite.open(this.dbFile);
//
// this.stmtGetProfile = this.db.then((db) => db.prepare(SQL.ProfileGet));
// this.stmtStoreProfile = this.db.then((db) => db.prepare(SQL.ProfileInsert));
// this.stmtUpdateCounts = this.db.then((db) => db.prepare(SQL.ProfileUpdateCount));
// }
//
//
// async getProfile(name: string): Promise<ProfileRecord | undefined> {
// const data = await (await this.stmtGetProfile).get(this.toProfileId(name));
//
// if (!data) {
// return;
// }
//
// // tslint:disable-next-line: no-unsafe-any
// data.profileData = JSON.parse(data.profileData) as ComplexCharacter;
//
// return data as ProfileRecord;
// }
//
//
// protected async run(stmtName: 'stmtStoreProfile' | 'stmtUpdateCounts', data: any[]): Promise<void> {
// await (await this[stmtName]).run(...data);
// }
//
//
// async start(): Promise<void> {
// await this.stop();
//
// await (await this.db).run(SQL.DatabaseMigration);
// }
//
//
// async stop(): Promise<void> {
// if (this.checkpointTimer) {
// clearInterval(this.checkpointTimer);
//
// this.checkpointTimer = null;
// }
// }
//
// }

View File

@ -1,144 +0,0 @@
/**
* Do not use
*/
// type Sqlite3Statement = any;
// type Sqlite3Database = any;
//
// import { Character as ComplexCharacter } from '../../site/character_page/interfaces';
//
// import * as SQL from './sql';
// import { ProfileRecord, SqlStore } from './sql-store';
//
// export class Sqlite3Store extends SqlStore {
// protected static Sqlite: any;
//
// protected stmtGetProfile: Promise<Sqlite3Statement>;
// protected stmtStoreProfile: Promise<Sqlite3Statement>;
// protected stmtUpdateCounts: Promise<Sqlite3Statement>;
//
// protected db: Promise<Sqlite3Database>;
//
// constructor(dbName: string = 'fchat-ascending.sqlite') {
// super(dbName);
//
// if (!Sqlite3Store.Sqlite) {
// throw new Error('Sqlite3Store.setSqlite() must be called before instantiation');
// }
//
// this.db = this.prepareDatabase(this.dbFile);
//
// this.stmtGetProfile = this.prepareStatement(SQL.ProfileGet);
// this.stmtStoreProfile = this.prepareStatement(SQL.ProfileInsert);
// this.stmtUpdateCounts = this.prepareStatement(SQL.ProfileUpdateCount);
// }
//
// static setSqlite(Sqlite: any) {
// Sqlite3Store.Sqlite = Sqlite;
// }
//
// prepareDatabase(fileName: string): Promise<Sqlite3Database> {
// return new Promise(
// (resolve, reject) => {
// // Sqlite3Store.Sqlite.verbose();
// const db = new Sqlite3Store.Sqlite.Database(
// fileName,
// (err: any) => {
// if (err) {
// reject(err);
// return;
// }
//
// resolve(db);
// }
// );
// }
// );
// }
//
// prepareStatement(sql: string): Promise<Sqlite3Statement> {
// return new Promise(
// async(resolve, reject) => {
// const s = (await this.db).prepare(
// sql,
// (err: any) => {
// if (err) {
// reject(err);
// return;
// }
//
// resolve(s);
// }
// );
// }
// );
// }
//
//
// getProfile(name: string): Promise<ProfileRecord | undefined> {
// return new Promise(
// async(resolve, reject) => {
// (await this.stmtGetProfile).get(
// this.toProfileId(name),
// (err: any, data: any) => {
// if (err) {
// reject(err);
// return;
// }
//
// if (!data) {
// resolve();
// return;
// }
//
// // tslint:disable-next-line: no-unsafe-any
// data.profileData = JSON.parse(data.profileData) as ComplexCharacter;
//
// resolve(data as ProfileRecord);
// }
// );
// }
// );
// }
//
//
// protected run(stmtName: 'stmtStoreProfile' | 'stmtUpdateCounts', data: any[]): Promise<void> {
// return new Promise(
// async(resolve, reject) => {
// if (!(stmtName in this)) {
// throw new Error(`Unknown statement: ${stmtName}`);
// }
//
// (await this[stmtName]).run(
// ...data,
// (err: any) => {
// if (err) {
// reject(err);
// return;
// }
//
// resolve();
// }
// );
// }
// );
// }
//
//
// async start(): Promise<void> {
// await this.stop();
//
// await (await this.db).run(SQL.DatabaseMigration);
// }
//
//
// async stop(): Promise<void> {
// if (this.checkpointTimer) {
// clearInterval(this.checkpointTimer);
//
// this.checkpointTimer = null;
// }
// }
//
// }

56
learn/store/types.ts Normal file
View File

@ -0,0 +1,56 @@
// tslint:disable-next-line:no-duplicate-imports
// import * as path from 'path';
// import core from '../../chat/core';
import {Character as ComplexCharacter, CharacterGroup, Guestbook} from '../../site/character_page/interfaces';
import { CharacterImage, SimpleCharacter } from '../../interfaces';
import { FurryPreference, Gender, Orientation, Species } from '../matcher-types';
// This design should be refactored; it's bad
export interface ProfileRecord {
id: string;
name: string;
profileData: ComplexCharacter;
firstSeen: number;
lastFetched: number;
gender: Gender | null;
orientation: Orientation | null;
furryPreference: FurryPreference | null;
species: Species | null;
age: number | null;
domSubRole: number | null;
position: number | null;
// lastCounted: number | null;
// guestbookCount: number | null;
// friendCount: number | null;
// groupCount: number | null;
lastMetaFetched: number | null;
guestbook: Guestbook | null;
images: CharacterImage[] | null;
friends: SimpleCharacter[] | null;
groups: CharacterGroup[] | null;
}
// export type Statement = any;
// export type Database = any;
export interface PermanentIndexedStore {
getProfile(name: string): Promise<ProfileRecord | undefined>;
storeProfile(c: ComplexCharacter): Promise<void>;
updateProfileMeta(
name: string,
images: CharacterImage[] | null,
guestbook: Guestbook | null,
friends: SimpleCharacter[] | null,
groups: CharacterGroup[] | null
): Promise<void>;
flushProfiles(daysToExpire: number): Promise<void>;
start(): Promise<void>;
stop(): Promise<void>;
}

57
learn/store/worker.ts Normal file
View File

@ -0,0 +1,57 @@
import {Character as ComplexCharacter, CharacterGroup, Guestbook} from '../../site/character_page/interfaces';
import { PermanentIndexedStore, ProfileRecord } from './types';
import { CharacterImage, SimpleCharacter } from '../../interfaces';
import { WorkerClient } from './worker/client';
export class WorkerStore implements PermanentIndexedStore {
protected readonly workerClient: WorkerClient;
constructor(jsEndpointFile: string) {
this.workerClient = new WorkerClient(jsEndpointFile);
}
static async open(jsEndpointFile: string, dbName?: string): Promise<WorkerStore> {
const store = new WorkerStore(jsEndpointFile);
await store.workerClient.request('init', { dbName });
return store;
}
async getProfile(name: string): Promise<ProfileRecord | undefined> {
return this.workerClient.request('get', { name });
}
async storeProfile(character: ComplexCharacter): Promise<void> {
return this.workerClient.request('store', { character });
}
async updateProfileMeta(
name: string,
images: CharacterImage[] | null,
guestbook: Guestbook | null,
friends: SimpleCharacter[] | null,
groups: CharacterGroup[] | null
): Promise<void> {
return this.workerClient.request('update-meta', { name, images, guestbook, friends, groups });
}
async start(): Promise<void> {
return this.workerClient.request('start');
}
async stop(): Promise<void> {
return this.workerClient.request('stop');
}
async flushProfiles(daysToExpire: number): Promise<void> {
return this.workerClient.request('flush', { daysToExpire });
}
}

View File

@ -0,0 +1,98 @@
import _ from 'lodash';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import { IndexedRequest, IndexedResponse, ProfileStoreCommand } from './types';
export interface WaiterDef {
id: string;
resolve(result?: any): void;
reject(result?: any): void;
}
export class WorkerClient {
private readonly worker: Worker;
private idCounter = 0;
private waiters: WaiterDef[] = [];
constructor(jsFile: string) {
this.worker = new Worker(jsFile);
this.worker.onmessage = this.generateMessageProcessor();
}
private generateId(): string {
this.idCounter++;
return `wc-${this.idCounter}`;
}
private when(id: string, resolve: (result?: any) => void, reject: (reason?: any) => void): void {
this.waiters.push({ id, resolve, reject });
}
private generateMessageProcessor(): ((e: Event) => void) {
return (e: Event) => {
const res = (e as any).data as IndexedResponse;
log.silly('store.worker.client.msg', { res });
if (!res) {
log.error('store.worker.client.msg.invalid', { res });
return;
}
const waiter = _.find(this.waiters, (w) => (w.id === res.id));
if (!waiter) {
log.error('store.worker.client.msg.unknown', { res });
return;
}
if (res.state === 'ok') {
waiter.resolve(res.result);
} else {
waiter.reject(new Error(res.msg));
}
this.clearWaiter(waiter.id);
};
}
private clearWaiter(id: string): void {
this.waiters = _.filter(this.waiters, (w) => (w.id !== id));
}
async request(cmd: ProfileStoreCommand, params: Record<string, any> = {}): Promise<any> {
const id = this.generateId();
const request: IndexedRequest = {
cmd,
id,
params
};
return new Promise(
(resolve, reject) => {
try {
this.when(
id,
resolve,
reject
);
this.worker.postMessage(request);
} catch (err) {
reject(err);
this.clearWaiter(id);
}
}
);
}
}

View File

@ -0,0 +1,72 @@
import _ from 'lodash';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import { IndexedStore } from '../indexed';
import { IndexedRequest, ProfileStoreCommand } from './types';
type IndexedCallback = (params: Record<string, any>) => Promise<any>;
let indexed: IndexedStore;
const reply = (req: IndexedRequest, result?: any, err?: string | Error): void => {
const res: any = {
type: 'res',
id: req.id,
state: err ? 'err' : 'ok',
result
};
if (err) {
console.error(err);
console.error('store.worker.endpoint.error', { err });
res.msg = _.isString(err) ? err : err.message;
}
log.debug('store.worker.endpoint.reply', { req, res });
postMessage(res);
};
const generateMessageProcessor = () => {
const messageMapper: Record<ProfileStoreCommand, IndexedCallback> = {
flush: (params: Record<string, any>) => indexed.flushProfiles(params.daysToExpire),
start: () => indexed.start(),
stop: () => indexed.stop(),
get: (params: Record<string, any>) => indexed.getProfile(params.name),
store: (params: Record<string, any>) => indexed.storeProfile(params.character),
'update-meta': (params: Record<string, any>) =>
indexed.updateProfileMeta(params.name, params.images, params.guestbook, params.friends, params.groups),
init: async(params: Record<string, any>): Promise<void> => {
indexed = await IndexedStore.open(params.dbName);
}
};
return async(e: Event) => {
log.silly('store.worker.endpoint.msg', { e });
const req = (e as any).data as IndexedRequest;
if (!req) {
return;
}
if (!(req.cmd in messageMapper)) {
reply(req, undefined, 'unknown command');
return;
}
try {
const result = await messageMapper[req.cmd](req.params);
reply(req, result);
} catch(err) {
reply(req, undefined, err);
}
};
};
onmessage = generateMessageProcessor();

View File

@ -0,0 +1,17 @@
export type ProfileStoreCommand = 'flush' | 'start' | 'stop' | 'update-meta' | 'store' | 'get' | 'init';
export interface IndexedRequest {
cmd: ProfileStoreCommand;
id: string;
params: Record<string, any>;
}
export interface IndexedResponse {
id: string;
type: 'event' | 'res';
state: 'err' | 'ok';
result?: any;
msg?: string;
}