verify before fetching with socket

This commit is contained in:
Michael Peters 2022-02-08 22:57:49 -06:00
parent 33f880f4b1
commit 4f2ee4fbd7
6 changed files with 468 additions and 195 deletions

View File

@ -26,18 +26,25 @@ export class AutoVerifierWithArg<T, K> {
private tokenizer: (query: K) => string, // must be one-to-one mapping
private primaryFunc: (query: K) => Promise<T | null>,
private trustedFunc: (query: K) => Promise<T | null>,
private ensureTrustedFuncReady: () => Promise<void>,
private verifyFunc: (query: K, primaryResult: T | null, trustedResult: T | null) => Promise<boolean>,
) {}
static createStandardPartialMessageListAutoVerifier<T extends WithEquals<T> & { id: string }>(
primaryFunc: (query: PartialMessageListQuery) => Promise<T[] | null>,
trustedFunc: (query: PartialMessageListQuery) => Promise<T[] | null>,
changesFunc: (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<T>) => Promise<boolean>,
ensureTrustedFuncReady: () => Promise<void>,
changesFunc: (
query: PartialMessageListQuery,
changesType: AutoVerifierChangesType,
changes: Changes<T>,
) => Promise<boolean>,
) {
return new AutoVerifierWithArg<T[], PartialMessageListQuery>(
query => `ch#${query.channelId} mo#${query.messageOrderId}->${query.number}`,
query => primaryFunc(query),
query => trustedFunc(query),
ensureTrustedFuncReady,
async (query: PartialMessageListQuery, primaryResult: T[] | null, trustedResult: T[] | null) => {
// lOG.debug('messages verify: ', {
// query,
@ -59,12 +66,19 @@ export class AutoVerifierWithArg<T, K> {
static createStandardIDQueriedSingleAutoVerifier<T extends WithEquals<T> & { id: string }>(
primaryFunc: (query: IDQuery) => Promise<T | null>,
trustedFunc: (query: IDQuery) => Promise<T | null>,
changesFunc: (query: IDQuery, changesType: AutoVerifierChangesType, primaryResult: T | null, trustedResult: T | null) => Promise<boolean>,
ensureTrustedFuncReady: () => Promise<void>,
changesFunc: (
query: IDQuery,
changesType: AutoVerifierChangesType,
primaryResult: T | null,
trustedResult: T | null,
) => Promise<boolean>,
) {
return new AutoVerifierWithArg<T, IDQuery>(
query => `id#${query.id}`,
query => primaryFunc(query),
query => trustedFunc(query),
ensureTrustedFuncReady,
async (query: IDQuery, primaryResult: T | null, trustedResult: T | null) => {
const changesType = AutoVerifier.getSingleChangesType<T>(primaryResult, trustedResult);
return await changesFunc(query, changesType, primaryResult, trustedResult);
@ -106,7 +120,9 @@ export class AutoVerifierWithArg<T, K> {
autoVerifier = new AutoVerifier<T>(
async () => await this.primaryFunc(query),
async () => await this.trustedFunc(query),
async (primaryResult: T | null, trustedResult: T | null) => await this.verifyFunc(query, primaryResult, trustedResult),
this.ensureTrustedFuncReady,
async (primaryResult: T | null, trustedResult: T | null) =>
await this.verifyFunc(query, primaryResult, trustedResult),
);
this.tokenAutoVerifiers.set(token, autoVerifier);
}

View File

@ -36,12 +36,20 @@ export class AutoVerifier<T> {
* @param trustedFunc The trusted function that will verify the results of the primary function (one time)
* @param verifyFunc The verification function that is called when the primary function's results need to be verified against the trusted function's results. Must return true if the structure was updated, false if not.
*/
constructor(private primaryFunc: () => Promise<T | null>, private trustedFunc: () => Promise<T | null>, private verifyFunc: (primaryResult: T | null, trustedResult: T | null) => Promise<boolean>) {
constructor(
private primaryFunc: () => Promise<T | null>,
private trustedFunc: () => Promise<T | null>,
private ensureTrustedFuncReady: () => Promise<void>,
private verifyFunc: (primaryResult: T | null, trustedResult: T | null) => Promise<boolean>,
) {
this.verifierId = uuid.v4();
}
/** returns the changes that must be made to primaryResult given trustedResult */
static getChanges<T extends WithEquals<T> & { id: string }>(primaryResult: T[] | null, trustedResult: T[] | null): Changes<T> {
static getChanges<T extends WithEquals<T> & { id: string }>(
primaryResult: T[] | null,
trustedResult: T[] | null,
): Changes<T> {
const changes: Changes<T> = { added: [], updated: [], deleted: [] };
if (primaryResult === null && trustedResult === null) {
@ -77,7 +85,11 @@ export class AutoVerifier<T> {
return changes;
}
static getListChangesType<T>(primaryResult: T[] | null, trustedResult: T[] | null, changes: Changes<T>): AutoVerifierChangesType {
static getListChangesType<T>(
primaryResult: T[] | null,
trustedResult: T[] | null,
changes: Changes<T>,
): AutoVerifierChangesType {
if (primaryResult === null && trustedResult === null) {
return AutoVerifierChangesType.NONE;
} else if (trustedResult === null) {
@ -91,7 +103,10 @@ export class AutoVerifier<T> {
}
}
static getSingleChangesType<T extends WithEquals<T>>(primaryResult: T | null, trustedResult: T | null): AutoVerifierChangesType {
static getSingleChangesType<T extends WithEquals<T>>(
primaryResult: T | null,
trustedResult: T | null,
): AutoVerifierChangesType {
if (primaryResult === null && trustedResult === null) {
return AutoVerifierChangesType.NONE;
} else if (trustedResult === null) {
@ -108,24 +123,40 @@ export class AutoVerifier<T> {
static createStandardListAutoVerifier<T extends WithEquals<T> & { id: string }>(
primaryFunc: () => Promise<T[] | null>,
trustedFunc: () => Promise<T[] | null>,
ensureTrustedFuncReady: () => Promise<void>,
changesFunc: (changesType: AutoVerifierChangesType, changes: Changes<T>) => Promise<boolean>,
) {
return new AutoVerifier<T[]>(primaryFunc, trustedFunc, async (primaryResult: T[] | null, trustedResult: T[] | null) => {
return new AutoVerifier<T[]>(
primaryFunc,
trustedFunc,
ensureTrustedFuncReady,
async (primaryResult: T[] | null, trustedResult: T[] | null) => {
const changes = AutoVerifier.getChanges<T>(primaryResult, trustedResult);
const changesType = AutoVerifier.getListChangesType<T>(primaryResult, trustedResult, changes);
return await changesFunc(changesType, changes);
});
},
);
}
static createStandardSingleAutoVerifier<T extends WithEquals<T>>(
primaryFunc: () => Promise<T | null>,
trustedFunc: () => Promise<T | null>,
changesFunc: (changesType: AutoVerifierChangesType, primaryResult: T | null, trustedResult: T | null) => Promise<boolean>,
ensureTrustedFuncReady: () => Promise<void>,
changesFunc: (
changesType: AutoVerifierChangesType,
primaryResult: T | null,
trustedResult: T | null,
) => Promise<boolean>,
) {
return new AutoVerifier<T>(primaryFunc, trustedFunc, async (primaryResult: T | null, trustedResult: T | null) => {
return new AutoVerifier<T>(
primaryFunc,
trustedFunc,
ensureTrustedFuncReady,
async (primaryResult: T | null, trustedResult: T | null) => {
const changesType = AutoVerifier.getSingleChangesType<T>(primaryResult, trustedResult);
return await changesFunc(changesType, primaryResult, trustedResult);
});
},
);
}
// you CAN safely call this while another fetch is going on! How convenient
@ -152,7 +183,8 @@ export class AutoVerifier<T> {
// @param debug: print debug messages. This is useful if you (unfortunately) think there is a bug in this
async fetchAndVerifyIfNeeded(lazyVerify = false, debug = false): Promise<T | null> {
// eslint-disable-next-line no-async-promise-executor
return await new Promise<T | null>(async (resolve: (result: T | null) => void, reject: (error: Error) => void) => {
return await new Promise<T | null>(
async (resolve: (result: T | null) => void, reject: (error: Error) => void) => {
let resolved = false;
const fetchId = debug && `v#${this.verifierId} f#${uuid.v4()}`;
try {
@ -181,7 +213,11 @@ export class AutoVerifier<T> {
resolve(primaryResult);
resolved = true;
if (lazyVerify || this.trustedStatus === 'verified') {
if (debug) LOG.debug(fetchId + ': not waiting on trusted promise', { lazyVerify, trustedStatus: this.trustedStatus });
if (debug)
LOG.debug(fetchId + ': not waiting on trusted promise', {
lazyVerify,
trustedStatus: this.trustedStatus,
});
return;
}
}
@ -203,6 +239,8 @@ export class AutoVerifier<T> {
this.trustedStatus = 'verifying';
if (debug) LOG.debug(fetchId + ': verifying... (awaiting trustedPromise)');
await this.ensureTrustedFuncReady();
// note: Promises that have already resolved will return the same value when awaited again :)
const origTrustedPromise: Promise<T | null> | null = this.trustedPromise;
const trustedResult = await origTrustedPromise;
@ -211,8 +249,14 @@ export class AutoVerifier<T> {
// we've been invalidated while we were waiting for the trusted result!
// TODO: This happens when a socket fetch is sent before the socket is connected to.
if (debug) LOG.debug(fetchId + ': unverified during fetch!', new Error());
if (debug) LOG.debug(fetchId + ': trustedPromise now: ', { trustedPromise: this.trustedPromise });
console.warn('RARE ALERT: we got unverified while trying to fetch a trusted promise for verification!', new Error());
if (debug)
LOG.debug(fetchId + ': trustedPromise now: ', {
trustedPromise: this.trustedPromise,
});
console.warn(
'RARE ALERT: we got unverified while trying to fetch a trusted promise for verification!',
new Error(),
);
if (this.trustedPromise === null) {
if (debug) LOG.debug(fetchId + ': re-fetching since trustedPromise was null');
this.trustedStatus = 'fetching';
@ -263,14 +307,22 @@ export class AutoVerifier<T> {
if (this.trustedPromise !== origTrustedPromise) {
// we've been invalidated while we were waiting for the trusted result!
if (debug) LOG.debug(fetchId + ': got unverified while waiting on the result of a different verifier!', new Error());
console.warn('ULTRA RARE ALERT: we got unverified while awaiting a trusted promise another path was verifying!');
if (debug)
LOG.debug(
fetchId +
': got unverified while waiting on the result of a different verifier!',
new Error(),
);
console.warn(
'ULTRA RARE ALERT: we got unverified while awaiting a trusted promise another path was verifying!',
);
await tryResolveTrustedPromise();
return;
}
if (!resolved) {
if (debug) LOG.debug(fetchId + ': resolving with trusted result of different verifier');
if (debug)
LOG.debug(fetchId + ': resolving with trusted result of different verifier');
resolve(trustedResult);
resolved = true;
}
@ -293,10 +345,16 @@ export class AutoVerifier<T> {
reject(e as Error);
resolved = true;
} else {
if (debug) LOG.debug(fetchId + ': server request failed after returning cache value (or we already rejected)', e);
if (debug)
LOG.debug(
fetchId +
': server request failed after returning cache value (or we already rejected)',
e,
);
console.warn('server request failed after returning cache value (or when already rejected)', e);
}
}
});
},
);
}
}

View File

@ -4,13 +4,13 @@ import Logger from '../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import { Changes, Channel, GuildMetadata, Member, Message, Resource, Token } from './data-types';
import { AsyncFetchable, Fetchable, Lackable, Conflictable } from './guild-types';
import { AsyncFetchable, Fetchable, Lackable, Conflictable, Ensurable } from './guild-types';
import { AutoVerifier, AutoVerifierChangesType } from './auto-verifier';
import { AutoVerifierWithArg, PartialMessageListQuery, IDQuery } from './auto-verifier-with-args';
import { EventEmitter } from 'tsee';
/** uses a primary fetchable and trusted fetchable and verifys the results. Can be chained together with itself as a trusted fetchable! */
export default class PairVerifierFetchable extends EventEmitter<Conflictable> implements AsyncFetchable {
export default class PairVerifierFetchable extends EventEmitter<Conflictable> implements AsyncFetchable, Ensurable {
private readonly fetchMetadataVerifier: AutoVerifier<GuildMetadata>;
private readonly fetchMembersVerifier: AutoVerifier<Member[]>;
private readonly fetchChannelsVerifier: AutoVerifier<Channel[]>;
@ -22,7 +22,7 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
public readonly fetchMessagesBeforeVerifier: AutoVerifierWithArg<Message[], PartialMessageListQuery>;
public readonly fetchMessagesAfterVerifier: AutoVerifierWithArg<Message[], PartialMessageListQuery>;
constructor(private primary: Fetchable & Lackable, private trusted: Fetchable) {
constructor(private primary: Fetchable & Lackable, private trusted: Fetchable & Ensurable) {
super();
if (trusted instanceof PairVerifierFetchable) {
@ -38,18 +38,21 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
this.fetchMetadataVerifier = AutoVerifier.createStandardSingleAutoVerifier<GuildMetadata>(
async () => await this.primary.fetchMetadata(),
async () => await this.trusted.fetchMetadata(),
async () => await this.trusted.ensureVerified(),
this.handleMetadataConflict.bind(this),
);
this.fetchMembersVerifier = AutoVerifier.createStandardListAutoVerifier<Member>(
async () => await this.primary.fetchMembers(),
async () => await this.trusted.fetchMembers(),
async () => await this.trusted.ensureVerified(),
this.handleMembersConflict.bind(this),
);
this.fetchChannelsVerifier = AutoVerifier.createStandardListAutoVerifier<Channel>(
async () => await this.primary.fetchChannels(),
async () => await this.trusted.fetchChannels(),
async () => await this.trusted.ensureVerified(),
this.handleChannelsConflict.bind(this),
);
@ -58,35 +61,54 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
// async () => { LOG.debug('fetching primary tokens for ' + this.trusted.constructor.name); return await this.trusted.fetchTokens() },
async () => await this.primary.fetchTokens(),
async () => await this.trusted.fetchTokens(),
async () => await this.trusted.ensureVerified(),
this.handleTokensConflict.bind(this),
);
this.fetchResourceVerifier = AutoVerifierWithArg.createStandardIDQueriedSingleAutoVerifier<Resource>(
async (query: IDQuery) => await this.primary.fetchResource(query.id),
async (query: IDQuery) => await this.trusted.fetchResource(query.id),
async () => await this.trusted.ensureVerified(),
this.handleResourceConflict.bind(this),
);
this.fetchMessagesRecentVerifier = AutoVerifierWithArg.createStandardPartialMessageListAutoVerifier(
async (query: PartialMessageListQuery) => await this.primary.fetchMessagesRecent(query.channelId, query.number),
async (query: PartialMessageListQuery) => await this.trusted.fetchMessagesRecent(query.channelId, query.number),
async (query: PartialMessageListQuery) =>
await this.primary.fetchMessagesRecent(query.channelId, query.number),
async (query: PartialMessageListQuery) =>
await this.trusted.fetchMessagesRecent(query.channelId, query.number),
async () => await this.trusted.ensureVerified(),
this.handleMessagesConflict.bind(this),
);
this.fetchMessagesBeforeVerifier = AutoVerifierWithArg.createStandardPartialMessageListAutoVerifier(
async (query: PartialMessageListQuery) => await this.primary.fetchMessagesBefore(query.channelId, query.messageOrderId as string, query.number),
async (query: PartialMessageListQuery) => await this.trusted.fetchMessagesBefore(query.channelId, query.messageOrderId as string, query.number),
async (query: PartialMessageListQuery) =>
await this.primary.fetchMessagesBefore(query.channelId, query.messageOrderId as string, query.number),
async (query: PartialMessageListQuery) =>
await this.trusted.fetchMessagesBefore(query.channelId, query.messageOrderId as string, query.number),
async () => await this.trusted.ensureVerified(),
this.handleMessagesConflict.bind(this),
);
this.fetchMessagesAfterVerifier = AutoVerifierWithArg.createStandardPartialMessageListAutoVerifier(
async (query: PartialMessageListQuery) => await this.primary.fetchMessagesAfter(query.channelId, query.messageOrderId as string, query.number),
async (query: PartialMessageListQuery) => await this.trusted.fetchMessagesAfter(query.channelId, query.messageOrderId as string, query.number),
async (query: PartialMessageListQuery) =>
await this.primary.fetchMessagesAfter(query.channelId, query.messageOrderId as string, query.number),
async (query: PartialMessageListQuery) =>
await this.trusted.fetchMessagesAfter(query.channelId, query.messageOrderId as string, query.number),
async () => await this.trusted.ensureVerified(),
this.handleMessagesConflict.bind(this),
);
}
async handleMetadataConflict(changesType: AutoVerifierChangesType, primaryMetadata: GuildMetadata | null, trustedMetadata: GuildMetadata | null): Promise<boolean> {
async ensureVerified(): Promise<void> {
/* do nothing */
}
async handleMetadataConflict(
changesType: AutoVerifierChangesType,
primaryMetadata: GuildMetadata | null,
trustedMetadata: GuildMetadata | null,
): Promise<boolean> {
// lOG.debug('metadata conflict', {
// primaryClass: this.primary.constructor.name,
// trustedClass: this.trusted.constructor.name,
@ -99,7 +121,12 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
success = success && (await this.primary.handleMetadataChanged(trustedMetadata as GuildMetadata));
} else if (changesType === AutoVerifierChangesType.CONFLICT) {
success = success && (await this.primary.handleMetadataChanged(trustedMetadata as GuildMetadata));
this.emit('conflict-metadata', changesType, primaryMetadata as GuildMetadata, trustedMetadata as GuildMetadata);
this.emit(
'conflict-metadata',
changesType,
primaryMetadata as GuildMetadata,
trustedMetadata as GuildMetadata,
);
}
return success;
}
@ -107,7 +134,10 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
async handleMembersConflict(changesType: AutoVerifierChangesType, changes: Changes<Member>): Promise<boolean> {
let success = true;
if (changes.added.length > 0) success = success && (await this.primary.handleMembersAdded(changes.added));
if (changes.updated.length > 0) success = success && (await this.primary.handleMembersChanged(changes.updated.map(change => change.newDataPoint)));
if (changes.updated.length > 0)
success =
success &&
(await this.primary.handleMembersChanged(changes.updated.map(change => change.newDataPoint)));
if (changes.deleted.length > 0) success = success && (await this.primary.handleMembersDeleted(changes.deleted));
if (changesType === AutoVerifierChangesType.CONFLICT) {
@ -119,8 +149,12 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
async handleChannelsConflict(changesType: AutoVerifierChangesType, changes: Changes<Channel>): Promise<boolean> {
let success = true;
if (changes.added.length > 0) success = success && (await this.primary.handleChannelsAdded(changes.added));
if (changes.updated.length > 0) success = success && (await this.primary.handleChannelsChanged(changes.updated.map(change => change.newDataPoint)));
if (changes.deleted.length > 0) success = success && (await this.primary.handleChannelsDeleted(changes.deleted));
if (changes.updated.length > 0)
success =
success &&
(await this.primary.handleChannelsChanged(changes.updated.map(change => change.newDataPoint)));
if (changes.deleted.length > 0)
success = success && (await this.primary.handleChannelsDeleted(changes.deleted));
if (changesType === AutoVerifierChangesType.CONFLICT) {
this.emit('conflict-channels', changesType, changes);
@ -131,7 +165,9 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
async handleTokensConflict(changesType: AutoVerifierChangesType, changes: Changes<Token>): Promise<boolean> {
let success = true;
if (changes.added.length > 0) success = success && (await this.primary.handleTokensAdded(changes.added));
if (changes.updated.length > 0) success = success && (await this.primary.handleTokensChanged(changes.updated.map(change => change.newDataPoint)));
if (changes.updated.length > 0)
success =
success && (await this.primary.handleTokensChanged(changes.updated.map(change => change.newDataPoint)));
if (changes.deleted.length > 0) success = success && (await this.primary.handleTokensDeleted(changes.deleted));
if (changesType === AutoVerifierChangesType.CONFLICT) {
@ -140,22 +176,51 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
return success;
}
async handleResourceConflict(query: IDQuery, changesType: AutoVerifierChangesType, primaryResource: Resource | null, trustedResource: Resource | null): Promise<boolean> {
async handleResourceConflict(
query: IDQuery,
changesType: AutoVerifierChangesType,
primaryResource: Resource | null,
trustedResource: Resource | null,
): Promise<boolean> {
let success = true;
if (changesType === AutoVerifierChangesType.PRIMARY_ONLY) {
success = success && (await this.primary.handleResourceDeleted(trustedResource as Resource, this.fetchResourceVerifier));
success =
success &&
(await this.primary.handleResourceDeleted(trustedResource as Resource, this.fetchResourceVerifier));
} else if (changesType === AutoVerifierChangesType.TRUSTED_ONLY) {
success = success && (await this.primary.handleResourceAdded(trustedResource as Resource, this.fetchResourceVerifier));
success =
success &&
(await this.primary.handleResourceAdded(trustedResource as Resource, this.fetchResourceVerifier));
} else if (changesType === AutoVerifierChangesType.CONFLICT) {
success = success && (await this.primary.handleResourceChanged(trustedResource as Resource, this.fetchResourceVerifier));
this.emit('conflict-resource', query, changesType, primaryResource as Resource, trustedResource as Resource);
success =
success &&
(await this.primary.handleResourceChanged(trustedResource as Resource, this.fetchResourceVerifier));
this.emit(
'conflict-resource',
query,
changesType,
primaryResource as Resource,
trustedResource as Resource,
);
}
return success;
}
async handleMessagesConflict(query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>): Promise<boolean> {
async handleMessagesConflict(
query: PartialMessageListQuery,
changesType: AutoVerifierChangesType,
changes: Changes<Message>,
): Promise<boolean> {
let success = true;
if (changes.added.length > 0) success = success && (await this.primary.handleMessagesAdded(changes.added, this.fetchMessagesRecentVerifier, this.fetchMessagesBeforeVerifier, this.fetchMessagesAfterVerifier));
if (changes.added.length > 0)
success =
success &&
(await this.primary.handleMessagesAdded(
changes.added,
this.fetchMessagesRecentVerifier,
this.fetchMessagesBeforeVerifier,
this.fetchMessagesAfterVerifier,
));
if (changes.updated.length > 0)
success =
success &&
@ -165,7 +230,15 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
this.fetchMessagesBeforeVerifier,
this.fetchMessagesAfterVerifier,
));
if (changes.deleted.length > 0) success = success && (await this.primary.handleMessagesDeleted(changes.deleted, this.fetchMessagesRecentVerifier, this.fetchMessagesBeforeVerifier, this.fetchMessagesAfterVerifier));
if (changes.deleted.length > 0)
success =
success &&
(await this.primary.handleMessagesDeleted(
changes.deleted,
this.fetchMessagesRecentVerifier,
this.fetchMessagesBeforeVerifier,
this.fetchMessagesAfterVerifier,
));
if (changesType === AutoVerifierChangesType.CONFLICT) {
this.emit('conflict-messages', query, changesType, changes);
@ -200,7 +273,11 @@ export default class PairVerifierFetchable extends EventEmitter<Conflictable> im
return await this.fetchChannelsVerifier.fetchAndVerifyIfNeeded();
}
async fetchMessagesRecent(channelId: string, number: number): Promise<Message[] | null> {
return await this.fetchMessagesRecentVerifier.fetchAndVerifyIfNeded({ channelId, messageOrderId: null, number });
return await this.fetchMessagesRecentVerifier.fetchAndVerifyIfNeded({
channelId,
messageOrderId: null,
number,
});
}
async fetchMessagesBefore(channelId: string, messageOrderId: string, number: number): Promise<Message[] | null> {
return await this.fetchMessagesBeforeVerifier.fetchAndVerifyIfNeded({ channelId, messageOrderId, number });

View File

@ -1,11 +1,20 @@
import { AsyncFetchable, AsyncLackable } from './guild-types';
import { Channel, GuildMetadata, GuildMetadataLocal, Member, Message, Resource, SocketConfig, Token } from './data-types';
import { AsyncFetchable, AsyncLackable, Ensurable } from './guild-types';
import {
Channel,
GuildMetadata,
GuildMetadataLocal,
Member,
Message,
Resource,
SocketConfig,
Token,
} from './data-types';
import PersonalDB from './personal-db';
import { AutoVerifierWithArg, PartialMessageListQuery } from './auto-verifier-with-args';
/** a guild connected to a local Sqlite database */
export default class PersonalDBGuild implements AsyncFetchable, AsyncLackable {
export default class PersonalDBGuild implements AsyncFetchable, AsyncLackable, Ensurable {
constructor(
private readonly db: PersonalDB,
private readonly guildId: number,
@ -50,6 +59,10 @@ export default class PersonalDBGuild implements AsyncFetchable, AsyncLackable {
return null; // personal db currently does not handle tokens
}
async ensureVerified(): Promise<void> {
/* do nothing, we are always ready */
}
// lacking Methods (resolving differences)
async handleMetadataChanged(changedMetaData: GuildMetadata): Promise<boolean> {

View File

@ -7,7 +7,7 @@ const LOG = Logger.create(__filename, electronConsole);
import * as socketio from 'socket.io-client';
import { Channel, GuildMetadata, Member, Message, Resource, Token } from './data-types';
import Globals from './globals';
import { Connectable, AsyncRequestable, AsyncGuaranteedFetchable } from './guild-types';
import { Connectable, AsyncRequestable, AsyncGuaranteedFetchable, Ensurable } from './guild-types';
import DedupAwaiter from './dedup-awaiter';
import Util from './util';
import SocketVerifier from './socket-verifier';
@ -15,7 +15,10 @@ import { EventEmitter } from 'tsee';
// note: you should not be calling the eventemitter functions on outside classes
/** a guild connected to a socket.io socket server */
export default class SocketGuild extends EventEmitter<Connectable> implements AsyncGuaranteedFetchable, AsyncRequestable {
export default class SocketGuild
extends EventEmitter<Connectable>
implements AsyncGuaranteedFetchable, AsyncRequestable, Ensurable
{
private queryDedups = new Map<string, DedupAwaiter<unknown>>();
constructor(private socket: socketio.Socket, public verifier: SocketVerifier) {
@ -86,21 +89,41 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
this.socket.disconnect();
}
public async ensureVerified(): Promise<void> {
await this.verifier.ensureVerified();
}
// server helper functions
private async _query(timeout: number, endpoint: string, ...args: any[]): Promise<any> {
if (!this.verifier.isVerified) {
throw new Error(`attempted to make query before verified @${endpoint} / [${args.map(arg => LOG.inspect(arg)).join(', ')}]`);
throw new Error(
`attempted to make query before verified @${endpoint} / [${args
.map(arg => LOG.inspect(arg))
.join(', ')}]`,
);
}
LOG.silly(`query@${endpoint} / [${args.map(arg => LOG.inspect(arg)).join(', ')}]`);
return await new Promise((resolve, reject) => {
Util.socketEmitTimeout(this.socket, timeout, endpoint, ...args, (errMsg: string | null, serverData: any) => {
Util.socketEmitTimeout(
this.socket,
timeout,
endpoint,
...args,
(errMsg: string | null, serverData: any) => {
if (errMsg) {
reject(new Error(`error fetching server data @${endpoint} / [${args.map(arg => LOG.inspect(arg)).join(', ')}]: ${errMsg}`));
reject(
new Error(
`error fetching server data @${endpoint} / [${args
.map(arg => LOG.inspect(arg))
.join(', ')}]: ${errMsg}`,
),
);
} else {
resolve(serverData);
}
});
},
);
});
}
@ -110,7 +133,11 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
}
// note: timeout is only respected for the first deduped request
private async queryOnceVerifiedDedup(timeout: number, endpoint: string, ...args: (string | number)[]): Promise<any> {
private async queryOnceVerifiedDedup(
timeout: number,
endpoint: string,
...args: (string | number)[]
): Promise<any> {
const id = `query-@${endpoint}(${args.join(',')})`;
let dedup = this.queryDedups.get(id);
if (!dedup) {
@ -141,15 +168,32 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
return data.map((dataChannel: any) => Channel.fromDBData(dataChannel));
}
async fetchMessagesRecent(channelId: string, number: number): Promise<Message[]> {
const data = await this.queryOnceVerifiedDedup(Globals.DEFAULT_SOCKET_TIMEOUT, 'fetch-messages-recent', channelId, number);
const data = await this.queryOnceVerifiedDedup(
Globals.DEFAULT_SOCKET_TIMEOUT,
'fetch-messages-recent',
channelId,
number,
);
return data.map((dataMessage: any) => Message.fromDBData(dataMessage));
}
async fetchMessagesBefore(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
const data = await this.queryOnceVerifiedDedup(Globals.DEFAULT_SOCKET_TIMEOUT, 'fetch-messages-before', channelId, messageOrderId, number);
const data = await this.queryOnceVerifiedDedup(
Globals.DEFAULT_SOCKET_TIMEOUT,
'fetch-messages-before',
channelId,
messageOrderId,
number,
);
return data.map((dataMessage: any) => Message.fromDBData(dataMessage));
}
async fetchMessagesAfter(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
const data = await this.queryOnceVerifiedDedup(Globals.DEFAULT_SOCKET_TIMEOUT, 'fetch-messages-after', channelId, messageOrderId, number);
const data = await this.queryOnceVerifiedDedup(
Globals.DEFAULT_SOCKET_TIMEOUT,
'fetch-messages-after',
channelId,
messageOrderId,
number,
);
return data.map((dataMessage: any) => Message.fromDBData(dataMessage));
}
async fetchResource(resourceId: string): Promise<Resource> {
@ -163,10 +207,27 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
// we don't want to dedup these
async requestSendMessage(channelId: string, text: string): Promise<void> {
/*const _dataMessage = */ await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'send-message', channelId, text);
/*const _dataMessage = */ await this.queryOnceVerified(
Globals.DEFAULT_SOCKET_TIMEOUT,
'send-message',
channelId,
text,
);
}
async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise<void> {
/*const _dataMessage = */ await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'send-message-with-resource', channelId, text, resource, resourceName);
async requestSendMessageWithResource(
channelId: string,
text: string | null,
resource: Buffer,
resourceName: string,
): Promise<void> {
/*const _dataMessage = */ await this.queryOnceVerified(
Globals.DEFAULT_SOCKET_TIMEOUT,
'send-message-with-resource',
channelId,
text,
resource,
resourceName,
);
}
async requestSetStatus(status: string): Promise<void> {
await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-status', status);
@ -184,11 +245,22 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-icon', guildIcon);
}
async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<Channel> {
const dataChangedChannel = await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'update-channel', channelId, name, flavorText);
const dataChangedChannel = await this.queryOnceVerified(
Globals.DEFAULT_SOCKET_TIMEOUT,
'update-channel',
channelId,
name,
flavorText,
);
return Channel.fromDBData(dataChangedChannel);
}
async requestDoCreateChannel(name: string, flavorText: string | null): Promise<Channel> {
const dataNewChannel = await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'create-text-channel', name, flavorText);
const dataNewChannel = await this.queryOnceVerified(
Globals.DEFAULT_SOCKET_TIMEOUT,
'create-text-channel',
name,
flavorText,
);
return Channel.fromDBData(dataNewChannel);
}
async requestDoRevokeToken(token: string): Promise<void> {

View File

@ -26,6 +26,10 @@ export interface AsyncFetchable {
}
export type Fetchable = SyncFetchable | AsyncFetchable;
export interface Ensurable {
ensureVerified: () => Promise<void>;
}
export interface AsyncGuaranteedFetchable {
fetchMetadata(): Promise<GuildMetadata>;
fetchMembers(): Promise<Member[]>;
@ -42,7 +46,12 @@ export type GuaranteedFetchable = AsyncGuaranteedFetchable;
export interface AsyncRequestable {
requestSendMessage(channelId: string, text: string): Promise<void>;
requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise<void>;
requestSendMessageWithResource(
channelId: string,
text: string | null,
resource: Buffer,
resourceName: string,
): Promise<void>;
requestSetStatus(status: string): Promise<void>;
requestSetDisplayName(displayName: string): Promise<void>;
requestSetAvatar(avatar: Buffer): Promise<void>;
@ -91,9 +100,18 @@ export interface AsyncLackable {
afterAutoVerifier: AutoVerifierWithArg<Message[], PartialMessageListQuery>,
): Promise<boolean>;
handleResourceAdded(addedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>): Promise<boolean>;
handleResourceChanged(changedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>): Promise<boolean>;
handleResourceDeleted(deletedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>): Promise<boolean>;
handleResourceAdded(
addedResource: Resource,
resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>,
): Promise<boolean>;
handleResourceChanged(
changedResource: Resource,
resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>,
): Promise<boolean>;
handleResourceDeleted(
deletedResource: Resource,
resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>,
): Promise<boolean>;
handleTokensAdded(addedTokens: Token[]): Promise<boolean>;
handleTokensChanged(changedTokens: Token[]): Promise<boolean>;
@ -131,8 +149,14 @@ export interface SyncLackable {
): boolean;
handleResourceAdded(addedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>): boolean;
handleResourceChanged(changedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>): boolean;
handleResourceDeleted(deletedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>): boolean;
handleResourceChanged(
changedResource: Resource,
resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>,
): boolean;
handleResourceDeleted(
deletedResource: Resource,
resourceAutoVerifier: AutoVerifierWithArg<Resource, IDQuery>,
): boolean;
handleTokensAdded(addedTokens: Token[]): boolean;
handleTokensChanged(changedTokens: Token[]): boolean;
@ -172,12 +196,25 @@ export type Connectable = {
// a Conflictable could emit conflict-based events if data changed based on verification
// these events should be emitted *after* the conflicts have been resolved
export type Conflictable = {
'conflict-metadata': (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void;
'conflict-metadata': (
changesType: AutoVerifierChangesType,
oldGuildMeta: GuildMetadata,
newGuildMeta: GuildMetadata,
) => void;
'conflict-channels': (changesType: AutoVerifierChangesType, changes: Changes<Channel>) => void;
'conflict-members': (changesType: AutoVerifierChangesType, changes: Changes<Member>) => void;
'conflict-tokens': (changesType: AutoVerifierChangesType, changes: Changes<Token>) => void;
'conflict-resource': (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => void;
'conflict-messages': (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>) => void;
'conflict-resource': (
query: IDQuery,
changesType: AutoVerifierChangesType,
oldResource: Resource,
newResource: Resource,
) => void;
'conflict-messages': (
query: PartialMessageListQuery,
changesType: AutoVerifierChangesType,
changes: Changes<Message>,
) => void;
};
export const GuildEventNames = [