diff --git a/client/webapp/auto-verifier-with-args.ts b/client/webapp/auto-verifier-with-args.ts index b9945e9..10c9609 100644 --- a/client/webapp/auto-verifier-with-args.ts +++ b/client/webapp/auto-verifier-with-args.ts @@ -1,15 +1,19 @@ // Intended to be used with message lists import { stringify } from "querystring"; -import { AutoVerifier } from "./auto-verifier"; +import { AutoVerifier, AutoVerifierChangesType } from "./auto-verifier"; import { Changes, WithEquals } from "./data-types"; -export interface PartialListQuery { +export interface PartialMessageListQuery { channelId: string; messageId: string | null; number: number; } +export interface IDQuery { + id: string; +} + export class AutoVerifierWithArg { private tokenAutoVerifiers = new Map>(); private tokenQueries = new Map(); @@ -23,17 +27,35 @@ export class AutoVerifierWithArg { private verifyFunc: (query: K, primaryResult: T | null, trustedResult: T | null) => Promise ) {} - static createPartialListAutoVerifier & { id: string }>( - primaryFunc: (query: PartialListQuery) => Promise, - trustedFunc: (query: PartialListQuery) => Promise, - changesFunc: (query: PartialListQuery, changes: Changes) => Promise + static createStandardPartialMessageListAutoVerifier & { id: string }>( + primaryFunc: (query: PartialMessageListQuery) => Promise, + trustedFunc: (query: PartialMessageListQuery) => Promise, + changesFunc: (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes) => Promise ) { - return new AutoVerifierWithArg( + return new AutoVerifierWithArg( query => `ch#${query.channelId} m#${query.messageId}->${query.number}`, query => primaryFunc(query), query => trustedFunc(query), - async (query: PartialListQuery, primaryResult: T[] | null, trustedResult: T[] | null) => { - await changesFunc(query, AutoVerifier.getChanges(primaryResult, trustedResult)); + async (query: PartialMessageListQuery, primaryResult: T[] | null, trustedResult: T[] | null) => { + let changes = AutoVerifier.getChanges(primaryResult, trustedResult); + let changesType = AutoVerifier.getListChangesType(primaryResult, trustedResult, changes); + await changesFunc(query, changesType, changes); + } + ); + } + + static createStandardIDQueriedSingleAutoVerifier & { id: string }>( + primaryFunc: (query: IDQuery) => Promise, + trustedFunc: (query: IDQuery) => 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), + async (query: IDQuery, primaryResult: T | null, trustedResult: T | null) => { + let changesType = AutoVerifier.getSingleChangesType(primaryResult, trustedResult); + await changesFunc(query, changesType, primaryResult, trustedResult); } ); } diff --git a/client/webapp/auto-verifier.ts b/client/webapp/auto-verifier.ts index 77c97a7..3a885be 100644 --- a/client/webapp/auto-verifier.ts +++ b/client/webapp/auto-verifier.ts @@ -4,12 +4,21 @@ import Logger from '../../logger/logger'; import { Changes, WithEquals } from './data-types'; const LOG = Logger.create(__filename, electronConsole); +export enum AutoVerifierChangesType { + NONE, // Both primaryFunc and trustedFunc returned null + PRIMARY_ONLY, // primaryFunc returned non-null and trustedFunc returned null + TRUSTED_ONLY, // trustedFunc returned non-null and primaryFunc returned null + VERIFIED, // primaryFunc and trustedFunc returned the same non-null result + CONFLICT, // primaryFunc and trustedFunc returned conflicting non-null results +}; + /** * This is probably complex piece of code in this entire project. * Some complexity comes because the result of the trusted function * can be invalidated while we are awaiting the trusted function. * That complexity stacks on top of the already complex cache-verification * to make this one fiesta of a class. + * If you have to edit this it's a very sad day. */ export class AutoVerifier { private trustedPromise: Promise | null = null; @@ -61,16 +70,61 @@ export class AutoVerifier { return changes; } + static getListChangesType(primaryResult: T[] | null, trustedResult: T[] | null, changes: Changes): AutoVerifierChangesType { + if (primaryResult === null && trustedResult === null) { + return AutoVerifierChangesType.NONE; + } else if (trustedResult === null) { + return AutoVerifierChangesType.PRIMARY_ONLY; + } else if (primaryResult === null) { + return AutoVerifierChangesType.TRUSTED_ONLY; + } else if (changes.added.length === 0 && changes.updated.length === 0 && changes.deleted.length === 0) { + return AutoVerifierChangesType.VERIFIED; + } else { + return AutoVerifierChangesType.CONFLICT; + } + } + + static getSingleChangesType>(primaryResult: T | null, trustedResult: T | null): AutoVerifierChangesType { + if (primaryResult === null && trustedResult === null) { + return AutoVerifierChangesType.NONE; + } else if (trustedResult === null) { + return AutoVerifierChangesType.PRIMARY_ONLY; + } else if (primaryResult === null) { + return AutoVerifierChangesType.TRUSTED_ONLY; + } else if (primaryResult.equals(trustedResult)) { + return AutoVerifierChangesType.VERIFIED; + } else { + return AutoVerifierChangesType.CONFLICT; + } + } + static createStandardListAutoVerifier & { id: string }>( primaryFunc: () => Promise, trustedFunc: () => Promise, - changesFunc: (changes: Changes) => Promise + changesFunc: (changesType: AutoVerifierChangesType, changes: Changes) => Promise ) { return new AutoVerifier( primaryFunc, trustedFunc, async (primaryResult: T[] | null, trustedResult: T[] | null) => { - await changesFunc(AutoVerifier.getChanges(primaryResult, trustedResult)); + let changes = AutoVerifier.getChanges(primaryResult, trustedResult); + let changesType = AutoVerifier.getListChangesType(primaryResult, trustedResult, changes); + await changesFunc(changesType, changes); + } + ); + } + + static createStandardSingleAutoVerifier>( + primaryFunc: () => Promise, + trustedFunc: () => Promise, + changesFunc: (changesType: AutoVerifierChangesType, primaryResult: T | null, trustedResult: T | null) => Promise + ) { + return new AutoVerifier( + primaryFunc, + trustedFunc, + async (primaryResult: T | null, trustedResult: T | null) => { + let changesType = AutoVerifier.getSingleChangesType(primaryResult, trustedResult); + await changesFunc(changesType, primaryResult, trustedResult); } ); } @@ -85,7 +139,7 @@ export class AutoVerifier { // If the primary fetchable returns null but has not been verified yet, this will return the result of the trusted fetchable // If the trusted fetchable has not been used to verify the primary fetchable yet, this queries the trusted fetchable and calls verify async fetchAndVerifyIfNeeded(): Promise { - return await new Promise(async (resolve, reject) => { + return await new Promise(async (resolve: (result: T | null) => void, reject: (error: Error) => void) => { let resolved = false; try { let primaryPromise = this.primaryFunc(); @@ -128,18 +182,21 @@ export class AutoVerifier { return; } - if (!resolved) { - resolve(trustedResult); - resolved = true; - } - + // Make sure to verify BEFORE potentially resolving + // This way the conflicts can be resolved before the result is returned await this.verifyFunc(primaryResult, trustedResult); + if (this.trustedPromise === origTrustedPromise) { this.trustedStatus = 'verified'; } else { LOG.warn('RARE ALERT: we got unverified during verification!'); // We don't have to re-resolve since we already would have resolved with the correct trusted result } + + if (!resolved && trustedResult) { + 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 diff --git a/client/webapp/data-source.ts b/client/webapp/data-source.ts deleted file mode 100644 index 00aee54..0000000 --- a/client/webapp/data-source.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** - * A data source is able to handle events - */ diff --git a/client/webapp/fetchable-ensured.ts b/client/webapp/fetchable-ensured.ts new file mode 100644 index 0000000..9823d3a --- /dev/null +++ b/client/webapp/fetchable-ensured.ts @@ -0,0 +1,38 @@ +import { GuildMetadata, Member, Channel, Message, Resource, Token } from "./data-types"; +import { Fetchable, GuaranteedFetchable } from "./guild-types"; + +export default class EnsuredFetchable implements GuaranteedFetchable { + constructor( + private fetchable: Fetchable + ) {} + + private static guarantee(value: T | null): T { + if (!value) throw new Error('got null value'); + return value; + } + + async fetchMetadata(): Promise { + return EnsuredFetchable.guarantee(await this.fetchable.fetchMetadata()); + } + async fetchMembers(): Promise { + return EnsuredFetchable.guarantee(await this.fetchable.fetchMembers()); + } + async fetchChannels(): Promise { + return EnsuredFetchable.guarantee(await this.fetchable.fetchChannels()); + } + async fetchMessagesRecent(channelId: string, number: number): Promise { + return EnsuredFetchable.guarantee(await this.fetchable.fetchMessagesRecent(channelId, number)); + } + async fetchMessagesBefore(channelId: string, messageId: string, number: number): Promise { + return EnsuredFetchable.guarantee(await this.fetchable.fetchMessagesBefore(channelId, messageId, number)); + } + async fetchMessagesAfter(channelId: string, messageId: string, number: number): Promise { + return EnsuredFetchable.guarantee(await this.fetchable.fetchMessagesAfter(channelId, messageId, number)); + } + async fetchResource(resourceId: string): Promise { + return EnsuredFetchable.guarantee(await this.fetchable.fetchResource(resourceId)); + } + async fetchTokens(): Promise { + return EnsuredFetchable.guarantee(await this.fetchable.fetchTokens()); + } +} diff --git a/client/webapp/fetchable-pair-verifier.ts b/client/webapp/fetchable-pair-verifier.ts new file mode 100644 index 0000000..3ac811c --- /dev/null +++ b/client/webapp/fetchable-pair-verifier.ts @@ -0,0 +1,176 @@ +import { Changes, Channel, GuildMetadata, Member, Message, Resource, Token } from './data-types'; +import { AsyncFetchable, Fetchable, Lackable, Conflictable } from './guild-types'; +import { AutoVerifier, AutoVerifierChangesType } from './auto-verifier'; +import { AutoVerifierWithArg, PartialMessageListQuery, IDQuery } from './auto-verifier-with-args'; +import { EventEmitter } from 'tsee'; + +export default class GuildPairVerifierGuild extends EventEmitter implements AsyncFetchable { + + private readonly fetchMetadataVerifier: AutoVerifier; + private readonly fetchMembersVerifier: AutoVerifier; + private readonly fetchChannelsVerifier: AutoVerifier; + private readonly fetchTokensVerifier: AutoVerifier; + + private readonly fetchResourceVerifier: AutoVerifierWithArg + + private readonly fetchMessagesRecentVerifier: AutoVerifierWithArg; + private readonly fetchMessagesBeforeVerifier: AutoVerifierWithArg; + private readonly fetchMessagesAfterVerifier: AutoVerifierWithArg; + + constructor( + primary: Fetchable & Lackable, + trusted: Fetchable + ) { + super(); + + this.fetchMetadataVerifier = AutoVerifier.createStandardSingleAutoVerifier( + async () => await primary.fetchMetadata(), + async () => await trusted.fetchMetadata(), + async (changesType: AutoVerifierChangesType, primaryMetadata: GuildMetadata | null, trustedMetadata: GuildMetadata | null) => { + if (changesType === AutoVerifierChangesType.TRUSTED_ONLY) { + await primary.handleMetadataChanged(trustedMetadata as GuildMetadata); + } else if (changesType === AutoVerifierChangesType.CONFLICT) { + await primary.handleMetadataChanged(trustedMetadata as GuildMetadata); + this.emit('conflict-metadata', primaryMetadata as GuildMetadata, trustedMetadata as GuildMetadata); + } + } + ) + + this.fetchMembersVerifier = AutoVerifier.createStandardListAutoVerifier( + async () => await primary.fetchMembers(), + async () => await trusted.fetchMembers(), + async (changesType: AutoVerifierChangesType, changes: Changes) => { + if (changes.added.length > 0) await primary.handleMembersAdded(changes.added); + if (changes.updated.length > 0) await primary.handleMembersChanged(changes.updated.map(change => change.newDataPoint)); + if (changes.deleted.length > 0) await primary.handleMembersDeleted(changes.deleted); + + if (changesType === AutoVerifierChangesType.CONFLICT) { + this.emit('conflict-members', changes); + } + } + ); + + this.fetchChannelsVerifier = AutoVerifier.createStandardListAutoVerifier( + async () => await primary.fetchChannels(), + async () => await trusted.fetchChannels(), + async (changesType: AutoVerifierChangesType, changes: Changes) => { + if (changes.added.length > 0) await primary.handleChannelsAdded(changes.added); + if (changes.updated.length > 0) await primary.handleChannelsChanged(changes.updated.map(change => change.newDataPoint)); + if (changes.deleted.length > 0) await primary.handleChannelsDeleted(changes.deleted); + + if (changesType === AutoVerifierChangesType.CONFLICT) { + this.emit('conflict-channels', changes); + } + } + ); + + this.fetchTokensVerifier = AutoVerifier.createStandardListAutoVerifier( + async () => await primary.fetchTokens(), + async () => await trusted.fetchTokens(), + async (changesType: AutoVerifierChangesType, changes: Changes) => { + if (changes.added.length > 0) await primary.handleTokensAdded(changes.added); + if (changes.updated.length > 0) await primary.handleTokensChanged(changes.updated.map(change => change.newDataPoint)); + if (changes.deleted.length > 0) await primary.handleTokensDeleted(changes.deleted); + + if (changesType === AutoVerifierChangesType.CONFLICT) { + this.emit('conflict-tokens', changes); + } + } + ); + + this.fetchResourceVerifier = AutoVerifierWithArg.createStandardIDQueriedSingleAutoVerifier( + async (query: IDQuery) => await primary.fetchResource(query.id), + async (query: IDQuery) => await trusted.fetchResource(query.id), + async (query: IDQuery, changesType: AutoVerifierChangesType, primaryResource: Resource | null, trustedResource: Resource | null) => { + if (changesType === AutoVerifierChangesType.PRIMARY_ONLY) { + await primary.handleResourceDeleted(trustedResource as Resource); + } else if (changesType === AutoVerifierChangesType.TRUSTED_ONLY) { + await primary.handleResourceAdded(trustedResource as Resource); + } else if (changesType === AutoVerifierChangesType.CONFLICT) { + await primary.handleResourceChanged(trustedResource as Resource); + this.emit('conflict-resource', primaryResource as Resource, trustedResource as Resource); + } + } + ); + + this.fetchMessagesRecentVerifier = AutoVerifierWithArg.createStandardPartialMessageListAutoVerifier( + async (query: PartialMessageListQuery) => await primary.fetchMessagesRecent(query.channelId, query.number), + async (query: PartialMessageListQuery) => await trusted.fetchMessagesRecent(query.channelId, query.number), + async (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes) => { + if (changes.added.length > 0) await primary.handleMessagesAdded(changes.added); + if (changes.updated.length > 0) await primary.handleMessagesChanged(changes.updated.map(change => change.newDataPoint)); + if (changes.deleted.length > 0) await primary.handleMessagesDeleted(changes.deleted); + + if (changesType === AutoVerifierChangesType.CONFLICT) { + this.emit('conflict-messages', changes); + } + } + ); + + this.fetchMessagesBeforeVerifier = AutoVerifierWithArg.createStandardPartialMessageListAutoVerifier( + async (query: PartialMessageListQuery) => await primary.fetchMessagesBefore(query.channelId, query.messageId as string, query.number), + async (query: PartialMessageListQuery) => await trusted.fetchMessagesBefore(query.channelId, query.messageId as string, query.number), + async (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes) => { + if (changes.added.length > 0) await primary.handleMessagesAdded(changes.added); + if (changes.updated.length > 0) await primary.handleMessagesChanged(changes.updated.map(change => change.newDataPoint)); + if (changes.deleted.length > 0) await primary.handleMessagesDeleted(changes.deleted); + + if (changesType === AutoVerifierChangesType.CONFLICT) { + this.emit('conflict-messages', changes); + } + } + ); + + this.fetchMessagesAfterVerifier = AutoVerifierWithArg.createStandardPartialMessageListAutoVerifier( + async (query: PartialMessageListQuery) => await primary.fetchMessagesAfter(query.channelId, query.messageId as string, query.number), + async (query: PartialMessageListQuery) => await trusted.fetchMessagesAfter(query.channelId, query.messageId as string, query.number), + async (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes) => { + if (changes.added.length > 0) await primary.handleMessagesAdded(changes.added); + if (changes.updated.length > 0) await primary.handleMessagesChanged(changes.updated.map(change => change.newDataPoint)); + if (changes.deleted.length > 0) await primary.handleMessagesDeleted(changes.deleted); + + if (changesType === AutoVerifierChangesType.CONFLICT) { + this.emit('conflict-messages', changes); + } + } + ); + } + + unverify() { + this.fetchMetadataVerifier.unverify(); + this.fetchMembersVerifier.unverify(); + this.fetchChannelsVerifier.unverify(); + this.fetchTokensVerifier.unverify(); + + this.fetchResourceVerifier.unverifyAll(); + + this.fetchMessagesRecentVerifier.unverifyAll(); + this.fetchMessagesBeforeVerifier.unverifyAll(); + this.fetchMessagesAfterVerifier.unverifyAll(); + } + + async fetchMetadata(): Promise { + return await this.fetchMetadataVerifier.fetchAndVerifyIfNeeded(); + } + async fetchMembers(): Promise { + return await this.fetchMembersVerifier.fetchAndVerifyIfNeeded(); + } + async fetchChannels(): Promise { + return await this.fetchChannelsVerifier.fetchAndVerifyIfNeeded(); + } + async fetchMessagesRecent(channelId: string, number: number): Promise { + return await this.fetchMessagesRecentVerifier.fetchAndVerifyIfNeded({ channelId, messageId: null, number }); + } + async fetchMessagesBefore(channelId: string, messageId: string, number: number): Promise { + return await this.fetchMessagesBeforeVerifier.fetchAndVerifyIfNeded({ channelId, messageId, number }); + } + async fetchMessagesAfter(channelId: string, messageId: string, number: number): Promise { + return await this.fetchMessagesAfterVerifier.fetchAndVerifyIfNeded({ channelId, messageId, number }); + } + async fetchResource(resourceId: string): Promise { + return await this.fetchResourceVerifier.fetchAndVerifyIfNeded({ id: resourceId }); + } + async fetchTokens(): Promise { + return await this.fetchTokensVerifier.fetchAndVerifyIfNeeded(); + } +} diff --git a/client/webapp/guild-combined.ts b/client/webapp/guild-combined.ts index 8e69d49..dcfd8bb 100644 --- a/client/webapp/guild-combined.ts +++ b/client/webapp/guild-combined.ts @@ -13,17 +13,17 @@ import MessageRAMCache from "./message-ram-cache"; import PersonalDB from "./personal-db"; import ResourceRAMCache from "./resource-ram-cache"; import SocketVerifier from './socket-verifier'; -import { Connectable, AsyncGuaranteedFetchable } from './guild-types'; -import { AutoVerifier } from './auto-verifier'; -import { AutoVerifierWithArg, PartialListQuery } from './auto-verifier-with-args'; -import GuildPairVerifierGuild from './guild-pair-verifier'; +import { Connectable, AsyncGuaranteedFetchable, Conflictable, AsyncRequestable } from './guild-types'; +import PairVerifierFetchable from './fetchable-pair-verifier'; +import EnsuredFetchable from './fetchable-ensured'; +import { EventEmitter } from 'tsee'; -export default class GuildController extends Connectable implements AsyncGuaranteedFetchable { +export default class CombinedGuild extends EventEmitter implements AsyncGuaranteedFetchable, AsyncRequestable { private readonly ramGuild: RAMGuild; private readonly personalDBGuild: PersonalDBGuild; private readonly socketGuild: SocketGuild; - private readonly pairVerifiers: GuildPairVerifierGuild[]; + private readonly pairVerifiers: PairVerifierFetchable[]; private readonly fetchable: AsyncGuaranteedFetchable; constructor( @@ -43,6 +43,7 @@ export default class GuildController extends Connectable implements AsyncGuarant // TODO: Only unverify the personaldb->socket connection on d/c? + // Connect/Disconnect this.socketGuild.on('connect', () => { LOG.info(`g#${this.id} connected`); this.emit('connect'); @@ -53,83 +54,98 @@ export default class GuildController extends Connectable implements AsyncGuarant this.emit('disconnect'); }); + // Metadata this.socketGuild.on('update-metadata', (guildMeta: GuildMetadata) => { LOG.info(`g#${this.id} updated metadata: ${guildMeta}`); this.emit('update-metadata', guildMeta); }); - let listenForList = ( - event: 'new-messages' | 'update-messages' | 'new-members' | 'update-members' | 'new-channels' | 'update-channels', - logFunc: (arg: T) => void, - guildUpdater: (args: T[]) => void - ) => { - this.socketGuild.on(event, (args: any[]) => { - for (let arg of args) { - logFunc(arg); - } - guildUpdater(args); - this.emit(event, args); - }); - } + // Members + this.socketGuild.on('new-members', async (members: Member[]) => { + for (let member of members) { + LOG.info(`g#${this.id} ${member}`); + } + this.ramGuild.handleMembersAdded(members); + await this.personalDBGuild.handleMembersAdded(members); + this.emit('new-members', members); + }); + this.socketGuild.on('update-members', async (members: Member[]) => { + for (let member of members) { + LOG.info(`g#${this.id} updated ${member}`); + } + this.ramGuild.handleMembersChanged(members); + await this.personalDBGuild.handleMembersChanged(members); + this.emit('update-members', members); + }); - listenForList('new-members', - (member: Member) => LOG.info(`g#${this.id} ${member}`), - (members: Member[]) => { - this.ramGuild.handleMembersAdded(members); - this.personalDBGuild.handleMembersAdded(members); + // Channels + this.socketGuild.on('new-channels', async (channels: Channel[]) => { + for (let channel of channels) { + LOG.info(`g#${this.id} ${channel}`); } - ); - listenForList('update-members', - (member: Member) => LOG.info(`g#${this.id} updated ${member}`), - (members: Member[]) => { - this.ramGuild.handleMembersChanged(members); - this.personalDBGuild.handleMembersChanged(members); + this.ramGuild.handleChannelsAdded(channels); + await this.personalDBGuild.handleChannelsAdded(channels); + this.emit('new-channels', channels); + }); + this.socketGuild.on('update-channels', async (channels: Channel[]) => { + for (let channel of channels) { + LOG.info(`g#${this.id} updated ${channel}`); } - ); + this.ramGuild.handleChannelsChanged(channels); + await this.personalDBGuild.handleChannelsChanged(channels); + this.emit('update-channels', channels); + }); - listenForList('new-members', - (channel: Channel) => LOG.info(`g#${this.id} ${channel}`), - (channels: Channel[]) => { - this.ramGuild.handleChannelsAdded(channels); - this.personalDBGuild.handleChannelsAdded(channels); + // Messages + this.socketGuild.on('new-messages', async (messages: Message[]) => { + for (let message of messages) { + LOG.info(`g#${this.id} ${message}`); } - ); - listenForList('update-members', - (channel: Channel) => LOG.info(`g#${this.id} updated ${channel}`), - (channels: Channel[]) => { - this.ramGuild.handleChannelsChanged(channels); - this.personalDBGuild.handleChannelsChanged(channels); + this.ramGuild.handleMessagesAdded(messages); + await this.personalDBGuild.handleMessagesAdded(messages); + this.emit('new-messages', messages); + }); + this.socketGuild.on('update-messages', async (messages: Message[]) => { + for (let message of messages) { + LOG.info(`g#${this.id} updated ${message}`); } - ); + this.ramGuild.handleMessagesChanged(messages); + await this.personalDBGuild.handleMessagesChanged(messages); + this.emit('update-messages', messages); + }); - listenForList('new-messages', - (message: Message) => LOG.info(`g#${this.id} ${message}`), - (messages: Message[]) => { - this.ramGuild.handleMessagesAdded(messages); - this.personalDBGuild.handleMessagesAdded(messages); - } - ); - listenForList('update-messages', - (message: Message) => LOG.info(`g#${this.id} updated ${message}`), - (messages: Message[]) => { - this.ramGuild.handleMessagesChanged(messages); - this.personalDBGuild.handleMessagesChanged(messages); - } - ); + let personalDBSocketPairVerifier = new PairVerifierFetchable(this.personalDBGuild, this.socketGuild); + let ramPersonalDBSocketPairVerifier = new PairVerifierFetchable(this.ramGuild, personalDBSocketPairVerifier); - let personalDBSocketPairVerifier = new GuildPairVerifierGuild( - this.personalDBGuild, - this.socketGuild - ); - - let ramPersonalDBSocketPairVerifier = new GuildPairVerifierGuild( - this.ramGuild, - personalDBSocketPairVerifier - ); + // Forward the conflict events from the last verifier in the chain + ramPersonalDBSocketPairVerifier.on('conflict-metadata', (oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => { + LOG.info(`g#${this.id} metadata conflict`, { oldGuildMeta, newGuildMeta }); + this.emit('conflict-metadata', oldGuildMeta, newGuildMeta); + }); + ramPersonalDBSocketPairVerifier.on('conflict-members', (changes: Changes) => { + LOG.info(`g#${this.id} members conflict`, { changes }); + this.emit('conflict-members', changes); + }); + ramPersonalDBSocketPairVerifier.on('conflict-channels', (changes: Changes) => { + LOG.info(`g#${this.id} channels conflict`, { changes }); + this.emit('conflict-channels', changes); + }); + ramPersonalDBSocketPairVerifier.on('conflict-messages', (changes: Changes) => { + LOG.info(`g#${this.id} messages conflict`, { changes }); + this.emit('conflict-messages', changes); + }); + ramPersonalDBSocketPairVerifier.on('conflict-tokens', (changes: Changes) => { + LOG.info(`g#${this.id} tokens conflict`, { changes }); + this.emit('conflict-tokens', changes); + }); + ramPersonalDBSocketPairVerifier.on('conflict-resource', (oldResource: Resource, newResource: Resource) => { + LOG.warn(`g#${this.id} resource conflict`, { oldResource, newResource }); + this.emit('conflict-resource', oldResource, newResource); + }); this.pairVerifiers = [ personalDBSocketPairVerifier, ramPersonalDBSocketPairVerifier ]; - this.fetchable = ramPersonalDBSocketPairVerifier; + this.fetchable = new EnsuredFetchable(ramPersonalDBSocketPairVerifier); } static async create( @@ -145,7 +161,7 @@ export default class GuildController extends Connectable implements AsyncGuarant }); let socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey); let memberId = await socketVerifier.verify(); - return new GuildController( + return new CombinedGuild( personalGuildId, memberId, socket, @@ -166,14 +182,17 @@ export default class GuildController extends Connectable implements AsyncGuarant if (this.ramGuild.getMembers().size === 0) { await this.fetchMembers(); } + if (this.ramGuild.getMembers().size === 0) throw new Error('RAM Members was not updated through fetchMembers'); } async ensureRAMChannels() { if (this.ramGuild.getChannels().size === 0) { await this.fetchChannels(); } + if (this.ramGuild.getChannels().size === 0) throw new Error('RAM Channels was not updated through fetchChannels'); } + // Fetched through the triple-cache system (RAM -> Disk -> Server) async fetchMetadata(): Promise { return await this.fetchable.fetchMetadata(); } @@ -198,4 +217,36 @@ export default class GuildController extends Connectable implements AsyncGuarant async fetchTokens(): Promise { return await this.fetchable.fetchTokens(); } + + // Simply forwarded to the socket guild + async requestSendMessage(channelId: string, text: string): Promise { + await this.socketGuild.requestSendMessage(channelId, text); + } + async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise { + await this.socketGuild.requestSendMessageWithResource(channelId, text, resource, resourceName); + } + async requestSetStatus(status: string): Promise { + await this.socketGuild.requestSetStatus(status); + } + async requestSetDisplayName(displayName: string): Promise { + await this.socketGuild.requestSetDisplayName(displayName); + } + async requestSetAvatar(avatar: Buffer): Promise { + await this.socketGuild.requestSetAvatar(avatar); + } + async requestSetServerName(serverName: string): Promise { + await this.socketGuild.requestSetServerName(serverName); + } + async requestSetServerIcon(serverIcon: Buffer): Promise { + await this.socketGuild.requestSetServerIcon(serverIcon); + } + async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise { + await this.socketGuild.requestDoUpdateChannel(channelId, name, flavorText); + } + async requestDoCreateChannel(name: string, flavorText: string | null): Promise { + await this.socketGuild.requestDoCreateChannel(name, flavorText); + } + async requestDoRevokeToken(token: string): Promise { + await this.socketGuild.requestDoRevokeToken(token); + } } diff --git a/client/webapp/guild-pair-verifier.ts b/client/webapp/guild-pair-verifier.ts deleted file mode 100644 index 3405afe..0000000 --- a/client/webapp/guild-pair-verifier.ts +++ /dev/null @@ -1,172 +0,0 @@ -import * as electronRemote from '@electron/remote'; -const electronConsole = electronRemote.getGlobal('console') as Console; -import Logger from '../../logger/logger'; -const LOG = Logger.create(__filename, electronConsole); - -import * as socketio from 'socket.io-client'; -import PersonalDBGuild from './guild-personal-db'; -import RAMGuild from './guild-ram'; -import SocketGuild from './guild-socket'; -import { Changes, Channel, GuildMetadata, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types'; - -import MessageRAMCache from "./message-ram-cache"; -import PersonalDB from "./personal-db"; -import ResourceRAMCache from "./resource-ram-cache"; -import SocketVerifier from './socket-verifier'; -import { Connectable, AsyncGuaranteedFetchable, Fetchable, Lackable, GuaranteedFetchable } from './guild-types'; -import { AutoVerifier } from './auto-verifier'; -import { AutoVerifierWithArg, PartialListQuery } from './auto-verifier-with-args'; - -export default class GuildPairVerifierGuild implements AsyncGuaranteedFetchable { - - // Verifiers for the personaldb to the socket - private readonly fetchMetadataVerifier: AutoVerifier; - private readonly fetchMembersVerifier: AutoVerifier; - private readonly fetchChannelsVerifier: AutoVerifier; - private readonly fetchTokensVerifier: AutoVerifier; - - private readonly fetchResourceVerifier: AutoVerifierWithArg - - private readonly fetchMessagesRecentVerifier: AutoVerifierWithArg; - private readonly fetchMessagesBeforeVerifier: AutoVerifierWithArg; - private readonly fetchMessagesAfterVerifier: AutoVerifierWithArg; - - // TODO: Figure out what to do to verify the resources - // TODO: Figure out what to do to verify the messages - - constructor( - readonly primary: Fetchable & Lackable, - readonly trusted: GuaranteedFetchable - ) { - this.fetchMetadataVerifier = new AutoVerifier( - async () => await this.primary.fetchMetadata(), - async () => await this.trusted.fetchMetadata(), - async (personalDBMetadata: GuildMetadata | null, socketMetadata: GuildMetadata | null) => { - if (personalDBMetadata === null && socketMetadata === null) { - return; - } else if (personalDBMetadata === null) { - this.primary.handleMetadataChanged(socketMetadata as GuildMetadata); - } else if (socketMetadata === null) { - return; - } else { - this.primary.handleMetadataChanged(socketMetadata as GuildMetadata); - } - } - ) - - this.fetchMembersVerifier = AutoVerifier.createStandardListAutoVerifier( - async () => await this.primary.fetchMembers(), - async () => await this.trusted.fetchMembers(), - async (changes: Changes) => { - if (changes.added.length > 0) this.primary.handleMembersAdded(changes.added); - if (changes.updated.length > 0) this.primary.handleMembersChanged(changes.updated.map(change => change.newDataPoint)); - if (changes.deleted.length > 0) this.primary.handleMembersDeleted(changes.deleted); - } - ); - - this.fetchChannelsVerifier = AutoVerifier.createStandardListAutoVerifier( - async () => await this.primary.fetchChannels(), - async () => await this.trusted.fetchChannels(), - async (changes: Changes) => { - if (changes.added.length > 0) this.primary.handleChannelsAdded(changes.added); - if (changes.updated.length > 0) this.primary.handleChannelsChanged(changes.updated.map(change => change.newDataPoint)); - if (changes.deleted.length > 0) this.primary.handleChannelsDeleted(changes.deleted); - } - ); - - this.fetchTokensVerifier = AutoVerifier.createStandardListAutoVerifier( - async () => await this.primary.fetchTokens(), - async () => await this.trusted.fetchTokens(), - async (changes: Changes) => { - if (changes.added.length > 0) this.primary.handleTokensAdded(changes.added); - if (changes.updated.length > 0) this.primary.handleTokensChanged(changes.updated.map(change => change.newDataPoint)); - if (changes.deleted.length > 0) this.primary.handleTokensDeleted(changes.deleted); - } - ); - - this.fetchResourceVerifier = new AutoVerifierWithArg( - (query: { resourceId: string }) => query.resourceId, - async (query: { resourceId: string }) => await this.primary.fetchResource(query.resourceId), - async (query: { resourceId: string }) => await this.trusted.fetchResource(query.resourceId), - async (query: { resourceId: string }, personalDBResource: Resource | null, socketResource: Resource | null) => { - if (personalDBResource === null && socketResource === null) { - return; - } else if (personalDBResource === null) { - this.primary.handleResourceAdded(socketResource as Resource); - } else if (socketResource === null) { - this.primary.handleResourceDeleted(personalDBResource as Resource); - } else { - this.primary.handleResourceChanged(socketResource as Resource); - } - } - ); - - this.fetchMessagesRecentVerifier = AutoVerifierWithArg.createPartialListAutoVerifier( - async (query: PartialListQuery) => await this.primary.fetchMessagesRecent(query.channelId, query.number), - async (query: PartialListQuery) => await this.trusted.fetchMessagesRecent(query.channelId, query.number), - async (query: PartialListQuery, changes: Changes) => { - if (changes.added.length > 0) this.primary.handleMessagesAdded(changes.added); - if (changes.updated.length > 0) this.primary.handleMessagesChanged(changes.updated.map(change => change.newDataPoint)); - if (changes.deleted.length > 0) this.primary.handleMessagesDeleted(changes.deleted); - } - ); - - this.fetchMessagesBeforeVerifier = AutoVerifierWithArg.createPartialListAutoVerifier( - async (query: PartialListQuery) => await this.primary.fetchMessagesBefore(query.channelId, query.messageId as string, query.number), - async (query: PartialListQuery) => await this.trusted.fetchMessagesBefore(query.channelId, query.messageId as string, query.number), - async (query: PartialListQuery, changes: Changes) => { - if (changes.added.length > 0) this.primary.handleMessagesAdded(changes.added); - if (changes.updated.length > 0) this.primary.handleMessagesChanged(changes.updated.map(change => change.newDataPoint)); - if (changes.deleted.length > 0) this.primary.handleMessagesDeleted(changes.deleted); - } - ); - - this.fetchMessagesAfterVerifier = AutoVerifierWithArg.createPartialListAutoVerifier( - async (query: PartialListQuery) => await this.primary.fetchMessagesAfter(query.channelId, query.messageId as string, query.number), - async (query: PartialListQuery) => await this.trusted.fetchMessagesAfter(query.channelId, query.messageId as string, query.number), - async (query: PartialListQuery, changes: Changes) => { - if (changes.added.length > 0) this.primary.handleMessagesAdded(changes.added); - if (changes.updated.length > 0) this.primary.handleMessagesChanged(changes.updated.map(change => change.newDataPoint)); - if (changes.deleted.length > 0) this.primary.handleMessagesDeleted(changes.deleted); - } - ); - } - - unverify() { - this.fetchMetadataVerifier.unverify(); - this.fetchMembersVerifier.unverify(); - this.fetchChannelsVerifier.unverify(); - this.fetchTokensVerifier.unverify(); - - this.fetchResourceVerifier.unverifyAll(); - - this.fetchMessagesRecentVerifier.unverifyAll(); - this.fetchMessagesBeforeVerifier.unverifyAll(); - this.fetchMessagesAfterVerifier.unverifyAll(); - } - - async fetchMetadata(): Promise { - return await this.fetchMetadataVerifier.fetchAndVerifyIfNeeded() as GuildMetadata; // can use 'as' since socket is a Guaranteed fetchable - } - async fetchMembers(): Promise { - return await this.fetchMembersVerifier.fetchAndVerifyIfNeeded() as Member[]; - } - async fetchChannels(): Promise { - return await this.fetchChannelsVerifier.fetchAndVerifyIfNeeded() as Channel[]; - } - async fetchMessagesRecent(channelId: string, number: number): Promise { - return await this.fetchMessagesRecentVerifier.fetchAndVerifyIfNeded({ channelId, messageId: null, number }) as Message[]; - } - async fetchMessagesBefore(channelId: string, messageId: string, number: number): Promise { - return await this.fetchMessagesBeforeVerifier.fetchAndVerifyIfNeded({ channelId, messageId, number }) as Message[]; - } - async fetchMessagesAfter(channelId: string, messageId: string, number: number): Promise { - return await this.fetchMessagesAfterVerifier.fetchAndVerifyIfNeded({ channelId, messageId, number }) as Message[]; - } - async fetchResource(resourceId: string): Promise { - return await this.fetchResourceVerifier.fetchAndVerifyIfNeded({ resourceId }) as Resource; - } - async fetchTokens(): Promise { - return await this.fetchTokensVerifier.fetchAndVerifyIfNeeded() as Token[]; - } -} diff --git a/client/webapp/guild-personal-db.ts b/client/webapp/guild-personal-db.ts index 0224aa6..454f112 100644 --- a/client/webapp/guild-personal-db.ts +++ b/client/webapp/guild-personal-db.ts @@ -1,16 +1,14 @@ -import { AsyncFetchable, AsyncLackable, AsyncLackableConnectable, Connectable } from "./guild-types"; -import { Channel, GuildMetadata, Member, Message, Resource, ServerMetaData, Token } from "./data-types"; +import { AsyncFetchable, AsyncLackable } from "./guild-types"; +import { Channel, GuildMetadata, Member, Message, Resource, Token } from "./data-types"; import PersonalDB from "./personal-db"; -export default class PersonalDBGuild extends AsyncLackableConnectable implements AsyncFetchable { +export default class PersonalDBGuild implements AsyncFetchable, AsyncLackable { constructor( private readonly db: PersonalDB, private readonly guildId: number, private readonly memberId: string, // would be used if we want to cache-update the status/display name of members - ) { - super(); - } + ) {} // Fetched Methods @@ -54,68 +52,53 @@ export default class PersonalDBGuild extends AsyncLackableConnectable implements (async () => { await this.db.updateGuildIcon(this.guildId, changedMetaData.iconResourceId); })(); } - async handleMembersAdded(hadData: boolean, addedMembers: Member[]): Promise { + async handleMembersAdded(addedMembers: Member[]): Promise { await this.db.addMembers(this.guildId, addedMembers); - await super.handleMembersAdded(addedMembers); } async handleMembersChanged(changedMembers: Member[]): Promise { await this.db.updateMembers(this.guildId, changedMembers); - await super.handleMembersChanged(changedMembers); } async handleMembersDeleted(deletedMembers: Member[]): Promise { await this.db.deleteMembers(this.guildId, deletedMembers); - await super.handleMembersDeleted(deletedMembers); } - async handleChannelsAdded(hadData: boolean, addedChannels: Channel[]): Promise { + async handleChannelsAdded(addedChannels: Channel[]): Promise { await this.db.addChannels(this.guildId, addedChannels); - await super.handleChannelsAdded(addedChannels); } async handleChannelsChanged(changedChannels: Channel[]): Promise { await this.db.updateChannels(this.guildId, changedChannels); - await super.handleChannelsChanged(changedChannels); } async handleChannelsDeleted(deletedChannels: Channel[]): Promise { await this.db.deleteChannels(this.guildId, deletedChannels); - await super.handleChannelsDeleted(deletedChannels); } - async handleMessagesAdded(hadData: boolean, addedMessages: Message[]): Promise { + async handleMessagesAdded(addedMessages: Message[]): Promise { await this.db.addMessages(this.guildId, addedMessages); - await super.handleMessagesAdded(addedMessages); } async handleMessagesChanged(changedMessages: Message[]): Promise { await this.db.updateMessages(this.guildId, changedMessages); - await super.handleMembersAdded(addedMembers); } async handleMessagesDeleted(deletedMessages: Message[]): Promise { await this.db.deleteMessages(this.guildId, deletedMessages); - await super.handleMessagesDeleted(deletedMessages); } - async handleResourceAdded(hadData: boolean, addedResource: Resource): Promise { + async handleResourceAdded(addedResource: Resource): Promise { await this.db.addResources(this.guildId, [ addedResource ]); - await super.handleResourceAdded(addedResource); } async handleResourceChanged(changedResource: Resource): Promise { await this.db.updateResources(this.guildId, [ changedResource ]); - await super.handleResourceChanged(changedResource); } async handleResourceDeleted(deletedResource: Resource): Promise { await this.db.deleteResources(this.guildId, [ deletedResource ]); - await super.handleResourceDeleted(deletedResource); } - async handleTokensAdded(hadData: boolean, addedTokens: Token[]): Promise { - await super.handleTokensAdded(addedTokens); + async handleTokensAdded(addedTokens: Token[]): Promise { return; // cache currently does not handle tokens } async handleTokensChanged(changedTokens: Token[]): Promise { - await super.handleTokensChanged(changedTokens); return; // cache currently does not handle tokens } async handleTokensDeleted(deletedTokens: Token[]): Promise { - await super.handleTokensDeleted(deletedTokens); return; // cache currently does not handle tokens } } \ No newline at end of file diff --git a/client/webapp/guild-ram.ts b/client/webapp/guild-ram.ts index 0488e25..8ce85e6 100644 --- a/client/webapp/guild-ram.ts +++ b/client/webapp/guild-ram.ts @@ -50,7 +50,7 @@ export default class RAMGuild implements SyncFetchable, SyncLackable { this.metadata = changedMetaData; } - handleMembersAdded(hadData: boolean, addedMembers: Member[]): void { + handleMembersAdded(addedMembers: Member[]): void { for (let member of addedMembers) { this.members.set(member.id, member); } @@ -66,7 +66,7 @@ export default class RAMGuild implements SyncFetchable, SyncLackable { } } - handleChannelsAdded(hadData: boolean, addedChannels: Channel[]): void { + handleChannelsAdded(addedChannels: Channel[]): void { for (let channel of addedChannels) { this.channels.set(channel.id, channel); } @@ -82,7 +82,7 @@ export default class RAMGuild implements SyncFetchable, SyncLackable { } } - handleMessagesAdded(hadData: boolean, addedMessages: Message[]): void { + handleMessagesAdded(addedMessages: Message[]): void { let byChannel = new Map(); for (let message of addedMessages) { if (!byChannel.has(message.channel.id)) { @@ -122,7 +122,7 @@ export default class RAMGuild implements SyncFetchable, SyncLackable { } } - handleResourceAdded(hadData: boolean, addedResource: Resource): void { + handleResourceAdded(addedResource: Resource): void { this.resources.putResource(this.guildId, addedResource); } handleResourceChanged(changedResource: Resource): void { @@ -135,7 +135,7 @@ export default class RAMGuild implements SyncFetchable, SyncLackable { return; } - handleTokensAdded(hadData: boolean, addedTokens: Token[]): void { + handleTokensAdded(addedTokens: Token[]): void { return; // TODO: token ram cache } handleTokensChanged(changedTokens: Token[]): void { diff --git a/client/webapp/guild-socket.ts b/client/webapp/guild-socket.ts index b56309e..1a64482 100644 --- a/client/webapp/guild-socket.ts +++ b/client/webapp/guild-socket.ts @@ -10,9 +10,10 @@ import { Connectable, AsyncRequestable, AsyncGuaranteedFetchable } from './guild import DedupAwaiter from './dedup-awaiter'; import Util from './util'; import SocketVerifier from './socket-verifier'; +import { EventEmitter } from 'tsee'; // Note: you should not be calling the eventemitter functions on outside classes -export default class SocketGuild extends Connectable implements AsyncGuaranteedFetchable, AsyncRequestable { +export default class SocketGuild extends EventEmitter implements AsyncGuaranteedFetchable, AsyncRequestable { private queryDedups = new Map>(); constructor( @@ -20,7 +21,6 @@ export default class SocketGuild extends Connectable implements AsyncGuaranteedF private verifier: SocketVerifier ) { super(); - // TODO: These requests should be 'bindable' through a connectable structure this.socket.on('connect', async () => { this.emit('connect'); }); @@ -28,18 +28,6 @@ export default class SocketGuild extends Connectable implements AsyncGuaranteedF this.emit('disconnect'); }); - // TODO: Make sure the UI updates when we get an added/updated member - - // TODO: The server does not emit update-message - this.socket.on('update-message', async (updatedDataMessage: any) => { - let updatedMessage = Message.fromDBData(updatedDataMessage); - this.emit('update-messages', [ updatedMessage ]); - }); - this.socket.on('new-message', async (dataMessage: any) => { - let message = Message.fromDBData(dataMessage); - this.emit('new-messages', [ message ]); - }); - this.socket.on('update-member', async (updatedDataMember: any) => { let updatedMember = Member.fromDBData(updatedDataMember); this.emit('update-members', [ updatedMember ]); @@ -63,6 +51,16 @@ export default class SocketGuild extends Connectable implements AsyncGuaranteedF let guildMeta = GuildMetadata.fromDBData(dataMeta); this.emit('update-metadata', guildMeta); }); + + // TODO: The server does not emit update-message + this.socket.on('update-message', async (updatedDataMessage: any) => { + let updatedMessage = Message.fromDBData(updatedDataMessage); + this.emit('update-messages', [ updatedMessage ]); + }); + this.socket.on('new-message', async (dataMessage: any) => { + let message = Message.fromDBData(dataMessage); + this.emit('new-messages', [ message ]); + }); } // server helper functions diff --git a/client/webapp/guild-types.ts b/client/webapp/guild-types.ts index e403803..a987bc3 100644 --- a/client/webapp/guild-types.ts +++ b/client/webapp/guild-types.ts @@ -1,5 +1,5 @@ -import { Channel, GuildMetadata, Member, Message, Resource, Token } from './data-types'; -import { EventEmitter } from 'tsee'; +import { Changes, Channel, GuildMetadata, Member, Message, Resource, Token } from './data-types'; +import { DefaultEventMap, EventEmitter } from 'tsee'; // Fetchable @@ -60,23 +60,23 @@ export type Requestable = AsyncRequestable; export interface AsyncLackable { handleMetadataChanged(changedMetaData: GuildMetadata): Promise; - handleMembersAdded(hadData: boolean, addedMembers: Member[]): Promise; + handleMembersAdded(addedMembers: Member[]): Promise; handleMembersChanged(changedMembers: Member[]): Promise; handleMembersDeleted(deletedMembers: Member[]): Promise; - handleChannelsAdded(hadData: boolean, addedChannels: Channel[]): Promise; + handleChannelsAdded(addedChannels: Channel[]): Promise; handleChannelsChanged(changedChannels: Channel[]): Promise; handleChannelsDeleted(deletedChannels: Channel[]): Promise; - handleMessagesAdded(hadData: boolean, addedMessages: Message[]): Promise; + handleMessagesAdded(addedMessages: Message[]): Promise; handleMessagesChanged(changedMessages: Message[]): Promise; handleMessagesDeleted(deletedMessages: Message[]): Promise; - handleResourceAdded(hadData: boolean, addedResource: Resource): Promise; + handleResourceAdded(addedResource: Resource): Promise; handleResourceChanged(changedResource: Resource): Promise; handleResourceDeleted(deletedResource: Resource): Promise; - handleTokensAdded(hadData: boolean, addedTokens: Token[]): Promise; + handleTokensAdded(addedTokens: Token[]): Promise; handleTokensChanged(changedTokens: Token[]): Promise; handleTokensDeleted(deletedTokens: Token[]): Promise; } @@ -84,126 +84,55 @@ export interface AsyncLackable { export interface SyncLackable { handleMetadataChanged(changedMetaData: GuildMetadata): void; - handleMembersAdded(hadData: boolean, addedMembers: Member[]): void; + handleMembersAdded(addedMembers: Member[]): void; handleMembersChanged(changedMembers: Member[]): void; handleMembersDeleted(deletedMembers: Member[]): void; - handleChannelsAdded(hadData: boolean, addedChannels: Channel[]): void; + handleChannelsAdded(addedChannels: Channel[]): void; handleChannelsChanged(changedChannels: Channel[]): void; handleChannelsDeleted(deletedChannels: Channel[]): void; - handleMessagesAdded(hadData: boolean, addedMessages: Message[]): void; + handleMessagesAdded(addedMessages: Message[]): void; handleMessagesChanged(changedMessages: Message[]): void; handleMessagesDeleted(deletedMessages: Message[]): void; - handleResourceAdded(hadData: boolean, addedResource: Resource): void; + handleResourceAdded(addedResource: Resource): void; handleResourceChanged(changedResource: Resource): void; handleResourceDeleted(deletedResource: Resource): void; - handleTokensAdded(hadData: boolean, addedTokens: Token[]): void; + handleTokensAdded(addedTokens: Token[]): void; handleTokensChanged(changedTokens: Token[]): void; handleTokensDeleted(deletedTokens: Token[]): void; } export type Lackable = AsyncLackable | SyncLackable; -// Connectable -export class Connectable extends EventEmitter<{ - 'connect': () => void, - 'disconnect': () => void, - 'update-metadata': (guildMeta: GuildMetadata) => void +// A Connectable can emit server-like events +export type Connectable = { + 'connect': () => void; + 'disconnect': () => void; + + 'update-metadata': (guildMeta: GuildMetadata) => void; - 'new-channels': (channels: Channel[]) => void, - 'update-channels': (updatedChannels: Channel[]) => void, + 'new-channels': (channels: Channel[]) => void; + 'update-channels': (updatedChannels: Channel[]) => void; - 'new-members': (members: Member[]) => void, - 'update-members': (updatedMembers: Member[]) => void, + 'new-members': (members: Member[]) => void; + 'update-members': (updatedMembers: Member[]) => void; - 'new-messages': (messages: Message[]) => void, - 'update-messages': (updatedMessages: Message[]) => void, -}> { - constructor() { super(); } + 'new-messages': (messages: Message[]) => void; + 'update-messages': (updatedMessages: Message[]) => void; } -export class AsyncLackableConnectable extends EventEmitter<{ - 'updated-metadata': (guildMeta: GuildMetadata) => void, - - 'added-channels': (channels: Channel[]) => void, - 'updated-channels': (updatedChannels: Channel[]) => void, - 'deleted-channels': (channels: Channel[]) => void, - - 'added-members': (members: Member[]) => void, - 'updated-members': (updatedMembers: Member[]) => void, - 'deleted-members': (members: Member[]) => void, - - 'added-messages': (messages: Message[]) => void, - 'updated-messages': (updatedMessages: Message[]) => void, - 'deleted-messages': (messages: Message[]) => void, - - 'added-resource': (resource: Resource) => void, - 'updated-resource': (updatedResource: Resource) => void, - 'deleted-resource': (resource: Resource) => void, - - 'added-tokens': (tokens: Token[]) => void, - 'updated-tokens': (updatedTokens: Token[]) => void, - 'deleted-tokens': (tokens: Token[]) => void, -}> implements AsyncLackable { - constructor() { super(); } - async handleMetadataChanged(changedMetaData: GuildMetadata): Promise { - this.emit('updated-metadata', changedMetaData); - } - - async handleMembersAdded(hadData: boolean, addedMembers: Member[]): Promise { - if (hadData) this.emit('added-members', addedMembers); - } - async handleMembersChanged(changedMembers: Member[]): Promise { - this.emit('updated-members', changedMembers); - } - async handleMembersDeleted(deletedMembers: Member[]): Promise { - this.emit('deleted-members', deletedMembers); - } - - async handleChannelsAdded(hadData: boolean, addedChannels: Channel[]): Promise { - if (hadData) this.emit('added-channels', addedChannels); - } - async handleChannelsChanged(changedChannels: Channel[]): Promise { - this.emit('updated-channels', changedChannels); - } - async handleChannelsDeleted(deletedChannels: Channel[]): Promise { - this.emit('deleted-channels', deletedChannels); - } - - // TODO: These are most likely pretty messy... - async handleMessagesAdded(hadData: boolean, addedMessages: Message[]): Promise { - if (hadData) this.emit('added-messages', addedMessages); - } - async handleMessagesChanged(changedMessages: Message[]): Promise { - this.emit('updated-messages', changedMessages); - } - async handleMessagesDeleted(deletedMessages: Message[]): Promise { - this.emit('deleted-messages', deletedMessages); - } - - async handleResourceAdded(hadData: boolean, addedResource: Resource): Promise { - if (hadData) this.emit('added-resource', addedResource); - } - async handleResourceChanged(changedResource: Resource): Promise { - this.emit('updated-resource', changedResource); - } - async handleResourceDeleted(deletedResource: Resource): Promise { - this.emit('deleted-resource', deletedResource); - } - - async handleTokensAdded(hadData: boolean, addedTokens: Token[]): Promise { - if (hadData) this.emit('added-tokens', addedTokens); - } - async handleTokensChanged(changedTokens: Token[]): Promise { - this.emit('updated-tokens', changedTokens); - } - async handleTokensDeleted(deletedTokens: Token[]): Promise { - this.emit('deleted-tokens', deletedTokens); - } +// 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': (oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void; + 'conflict-channels': (changes: Changes) => void; + 'conflict-members': (changes: Changes) => void; + 'conflict-messages': (changes: Changes) => void; + 'conflict-tokens': (changes: Changes) => void; + 'conflict-resource': (oldResource: Resource, newResource: Resource) => void; } -