268 lines
8.6 KiB
TypeScript
268 lines
8.6 KiB
TypeScript
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
|
|
import * as _ from 'lodash';
|
|
|
|
import {Character as ComplexCharacter, CharacterGroup, Guestbook} from '../../site/character_page/interfaces';
|
|
import { CharacterAnalysis } from '../matcher';
|
|
import { PermanentIndexedStore, ProfileRecord } from './types';
|
|
import { CharacterImage, SimpleCharacter } from '../../interfaces';
|
|
|
|
|
|
async function promisifyRequest<T>(req: IDBRequest): Promise<T> {
|
|
return new Promise<T>((resolve, reject) => {
|
|
req.onsuccess = () => resolve(<T>req.result);
|
|
req.onerror = () => reject(req.error);
|
|
});
|
|
}
|
|
|
|
|
|
export class IndexedStore implements PermanentIndexedStore {
|
|
protected dbName: string;
|
|
protected db: IDBDatabase;
|
|
|
|
protected static readonly STORE_NAME = 'profiles';
|
|
protected static readonly LAST_FETCHED_INDEX_NAME = 'idxLastFetched';
|
|
|
|
constructor(db: IDBDatabase, dbName: string) {
|
|
this.dbName = dbName;
|
|
this.db = db;
|
|
}
|
|
|
|
static async open(dbName: string = 'flist-ascending-profiles'): Promise<IndexedStore> {
|
|
const request = indexedDB.open(dbName, 3);
|
|
|
|
request.onupgradeneeded = async(event) => {
|
|
const db = request.result;
|
|
|
|
if (event.oldVersion < 1) {
|
|
db.createObjectStore(IndexedStore.STORE_NAME, { keyPath: 'id' });
|
|
}
|
|
|
|
if (event.oldVersion < 2) {
|
|
const store = request.transaction!.objectStore(IndexedStore.STORE_NAME);
|
|
|
|
store.createIndex(
|
|
IndexedStore.LAST_FETCHED_INDEX_NAME,
|
|
'lastFetched',
|
|
{
|
|
unique: false,
|
|
multiEntry: false
|
|
}
|
|
);
|
|
}
|
|
|
|
if (event.oldVersion < 3) {
|
|
const store = request.transaction!.objectStore(IndexedStore.STORE_NAME);
|
|
const req = store.clear();
|
|
|
|
await promisifyRequest(req);
|
|
}
|
|
};
|
|
|
|
return new IndexedStore(await promisifyRequest<IDBDatabase>(request), dbName);
|
|
}
|
|
|
|
|
|
// tslint:disable-next-line prefer-function-over-method
|
|
protected toProfileId(name: string): string {
|
|
return name.toLowerCase();
|
|
}
|
|
|
|
|
|
async getProfile(name: string): Promise<ProfileRecord | undefined> {
|
|
const tx = this.db.transaction(IndexedStore.STORE_NAME, 'readonly');
|
|
const store = tx.objectStore(IndexedStore.STORE_NAME);
|
|
const getRequest = store.get(this.toProfileId(name));
|
|
|
|
// tslint:disable-next-line no-any
|
|
const data = await promisifyRequest<any>(getRequest);
|
|
|
|
if (!data) {
|
|
// console.info('IDX empty profile', name);
|
|
return;
|
|
}
|
|
|
|
// tslint:disable-next-line: no-unsafe-any
|
|
data.profileData = data.profileData as ComplexCharacter;
|
|
|
|
// fix to clean out extra customs that somehow sometimes appear:
|
|
if (_.isArray(data.profileData.character.customs)) {
|
|
log.warn('character.customs.strange.indexed.getProfile', {name: data.profileData.character.name, data, customs: data.profileData.character.customs});
|
|
data.profileData.character.customs = {};
|
|
await this.storeProfile(data.profileData);
|
|
}
|
|
|
|
// console.log('IDX profile', name, data);
|
|
|
|
return data as ProfileRecord;
|
|
}
|
|
|
|
|
|
private async prepareProfileData(c: ComplexCharacter): Promise<ProfileRecord> {
|
|
const existing = await this.getProfile(c.character.name);
|
|
const ca = new CharacterAnalysis(c.character);
|
|
|
|
// fix to clean out extra customs that somehow sometimes appear:
|
|
if (_.isArray(c.character.customs) || !_.isPlainObject(c.character.customs)) {
|
|
log.debug('character.customs.strange.indexed.prepareProfileData', {name: c.character.name, c, customs: c.character.customs});
|
|
c.character.customs = {};
|
|
}
|
|
|
|
const data: ProfileRecord = {
|
|
id: this.toProfileId(c.character.name),
|
|
name: c.character.name,
|
|
profileData: c,
|
|
firstSeen: Math.round(Date.now() / 1000),
|
|
lastFetched: Math.round(Date.now() / 1000),
|
|
gender: ca.gender,
|
|
orientation: ca.orientation,
|
|
furryPreference: ca.furryPreference,
|
|
species: ca.species,
|
|
age: ca.age,
|
|
domSubRole: ca.subDomRole, // domSubRole
|
|
position: ca.position, // position
|
|
|
|
lastMetaFetched: null,
|
|
guestbook: null,
|
|
images: null,
|
|
friends: null,
|
|
groups: null
|
|
|
|
// lastCounted: null,
|
|
// guestbookCount: null,
|
|
// friendCount: null,
|
|
// groupCount: null
|
|
};
|
|
|
|
return (existing)
|
|
? _.merge(existing, data, _.pick(existing, ['firstSeen', 'lastMetaFetched', 'guestbook', 'images', 'friends', 'groups']))
|
|
: data;
|
|
}
|
|
|
|
|
|
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);
|
|
const putRequest = store.put(data);
|
|
|
|
// tslint:disable-next-line no-any
|
|
await promisifyRequest<any>(putRequest);
|
|
|
|
// console.log('IDX store profile', c.character.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(
|
|
name: string,
|
|
images: CharacterImage[] | null,
|
|
guestbook: Guestbook | null,
|
|
friends: SimpleCharacter[] | null,
|
|
groups: CharacterGroup[] | null
|
|
): Promise<void> {
|
|
const existing = await this.getProfile(name);
|
|
|
|
if (!existing) {
|
|
return;
|
|
}
|
|
|
|
const data = _.merge(
|
|
existing,
|
|
{
|
|
lastMetaFetched: Math.round(Date.now() / 1000),
|
|
guestbook,
|
|
friends,
|
|
groups,
|
|
images
|
|
}
|
|
);
|
|
|
|
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 start(): Promise<void> {
|
|
// empty
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
// empty
|
|
}
|
|
|
|
|
|
async flushProfiles(daysToExpire: number): Promise<void> {
|
|
const tx = this.db.transaction(IndexedStore.STORE_NAME, 'readwrite');
|
|
const store = tx.objectStore(IndexedStore.STORE_NAME);
|
|
const idx = store.index(IndexedStore.LAST_FETCHED_INDEX_NAME);
|
|
|
|
const totalRecords = await promisifyRequest<number>(store.count());
|
|
|
|
const expirationTime = Math.round(Date.now() / 1000) - (daysToExpire * 24 * 60 * 60);
|
|
const getAllKeysRequest = idx.getAllKeys(IDBKeyRange.upperBound(expirationTime));
|
|
const result = await promisifyRequest<IDBValidKey[]>(getAllKeysRequest);
|
|
|
|
log.info('character.cache.expire', {daysToExpire, totalRecords, removableRecords: result.length});
|
|
|
|
return new Promise(
|
|
(resolve, reject) => {
|
|
const gen = (index: number): void => {
|
|
if(index >= result.length) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const pk = result[index];
|
|
log.silly('character.cache.expire.name', { name: pk });
|
|
|
|
const req = store.delete(pk);
|
|
|
|
req.onsuccess = () => gen(index + 1);
|
|
req.onerror = reject;
|
|
};
|
|
|
|
gen(0);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|