From 4f2ee4fbd7e9ba3d8d7964efa2cda801c2dc805d Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Tue, 8 Feb 2022 22:57:49 -0600 Subject: [PATCH] verify before fetching with socket --- src/client/webapp/auto-verifier-with-args.ts | 22 +- src/client/webapp/auto-verifier.ts | 332 +++++++++++-------- src/client/webapp/fetchable-pair-verifier.ts | 125 +++++-- src/client/webapp/guild-personal-db.ts | 19 +- src/client/webapp/guild-socket.ts | 110 ++++-- src/client/webapp/guild-types.ts | 55 ++- 6 files changed, 468 insertions(+), 195 deletions(-) diff --git a/src/client/webapp/auto-verifier-with-args.ts b/src/client/webapp/auto-verifier-with-args.ts index 98dd71e..7f00ca1 100644 --- a/src/client/webapp/auto-verifier-with-args.ts +++ b/src/client/webapp/auto-verifier-with-args.ts @@ -26,18 +26,25 @@ export class AutoVerifierWithArg { private tokenizer: (query: K) => string, // must be one-to-one mapping private primaryFunc: (query: K) => Promise, private trustedFunc: (query: K) => Promise, + private ensureTrustedFuncReady: () => Promise, private verifyFunc: (query: K, primaryResult: T | null, trustedResult: T | null) => Promise, ) {} static createStandardPartialMessageListAutoVerifier & { id: string }>( primaryFunc: (query: PartialMessageListQuery) => Promise, trustedFunc: (query: PartialMessageListQuery) => Promise, - changesFunc: (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes) => Promise, + ensureTrustedFuncReady: () => Promise, + changesFunc: ( + query: PartialMessageListQuery, + changesType: AutoVerifierChangesType, + changes: Changes, + ) => Promise, ) { return new AutoVerifierWithArg( 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 { static createStandardIDQueriedSingleAutoVerifier & { id: string }>( primaryFunc: (query: IDQuery) => Promise, trustedFunc: (query: IDQuery) => Promise, - changesFunc: (query: IDQuery, changesType: AutoVerifierChangesType, primaryResult: T | null, trustedResult: T | null) => Promise, + ensureTrustedFuncReady: () => Promise, + changesFunc: ( + query: IDQuery, + changesType: AutoVerifierChangesType, + primaryResult: T | null, + trustedResult: T | null, + ) => Promise, ) { return new AutoVerifierWithArg( query => `id#${query.id}`, query => primaryFunc(query), query => trustedFunc(query), + ensureTrustedFuncReady, async (query: IDQuery, primaryResult: T | null, trustedResult: T | null) => { const changesType = AutoVerifier.getSingleChangesType(primaryResult, trustedResult); return await changesFunc(query, changesType, primaryResult, trustedResult); @@ -106,7 +120,9 @@ export class AutoVerifierWithArg { autoVerifier = new AutoVerifier( 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); } diff --git a/src/client/webapp/auto-verifier.ts b/src/client/webapp/auto-verifier.ts index b5a98bb..1de2619 100644 --- a/src/client/webapp/auto-verifier.ts +++ b/src/client/webapp/auto-verifier.ts @@ -36,12 +36,20 @@ export class AutoVerifier { * @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, private trustedFunc: () => Promise, private verifyFunc: (primaryResult: T | null, trustedResult: T | null) => Promise) { + constructor( + private primaryFunc: () => Promise, + private trustedFunc: () => Promise, + private ensureTrustedFuncReady: () => Promise, + private verifyFunc: (primaryResult: T | null, trustedResult: T | null) => Promise, + ) { this.verifierId = uuid.v4(); } /** returns the changes that must be made to primaryResult given trustedResult */ - static getChanges & { id: string }>(primaryResult: T[] | null, trustedResult: T[] | null): Changes { + static getChanges & { id: string }>( + primaryResult: T[] | null, + trustedResult: T[] | null, + ): Changes { const changes: Changes = { added: [], updated: [], deleted: [] }; if (primaryResult === null && trustedResult === null) { @@ -77,7 +85,11 @@ export class AutoVerifier { return changes; } - static getListChangesType(primaryResult: T[] | null, trustedResult: T[] | null, changes: Changes): AutoVerifierChangesType { + static getListChangesType( + primaryResult: T[] | null, + trustedResult: T[] | null, + changes: Changes, + ): AutoVerifierChangesType { if (primaryResult === null && trustedResult === null) { return AutoVerifierChangesType.NONE; } else if (trustedResult === null) { @@ -91,7 +103,10 @@ export class AutoVerifier { } } - static getSingleChangesType>(primaryResult: T | null, trustedResult: T | null): AutoVerifierChangesType { + static getSingleChangesType>( + 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 { static createStandardListAutoVerifier & { id: string }>( primaryFunc: () => Promise, trustedFunc: () => Promise, + ensureTrustedFuncReady: () => Promise, changesFunc: (changesType: AutoVerifierChangesType, changes: Changes) => Promise, ) { - return new AutoVerifier(primaryFunc, trustedFunc, async (primaryResult: T[] | null, trustedResult: T[] | null) => { - const changes = AutoVerifier.getChanges(primaryResult, trustedResult); - const changesType = AutoVerifier.getListChangesType(primaryResult, trustedResult, changes); - return await changesFunc(changesType, changes); - }); + return new AutoVerifier( + primaryFunc, + trustedFunc, + ensureTrustedFuncReady, + async (primaryResult: T[] | null, trustedResult: T[] | null) => { + const changes = AutoVerifier.getChanges(primaryResult, trustedResult); + const changesType = AutoVerifier.getListChangesType(primaryResult, trustedResult, changes); + return await changesFunc(changesType, changes); + }, + ); } static createStandardSingleAutoVerifier>( primaryFunc: () => Promise, trustedFunc: () => Promise, - changesFunc: (changesType: AutoVerifierChangesType, primaryResult: T | null, trustedResult: T | null) => Promise, + ensureTrustedFuncReady: () => Promise, + changesFunc: ( + changesType: AutoVerifierChangesType, + primaryResult: T | null, + trustedResult: T | null, + ) => Promise, ) { - return new AutoVerifier(primaryFunc, trustedFunc, async (primaryResult: T | null, trustedResult: T | null) => { - const changesType = AutoVerifier.getSingleChangesType(primaryResult, trustedResult); - return await changesFunc(changesType, primaryResult, trustedResult); - }); + return new AutoVerifier( + primaryFunc, + trustedFunc, + ensureTrustedFuncReady, + async (primaryResult: T | null, trustedResult: T | null) => { + const changesType = AutoVerifier.getSingleChangesType(primaryResult, trustedResult); + return await changesFunc(changesType, primaryResult, trustedResult); + }, + ); } // you CAN safely call this while another fetch is going on! How convenient @@ -152,151 +183,178 @@ export class AutoVerifier { // @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 { // eslint-disable-next-line no-async-promise-executor - return await new Promise(async (resolve: (result: T | null) => void, reject: (error: Error) => void) => { - let resolved = false; - const fetchId = debug && `v#${this.verifierId} f#${uuid.v4()}`; - try { - if (this.primaryPromise === null) { - if (debug) LOG.debug(fetchId + ': created primary promise'); - this.primaryPromise = this.primaryFunc(); - } - const origPrimaryPromise = this.primaryPromise; - - // pre-fetch the trusted result while we fetch the primary result - if (!lazyVerify && this.trustedStatus === 'none') { - if (debug) LOG.debug(fetchId + ": created trusted promise, set to 'fetching'"); - this.trustedStatus = 'fetching'; - this.trustedPromise = this.trustedFunc(); - } - - const primaryResult = await this.primaryPromise; - if (this.primaryPromise === origPrimaryPromise) { - // reset the primary promise so we create a new one next time - if (debug) LOG.debug('reset the primary promise for next time'); - this.primaryPromise = null; - } - - if (primaryResult) { - if (debug) LOG.debug('resolving with primary result'); - resolve(primaryResult); - resolved = true; - if (lazyVerify || this.trustedStatus === 'verified') { - if (debug) LOG.debug(fetchId + ': not waiting on trusted promise', { lazyVerify, trustedStatus: this.trustedStatus }); - return; + return await new Promise( + async (resolve: (result: T | null) => void, reject: (error: Error) => void) => { + let resolved = false; + const fetchId = debug && `v#${this.verifierId} f#${uuid.v4()}`; + try { + if (this.primaryPromise === null) { + if (debug) LOG.debug(fetchId + ': created primary promise'); + this.primaryPromise = this.primaryFunc(); } - } + const origPrimaryPromise = this.primaryPromise; - if (this.trustedStatus === 'none') { - // try to re-fetch the trusted result - if (debug) LOG.debug(fetchId + ": creating trusted result (since status is 'none'"); - this.trustedStatus = 'fetching'; - this.trustedPromise = this.trustedFunc(); - } + // pre-fetch the trusted result while we fetch the primary result + if (!lazyVerify && this.trustedStatus === 'none') { + if (debug) LOG.debug(fetchId + ": created trusted promise, set to 'fetching'"); + this.trustedStatus = 'fetching'; + this.trustedPromise = this.trustedFunc(); + } - const tryResolveTrustedPromise = async () => { - if (this.trustedPromise) { - // there is a trusted promise that we can check + const primaryResult = await this.primaryPromise; + if (this.primaryPromise === origPrimaryPromise) { + // reset the primary promise so we create a new one next time + if (debug) LOG.debug('reset the primary promise for next time'); + this.primaryPromise = null; + } - if (this.trustedStatus === 'fetching') { - // no one has started verifying the trusted yet + if (primaryResult) { + if (debug) LOG.debug('resolving with primary result'); + resolve(primaryResult); + resolved = true; + if (lazyVerify || this.trustedStatus === 'verified') { + if (debug) + LOG.debug(fetchId + ': not waiting on trusted promise', { + lazyVerify, + trustedStatus: this.trustedStatus, + }); + return; + } + } - this.trustedStatus = 'verifying'; - if (debug) LOG.debug(fetchId + ': verifying... (awaiting trustedPromise)'); + if (this.trustedStatus === 'none') { + // try to re-fetch the trusted result + if (debug) LOG.debug(fetchId + ": creating trusted result (since status is 'none'"); + this.trustedStatus = 'fetching'; + this.trustedPromise = this.trustedFunc(); + } - // note: Promises that have already resolved will return the same value when awaited again :) - const origTrustedPromise: Promise | null = this.trustedPromise; - const trustedResult = await origTrustedPromise; + const tryResolveTrustedPromise = async () => { + if (this.trustedPromise) { + // there is a trusted promise that we can check - if (this.trustedPromise !== origTrustedPromise) { - // 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 (this.trustedPromise === null) { - if (debug) LOG.debug(fetchId + ': re-fetching since trustedPromise was null'); - this.trustedStatus = 'fetching'; - this.trustedPromise = this.trustedFunc(); + if (this.trustedStatus === 'fetching') { + // no one has started verifying the trusted yet + + 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 | null = this.trustedPromise; + const trustedResult = await origTrustedPromise; + + if (this.trustedPromise !== origTrustedPromise) { + // 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 (this.trustedPromise === null) { + if (debug) LOG.debug(fetchId + ': re-fetching since trustedPromise was null'); + this.trustedStatus = 'fetching'; + this.trustedPromise = this.trustedFunc(); + } + await tryResolveTrustedPromise(); + return; } - await tryResolveTrustedPromise(); - return; - } - // make sure to verify BEFORE potentially resolving - // this way the conflicts can be resolved before the result is returned - const primaryUpToDate = await this.verifyFunc(primaryResult, trustedResult); + // make sure to verify BEFORE potentially resolving + // this way the conflicts can be resolved before the result is returned + const primaryUpToDate = await this.verifyFunc(primaryResult, trustedResult); - if (this.trustedPromise === origTrustedPromise) { - if (trustedResult !== null && primaryUpToDate) { - // we got a good trusted result and the primary data has been updated - // to reflect the trusted data (or already reflects it). - if (debug) LOG.debug(fetchId + ': verified successfully.'); - this.trustedStatus = 'verified'; + if (this.trustedPromise === origTrustedPromise) { + if (trustedResult !== null && primaryUpToDate) { + // we got a good trusted result and the primary data has been updated + // to reflect the trusted data (or already reflects it). + if (debug) LOG.debug(fetchId + ': verified successfully.'); + this.trustedStatus = 'verified'; + } else { + // we have to re-fetch the trusted promise again next fetch + if (debug) LOG.debug(fetchId + ': needs trusted promise re-fetched next time'); + this.trustedStatus = 'none'; + } } else { - // we have to re-fetch the trusted promise again next fetch - if (debug) LOG.debug(fetchId + ': needs trusted promise re-fetched next time'); - this.trustedStatus = 'none'; + // this actually could be quite common + //console.warn('RARE ALERT: we got unverified during verification!'); + // ** complexity: + // keep in mind that this will resolve 'unverified' promises with their resulting origTrustedPromise (which should be a good thing) + } + + if (!resolved) { + // removed 01/09/2021 pretty sure should not be here... && trustedResult + if (debug) LOG.debug(fetchId + ': resolving with trusted result'); + resolve(trustedResult); + resolved = true; } } else { - // this actually could be quite common - //console.warn('RARE ALERT: we got unverified during verification!'); - // ** complexity: - // keep in mind that this will resolve 'unverified' promises with their resulting origTrustedPromise (which should be a good thing) - } + // some code is already dealing with (or dealt with) verifying the trusted result + // await the same trusted promise and return its result if we didn't get a result + // from the primary source. - if (!resolved) { - // removed 01/09/2021 pretty sure should not be here... && trustedResult - if (debug) LOG.debug(fetchId + ': resolving with trusted result'); - resolve(trustedResult); - resolved = true; + if (debug) LOG.debug(fetchId + ': waiting on result of a different verifier...'); + + // note: Promises that have already resolved will return the same value when awaited again :) + const origTrustedPromise: Promise | null = this.trustedPromise; + const trustedResult = await origTrustedPromise; + + 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!', + ); + await tryResolveTrustedPromise(); + return; + } + + if (!resolved) { + if (debug) + LOG.debug(fetchId + ': resolving with trusted result of different verifier'); + resolve(trustedResult); + resolved = true; + } } } else { - // some code is already dealing with (or dealt with) verifying the trusted result - // await the same trusted promise and return its result if we didn't get a result - // from the primary source. - - if (debug) LOG.debug(fetchId + ': waiting on result of a different verifier...'); - - // note: Promises that have already resolved will return the same value when awaited again :) - const origTrustedPromise: Promise | null = this.trustedPromise; - const trustedResult = await origTrustedPromise; - - 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!'); - await tryResolveTrustedPromise(); - return; - } - + // we are all up to date, make sure to resolve if primaryResult is null if (!resolved) { - if (debug) LOG.debug(fetchId + ': resolving with trusted result of different verifier'); - resolve(trustedResult); + if (debug) LOG.debug(fetchId + ': no trusted promise, resolving with null'); + resolve(null); resolved = true; } } + }; + await tryResolveTrustedPromise(); + } catch (e: unknown) { + this.unverify(); + if (!resolved) { + if (debug) LOG.debug(fetchId + ': error during fetch', e); + // eslint-disable-next-line prefer-promise-reject-errors + reject(e as Error); + resolved = true; } else { - // we are all up to date, make sure to resolve if primaryResult is null - if (!resolved) { - if (debug) LOG.debug(fetchId + ': no trusted promise, resolving with null'); - resolve(null); - resolved = true; - } + 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); } - }; - await tryResolveTrustedPromise(); - } catch (e: unknown) { - this.unverify(); - if (!resolved) { - if (debug) LOG.debug(fetchId + ': error during fetch', e); - // eslint-disable-next-line prefer-promise-reject-errors - reject(e as Error); - resolved = true; - } else { - 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); } - } - }); + }, + ); } } diff --git a/src/client/webapp/fetchable-pair-verifier.ts b/src/client/webapp/fetchable-pair-verifier.ts index f33e54f..d0d9c38 100644 --- a/src/client/webapp/fetchable-pair-verifier.ts +++ b/src/client/webapp/fetchable-pair-verifier.ts @@ -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 implements AsyncFetchable { +export default class PairVerifierFetchable extends EventEmitter implements AsyncFetchable, Ensurable { private readonly fetchMetadataVerifier: AutoVerifier; private readonly fetchMembersVerifier: AutoVerifier; private readonly fetchChannelsVerifier: AutoVerifier; @@ -22,7 +22,7 @@ export default class PairVerifierFetchable extends EventEmitter im public readonly fetchMessagesBeforeVerifier: AutoVerifierWithArg; public readonly fetchMessagesAfterVerifier: AutoVerifierWithArg; - 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 im this.fetchMetadataVerifier = AutoVerifier.createStandardSingleAutoVerifier( async () => await this.primary.fetchMetadata(), async () => await this.trusted.fetchMetadata(), + async () => await this.trusted.ensureVerified(), this.handleMetadataConflict.bind(this), ); this.fetchMembersVerifier = AutoVerifier.createStandardListAutoVerifier( async () => await this.primary.fetchMembers(), async () => await this.trusted.fetchMembers(), + async () => await this.trusted.ensureVerified(), this.handleMembersConflict.bind(this), ); this.fetchChannelsVerifier = AutoVerifier.createStandardListAutoVerifier( 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 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( 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 { + async ensureVerified(): Promise { + /* do nothing */ + } + + async handleMetadataConflict( + changesType: AutoVerifierChangesType, + primaryMetadata: GuildMetadata | null, + trustedMetadata: GuildMetadata | null, + ): Promise { // lOG.debug('metadata conflict', { // primaryClass: this.primary.constructor.name, // trustedClass: this.trusted.constructor.name, @@ -99,7 +121,12 @@ export default class PairVerifierFetchable extends EventEmitter 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 im async handleMembersConflict(changesType: AutoVerifierChangesType, changes: Changes): Promise { 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 im async handleChannelsConflict(changesType: AutoVerifierChangesType, changes: Changes): Promise { 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 im async handleTokensConflict(changesType: AutoVerifierChangesType, changes: Changes): Promise { 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 im return success; } - async handleResourceConflict(query: IDQuery, changesType: AutoVerifierChangesType, primaryResource: Resource | null, trustedResource: Resource | null): Promise { + async handleResourceConflict( + query: IDQuery, + changesType: AutoVerifierChangesType, + primaryResource: Resource | null, + trustedResource: Resource | null, + ): Promise { 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): Promise { + async handleMessagesConflict( + query: PartialMessageListQuery, + changesType: AutoVerifierChangesType, + changes: Changes, + ): Promise { 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 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 im return await this.fetchChannelsVerifier.fetchAndVerifyIfNeeded(); } async fetchMessagesRecent(channelId: string, number: number): Promise { - 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 { return await this.fetchMessagesBeforeVerifier.fetchAndVerifyIfNeded({ channelId, messageOrderId, number }); diff --git a/src/client/webapp/guild-personal-db.ts b/src/client/webapp/guild-personal-db.ts index 2f827d6..ab24580 100644 --- a/src/client/webapp/guild-personal-db.ts +++ b/src/client/webapp/guild-personal-db.ts @@ -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 { + /* do nothing, we are always ready */ + } + // lacking Methods (resolving differences) async handleMetadataChanged(changedMetaData: GuildMetadata): Promise { diff --git a/src/client/webapp/guild-socket.ts b/src/client/webapp/guild-socket.ts index de55cb5..019957f 100644 --- a/src/client/webapp/guild-socket.ts +++ b/src/client/webapp/guild-socket.ts @@ -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 implements AsyncGuaranteedFetchable, AsyncRequestable { +export default class SocketGuild + extends EventEmitter + implements AsyncGuaranteedFetchable, AsyncRequestable, Ensurable +{ private queryDedups = new Map>(); constructor(private socket: socketio.Socket, public verifier: SocketVerifier) { @@ -86,21 +89,41 @@ export default class SocketGuild extends EventEmitter implements As this.socket.disconnect(); } + public async ensureVerified(): Promise { + await this.verifier.ensureVerified(); + } + // server helper functions private async _query(timeout: number, endpoint: string, ...args: any[]): Promise { 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) => { - if (errMsg) { - reject(new Error(`error fetching server data @${endpoint} / [${args.map(arg => LOG.inspect(arg)).join(', ')}]: ${errMsg}`)); - } else { - resolve(serverData); - } - }); + 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}`, + ), + ); + } else { + resolve(serverData); + } + }, + ); }); } @@ -110,7 +133,11 @@ export default class SocketGuild extends EventEmitter implements As } // note: timeout is only respected for the first deduped request - private async queryOnceVerifiedDedup(timeout: number, endpoint: string, ...args: (string | number)[]): Promise { + private async queryOnceVerifiedDedup( + timeout: number, + endpoint: string, + ...args: (string | number)[] + ): Promise { const id = `query-@${endpoint}(${args.join(',')})`; let dedup = this.queryDedups.get(id); if (!dedup) { @@ -141,15 +168,32 @@ export default class SocketGuild extends EventEmitter implements As return data.map((dataChannel: any) => Channel.fromDBData(dataChannel)); } async fetchMessagesRecent(channelId: string, number: number): Promise { - 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 { - 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 { - 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 { @@ -163,10 +207,27 @@ export default class SocketGuild extends EventEmitter implements As // we don't want to dedup these async requestSendMessage(channelId: string, text: string): Promise { - /*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 { - /*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 { + /*const _dataMessage = */ await this.queryOnceVerified( + Globals.DEFAULT_SOCKET_TIMEOUT, + 'send-message-with-resource', + channelId, + text, + resource, + resourceName, + ); } async requestSetStatus(status: string): Promise { await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-status', status); @@ -184,11 +245,22 @@ export default class SocketGuild extends EventEmitter implements As await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-icon', guildIcon); } async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise { - 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 { - 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 { diff --git a/src/client/webapp/guild-types.ts b/src/client/webapp/guild-types.ts index 9e1c20e..d1db2a7 100644 --- a/src/client/webapp/guild-types.ts +++ b/src/client/webapp/guild-types.ts @@ -26,6 +26,10 @@ export interface AsyncFetchable { } export type Fetchable = SyncFetchable | AsyncFetchable; +export interface Ensurable { + ensureVerified: () => Promise; +} + export interface AsyncGuaranteedFetchable { fetchMetadata(): Promise; fetchMembers(): Promise; @@ -42,7 +46,12 @@ export type GuaranteedFetchable = AsyncGuaranteedFetchable; export interface AsyncRequestable { requestSendMessage(channelId: string, text: string): Promise; - requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise; + requestSendMessageWithResource( + channelId: string, + text: string | null, + resource: Buffer, + resourceName: string, + ): Promise; requestSetStatus(status: string): Promise; requestSetDisplayName(displayName: string): Promise; requestSetAvatar(avatar: Buffer): Promise; @@ -91,9 +100,18 @@ export interface AsyncLackable { afterAutoVerifier: AutoVerifierWithArg, ): Promise; - handleResourceAdded(addedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg): Promise; - handleResourceChanged(changedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg): Promise; - handleResourceDeleted(deletedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg): Promise; + handleResourceAdded( + addedResource: Resource, + resourceAutoVerifier: AutoVerifierWithArg, + ): Promise; + handleResourceChanged( + changedResource: Resource, + resourceAutoVerifier: AutoVerifierWithArg, + ): Promise; + handleResourceDeleted( + deletedResource: Resource, + resourceAutoVerifier: AutoVerifierWithArg, + ): Promise; handleTokensAdded(addedTokens: Token[]): Promise; handleTokensChanged(changedTokens: Token[]): Promise; @@ -131,8 +149,14 @@ export interface SyncLackable { ): boolean; handleResourceAdded(addedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg): boolean; - handleResourceChanged(changedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg): boolean; - handleResourceDeleted(deletedResource: Resource, resourceAutoVerifier: AutoVerifierWithArg): boolean; + handleResourceChanged( + changedResource: Resource, + resourceAutoVerifier: AutoVerifierWithArg, + ): boolean; + handleResourceDeleted( + deletedResource: Resource, + resourceAutoVerifier: AutoVerifierWithArg, + ): 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) => void; 'conflict-members': (changesType: AutoVerifierChangesType, changes: Changes) => void; 'conflict-tokens': (changesType: AutoVerifierChangesType, changes: Changes) => void; - 'conflict-resource': (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => void; - 'conflict-messages': (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes) => void; + 'conflict-resource': ( + query: IDQuery, + changesType: AutoVerifierChangesType, + oldResource: Resource, + newResource: Resource, + ) => void; + 'conflict-messages': ( + query: PartialMessageListQuery, + changesType: AutoVerifierChangesType, + changes: Changes, + ) => void; }; export const GuildEventNames = [