diff --git a/js/background.js b/js/background.js index fc18b5435a..558c143c65 100644 --- a/js/background.js +++ b/js/background.js @@ -450,11 +450,7 @@ }, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000); window.NewReceiver.queueAllCached(); - window - .getSwarmPollingInstance() - .addPubkey(window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache()); - window.getSwarmPollingInstance().start(); window.libsession.Utils.AttachmentDownloads.start({ logger: window.log, }); diff --git a/package.json b/package.json index 72a9eb2157..a99ffbff18 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "format-full": "prettier --list-different --write \"*.{css,js,json,scss,ts,tsx}\" \"./**/*.{css,js,json,scss,ts,tsx}\"", "transpile": "tsc --incremental", "transpile:watch": "tsc -w", - "clean-transpile": "rimraf ts/**/*.js ts/*.js ts/*.js.map ts/**/*.js.map && rimraf tsconfig.tsbuildinfo;", + "clean-transpile": "rimraf 'ts/**/*.js ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map' && rimraf tsconfig.tsbuildinfo;", "ready": "yarn clean-transpile; yarn grunt && yarn lint-full && yarn test" }, "dependencies": { diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 5b3d349827..6c32f113d9 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -245,9 +245,7 @@ const doAppStartUp = () => { debounce(triggerAvatarReUploadIfNeeded, 200); // TODO: Investigate the case where we reconnect - const ourKey = UserUtils.getOurPubKeyStrFromCache(); - getSwarmPollingInstance().addPubkey(ourKey); - getSwarmPollingInstance().start(); + void getSwarmPollingInstance().start(); }; /** diff --git a/ts/data/data.ts b/ts/data/data.ts index cb2826eea1..2613be494f 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -1,7 +1,7 @@ import Electron from 'electron'; const { ipcRenderer } = Electron; -// tslint:disable: function-name no-require-imports no-var-requires one-variable-per-declaration no-void-expression +// tslint:disable: no-require-imports no-var-requires one-variable-per-declaration no-void-expression import _ from 'lodash'; import { ConversationCollection, ConversationModel } from '../models/conversation'; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index bae1f445af..c8c13fac06 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -44,6 +44,7 @@ import { NotificationForConvoOption } from '../components/conversation/Conversat import { useDispatch } from 'react-redux'; import { updateConfirmModal } from '../state/ducks/modalDialog'; import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout'; +import { DURATION, SWARM_POLLING_TIMEOUT } from '../session/constants'; export enum ConversationTypeEnum { GROUP = 'group', diff --git a/ts/session/constants.ts b/ts/session/constants.ts index eebeb41202..df97e93d2c 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -17,6 +17,12 @@ export const TTL_DEFAULT = { TTL_MAX: 14 * DURATION.DAYS, }; +export const SWARM_POLLING_TIMEOUT = { + ACTIVE: DURATION.SECONDS * 5, + MEDIUM_ACTIVE: DURATION.SECONDS * 60, + INACTIVE: DURATION.MINUTES * 60, +}; + export const PROTOCOLS = { // tslint:disable-next-line: no-http-string HTTP: 'http:', diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index fdf7faed18..5d249c1b4a 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -116,7 +116,7 @@ export class ConversationController { conversation.initialPromise = create(); conversation.initialPromise.then(async () => { - if (window.inboxStore) { + if (window?.inboxStore) { window.inboxStore?.dispatch( conversationActions.conversationAdded(conversation.id, conversation.getProps()) ); @@ -242,7 +242,7 @@ export class ConversationController { window.log.info(`deleteContact !isPrivate, convo removed from DB: ${id}`); this.conversations.remove(conversation); - if (window.inboxStore) { + if (window?.inboxStore) { window.inboxStore?.dispatch(conversationActions.conversationRemoved(conversation.id)); window.inboxStore?.dispatch( conversationActions.conversationChanged(conversation.id, conversation.getProps()) @@ -310,7 +310,7 @@ export class ConversationController { public reset() { this._initialPromise = Promise.resolve(); this._initialFetchComplete = false; - if (window.inboxStore) { + if (window?.inboxStore) { window.inboxStore?.dispatch(conversationActions.removeAllConversations()); } this.conversations.reset([]); diff --git a/ts/session/messages/MessageController.ts b/ts/session/messages/MessageController.ts index aaa7d37a18..c3e276abe1 100644 --- a/ts/session/messages/MessageController.ts +++ b/ts/session/messages/MessageController.ts @@ -69,7 +69,6 @@ export class MessageController { }); } - // tslint:disable-next-line: function-name public get(identifier: string) { return this.messageLookup.get(identifier); } diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index ea2512e458..87239c4270 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -1,5 +1,5 @@ import { PubKey } from '../types'; -import { getSwarmFor } from './snodePool'; +import * as snodePool from './snodePool'; import { retrieveNextMessages } from './SNodeAPI'; import { SignalService } from '../../protobuf'; import * as Receiver from '../../receiver/receiver'; @@ -12,9 +12,10 @@ import { updateLastHash, } from '../../../ts/data/data'; -import { StringUtils } from '../../session/utils'; -import { getConversationController } from '../conversations'; +import { StringUtils, UserUtils } from '../../session/utils'; import { ConversationModel } from '../../models/conversation'; +import { DURATION, SWARM_POLLING_TIMEOUT } from '../constants'; +import { getConversationController } from '../conversations'; type PubkeyToHash = { [key: string]: string }; @@ -50,49 +51,133 @@ export const getSwarmPollingInstance = () => { }; export class SwarmPolling { - private pubkeys: Array; - private groupPubkeys: Array; + private ourPubkey: PubKey | undefined; + private groupPolling: Array<{ pubkey: PubKey; lastPolledTimestamp: number }>; private readonly lastHashes: { [key: string]: PubkeyToHash }; constructor() { - this.pubkeys = []; - this.groupPubkeys = []; + this.groupPolling = []; this.lastHashes = {}; } - public start(): void { + public async start(waitForFirstPoll = false): Promise { + this.ourPubkey = UserUtils.getOurPubKeyFromCache(); this.loadGroupIds(); - void this.pollForAllKeys(); + if (waitForFirstPoll) { + await this.TEST_pollForAllKeys(); + } else { + void this.TEST_pollForAllKeys(); + } + } + + /** + * Used fo testing only + */ + public TEST_reset() { + this.ourPubkey = undefined; + this.groupPolling = []; } public addGroupId(pubkey: PubKey) { - if (this.groupPubkeys.findIndex(m => m.key === pubkey.key) === -1) { + if (this.groupPolling.findIndex(m => m.pubkey.key === pubkey.key) === -1) { window?.log?.info('Swarm addGroupId: adding pubkey to polling', pubkey.key); - this.groupPubkeys.push(pubkey); + this.groupPolling.push({ pubkey, lastPolledTimestamp: 0 }); } } - public addPubkey(pk: PubKey | string) { + public removePubkey(pk: PubKey | string) { const pubkey = PubKey.cast(pk); - if (this.pubkeys.findIndex(m => m.key === pubkey.key) === -1) { - this.pubkeys.push(pubkey); + window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key); + + if (this.ourPubkey && PubKey.cast(pk).isEqual(this.ourPubkey)) { + this.ourPubkey = undefined; } + this.groupPolling = this.groupPolling.filter(group => !pubkey.isEqual(group.pubkey)); } - public removePubkey(pk: PubKey | string) { - const pubkey = PubKey.cast(pk); - window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key); + /** + * Only public for testing + * As of today, we pull closed group pubkeys as follow: + * if activeAt is not set, poll only once per hour + * if activeAt is less than an hour old, poll every 5 seconds or so + * if activeAt is less than a day old, poll every minutes only. + * If activeAt is more than a day old, poll only once per hour + */ + public TEST_getPollingTimeout(convoId: PubKey) { + const convo = getConversationController().get(convoId.key); + if (!convo) { + return SWARM_POLLING_TIMEOUT.INACTIVE; + } + const activeAt = convo.get('active_at'); + if (!activeAt) { + return SWARM_POLLING_TIMEOUT.INACTIVE; + } + + const currentTimestamp = Date.now(); - this.pubkeys = this.pubkeys.filter(key => !pubkey.isEqual(key)); - this.groupPubkeys = this.groupPubkeys.filter(key => !pubkey.isEqual(key)); + // consider that this is an active group if activeAt is less than an hour old + if (currentTimestamp - activeAt <= DURATION.HOURS * 1) { + return SWARM_POLLING_TIMEOUT.ACTIVE; + } + + if (currentTimestamp - activeAt <= DURATION.DAYS * 1) { + return SWARM_POLLING_TIMEOUT.MEDIUM_ACTIVE; + } + + return SWARM_POLLING_TIMEOUT.INACTIVE; } - protected async pollOnceForKey(pubkey: PubKey, isGroup: boolean) { + /** + * Only public for testing + */ + public async TEST_pollForAllKeys() { + // we always poll as often as possible for our pubkey + const directPromise = this.ourPubkey + ? this.TEST_pollOnceForKey(this.ourPubkey, false) + : Promise.resolve(); + + const now = Date.now(); + const groupPromises = this.groupPolling.map(async group => { + const convoPollingTimeout = this.TEST_getPollingTimeout(group.pubkey); + + const diff = now - group.lastPolledTimestamp; + + const loggingId = + getConversationController() + .get(group.pubkey.key) + ?.idForLogging() || group.pubkey.key; + + if (diff >= convoPollingTimeout) { + (window?.log?.info || console.warn)( + `Polling for ${loggingId}; timeout: ${convoPollingTimeout} ; diff: ${diff}` + ); + return this.TEST_pollOnceForKey(group.pubkey, true); + } + (window?.log?.info || console.warn)( + `Not polling for ${loggingId}; timeout: ${convoPollingTimeout} ; diff: ${diff}` + ); + + return Promise.resolve(); + }); + try { + await Promise.all(_.concat(directPromise, groupPromises)); + } catch (e) { + (window?.log?.info || console.warn)('pollForAllKeys swallowing exception: ', e); + throw e; + } finally { + setTimeout(this.TEST_pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE); + } + } + + /** + * Only exposed as public for testing + */ + public async TEST_pollOnceForKey(pubkey: PubKey, isGroup: boolean) { // NOTE: sometimes pubkey is string, sometimes it is object, so // accept both until this is fixed: const pkStr = pubkey.key; - const snodes = await getSwarmFor(pkStr); + const snodes = await snodePool.getSwarmFor(pkStr); // Select nodes for which we already have lastHashes const alreadyPolled = snodes.filter((n: Snode) => this.lastHashes[n.pubkey_ed25519]); @@ -123,6 +208,19 @@ export class SwarmPolling { // Merge results into one list of unique messages const messages = _.uniqBy(_.flatten(results), (x: any) => x.hash); + if (isGroup) { + // update the last fetched timestamp + this.groupPolling = this.groupPolling.map(group => { + if (PubKey.isEqual(pubkey, group.pubkey)) { + return { + ...group, + lastPolledTimestamp: Date.now(), + }; + } + return group; + }); + } + const newMessages = await this.handleSeenMessages(messages); newMessages.forEach((m: Message) => { @@ -133,7 +231,7 @@ export class SwarmPolling { // Fetches messages for `pubkey` from `node` potentially updating // the lash hash record - protected async pollNodeForKey(node: Snode, pubkey: PubKey): Promise> { + private async pollNodeForKey(node: Snode, pubkey: PubKey): Promise> { const edkey = node.pubkey_ed25519; const pkStr = pubkey.key; @@ -188,24 +286,6 @@ export class SwarmPolling { return newMessages; } - private async pollForAllKeys() { - const directPromises = this.pubkeys.map(async pk => { - return this.pollOnceForKey(pk, false); - }); - - const groupPromises = this.groupPubkeys.map(async pk => { - return this.pollOnceForKey(pk, true); - }); - try { - await Promise.all(_.concat(directPromises, groupPromises)); - } catch (e) { - window?.log?.warn('pollForAllKeys swallowing exception: ', e); - throw e; - } finally { - setTimeout(this.pollForAllKeys.bind(this), 2000); - } - } - private async updateLastHash( edkey: string, pubkey: PubKey, diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index 94ef6f6864..63adcd9d9f 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -152,6 +152,10 @@ export class PubKey { return key.replace(PubKey.PREFIX_GROUP_TEXTSECURE, ''); } + public static isEqual(comparator1: PubKey | string, comparator2: PubKey | string) { + return PubKey.cast(comparator1).isEqual(comparator2); + } + public isEqual(comparator: PubKey | string) { return comparator instanceof PubKey ? this.key === comparator.key diff --git a/ts/session/utils/AttachmentsDownload.ts b/ts/session/utils/AttachmentsDownload.ts index 6ae23e6f8d..9df66264e5 100644 --- a/ts/session/utils/AttachmentsDownload.ts +++ b/ts/session/utils/AttachmentsDownload.ts @@ -90,7 +90,6 @@ export async function addJob(attachment: any, job: any = {}) { }; } -// tslint:disable: function-name async function _tick() { await _maybeStartJob(); timeout = setTimeout(_tick, TICK_INTERVAL); diff --git a/ts/state/ducks/defaultRooms.tsx b/ts/state/ducks/defaultRooms.tsx index 40dd860e88..ab08894adb 100644 --- a/ts/state/ducks/defaultRooms.tsx +++ b/ts/state/ducks/defaultRooms.tsx @@ -33,7 +33,6 @@ const defaultRoomsSlice = createSlice({ }, updateDefaultRoomsInProgress(state, action) { const inProgress = action.payload as boolean; - window?.log?.info('fetching default rooms inProgress?', action.payload); return { ...state, inProgress }; }, updateDefaultBase64RoomData(state, action: PayloadAction) { diff --git a/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts b/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts new file mode 100644 index 0000000000..5ff3eca583 --- /dev/null +++ b/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts @@ -0,0 +1,307 @@ +// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression + +import chai from 'chai'; +import Sinon, * as sinon from 'sinon'; +import _, { noop } from 'lodash'; +import { describe } from 'mocha'; + +import chaiAsPromised from 'chai-as-promised'; +import { TestUtils } from '../../../test-utils'; +import { UserUtils } from '../../../../session/utils'; +import { getConversationController } from '../../../../session/conversations'; +import * as Data from '../../../../../ts/data/data'; +import { getSwarmPollingInstance, SnodePool } from '../../../../session/snode_api'; +import { SwarmPolling } from '../../../../session/snode_api/swarmPolling'; +import { SWARM_POLLING_TIMEOUT } from '../../../../session/constants'; +import { + ConversationCollection, + ConversationModel, + ConversationTypeEnum, +} from '../../../../models/conversation'; +import { PubKey } from '../../../../session/types'; +// tslint:disable: chai-vague-errors + +chai.use(chaiAsPromised as any); +chai.should(); + +const { expect } = chai; + +// tslint:disable-next-line: max-func-body-length +describe('SwarmPolling', () => { + // Initialize new stubbed cache + const sandbox = sinon.createSandbox(); + const ourPubkey = TestUtils.generateFakePubKey(); + const ourNumber = ourPubkey.key; + + let pollOnceForKeySpy: Sinon.SinonSpy; + + let swarmPolling: SwarmPolling; + + let clock: Sinon.SinonFakeTimers; + beforeEach(async () => { + // Utils Stubs + sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber); + + sandbox.stub(Data, 'getAllConversations').resolves(new ConversationCollection()); + sandbox.stub(Data, 'getItemById').resolves(); + sandbox.stub(Data, 'saveConversation').resolves(); + sandbox.stub(Data, 'getSwarmNodesForPubkey').resolves(); + sandbox.stub(SnodePool, 'getSwarmFor').resolves([]); + TestUtils.stubWindow('profileImages', { removeImagesNotInArray: noop, hasImage: noop }); + TestUtils.stubWindow('inboxStore', undefined); + const convoController = getConversationController(); + await convoController.load(); + getConversationController().getOrCreate(ourPubkey.key, ConversationTypeEnum.PRIVATE); + + swarmPolling = getSwarmPollingInstance(); + swarmPolling.TEST_reset(); + pollOnceForKeySpy = sandbox.spy(swarmPolling, 'TEST_pollOnceForKey'); + + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(() => { + TestUtils.restoreStubs(); + sandbox.restore(); + getConversationController().reset(); + clock.restore(); + }); + + describe('getPollingTimeout', () => { + it('returns INACTIVE for non existing convo', () => { + const fakeConvo = TestUtils.generateFakePubKey(); + + expect(swarmPolling.TEST_getPollingTimeout(fakeConvo)).to.eq(SWARM_POLLING_TIMEOUT.INACTIVE); + }); + + it('returns ACTIVE for convo with less than an hour old activeAt', () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + convo.set('active_at', Date.now() - 3555); + expect(swarmPolling.TEST_getPollingTimeout(PubKey.cast(convo.id))).to.eq( + SWARM_POLLING_TIMEOUT.ACTIVE + ); + }); + + it('returns INACTIVE for convo with undefined activeAt', () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + convo.set('active_at', undefined); + expect(swarmPolling.TEST_getPollingTimeout(PubKey.cast(convo.id))).to.eq( + SWARM_POLLING_TIMEOUT.INACTIVE + ); + }); + + it('returns MEDIUM_ACTIVE for convo with activeAt of less than a day', () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + convo.set('active_at', Date.now() - 1000 * 3600 * 23); + expect(swarmPolling.TEST_getPollingTimeout(PubKey.cast(convo.id))).to.eq( + SWARM_POLLING_TIMEOUT.MEDIUM_ACTIVE + ); + }); + + it('returns INACTIVE for convo with activeAt of more than a day', () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + convo.set('active_at', Date.now() - 1000 * 3600 * 25); + expect(swarmPolling.TEST_getPollingTimeout(PubKey.cast(convo.id))).to.eq( + SWARM_POLLING_TIMEOUT.INACTIVE + ); + }); + }); + + describe('pollForAllKeys', () => { + it('does run for our pubkey even if activeAt is really old ', async () => { + const convo = getConversationController().getOrCreate( + ourNumber, + ConversationTypeEnum.PRIVATE + ); + convo.set('active_at', Date.now() - 1000 * 3600 * 25); + await swarmPolling.start(true); + + expect(pollOnceForKeySpy.callCount).to.eq(1); + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]); + }); + + it('does run for our pubkey even if activeAt is recent ', async () => { + const convo = getConversationController().getOrCreate( + ourNumber, + ConversationTypeEnum.PRIVATE + ); + convo.set('active_at', Date.now()); + await swarmPolling.start(true); + + expect(pollOnceForKeySpy.callCount).to.eq(1); + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]); + }); + + it('does run for group pubkey on start no matter the recent timestamp ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + convo.set('active_at', Date.now()); + const groupConvoPubkey = PubKey.cast(convo.id); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + + // our pubkey will be polled for, hence the 2 + expect(pollOnceForKeySpy.callCount).to.eq(2); + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]); + }); + + it('does run for group pubkey on start no matter the old timestamp ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + + convo.set('active_at', 1); + const groupConvoPubkey = PubKey.cast(convo.id); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + + // our pubkey will be polled for, hence the 2 + expect(pollOnceForKeySpy.callCount).to.eq(2); + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]); + }); + + it('does run for group pubkey on start but not another time if activeAt is old ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + + convo.set('active_at', 1); + const groupConvoPubkey = PubKey.cast(convo.id); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + + await swarmPolling.TEST_pollForAllKeys(); + + expect(pollOnceForKeySpy.callCount).to.eq(3); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false]); + }); + + it('does run twice if activeAt less than one hour ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + + convo.set('active_at', Date.now()); + const groupConvoPubkey = PubKey.cast(convo.id); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + clock.tick(6000); + // no need to do that as the tick will trigger a call in all cases after 5 secs + // await swarmPolling.TEST_pollForAllKeys(); + + expect(pollOnceForKeySpy.callCount).to.eq(4); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false]); + expect(pollOnceForKeySpy.lastCall.args).to.deep.eq([groupConvoPubkey, true]); + }); + + it('does run once only if activeAt is more than one hour', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + + convo.set('active_at', Date.now()); + const groupConvoPubkey = PubKey.cast(convo.id); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + + // more than hour old, we should not tick after just 5 seconds + convo.set('active_at', Date.now() - 3605 * 1000); + + clock.tick(6000); + + expect(pollOnceForKeySpy.callCount).to.eq(3); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false]); + }); + + it('does run once if activeAt is more than 1 days old ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + + convo.set('active_at', Date.now()); + const groupConvoPubkey = PubKey.cast(convo.id); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + + // more than hour old, we should not tick after just 5 seconds + convo.set('active_at', Date.now() - 25 * 3600 * 1000); + + clock.tick(6 * 1000); // active + + expect(pollOnceForKeySpy.callCount).to.eq(3); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false]); + }); + + describe('multiple runs', () => { + let convo: ConversationModel; + let groupConvoPubkey: PubKey; + + beforeEach(async () => { + convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + + convo.set('active_at', Date.now()); + groupConvoPubkey = PubKey.cast(convo.id); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + }); + + it('does run twice if activeAt is more than 1 hour old and we tick more than one minute ', async () => { + pollOnceForKeySpy.resetHistory(); + // more than hour old but less than a day, we should tick after just 60 seconds + convo.set('active_at', Date.now() - 3605 * 1000); + + clock.tick(61 * 1000); // medium_active + + await swarmPolling.TEST_pollForAllKeys(); + expect(pollOnceForKeySpy.callCount).to.eq(3); + // first two calls are our pubkey + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([ourPubkey, false]); + + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([groupConvoPubkey, true]); + }); + + it('does run twice if activeAt is more than 1 day old and we tick more than one hour ', async () => { + pollOnceForKeySpy.resetHistory(); + convo.set('active_at', Date.now() - 25 * 3600 * 1000); + + clock.tick(3700 * 1000); // inactive + + await swarmPolling.TEST_pollForAllKeys(); + expect(pollOnceForKeySpy.callCount).to.eq(3); + // first two calls are our pubkey + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([ourPubkey, false]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([groupConvoPubkey, true]); + }); + }); + }); +}); diff --git a/ts/test/test-utils/utils/pubkey.ts b/ts/test/test-utils/utils/pubkey.ts index 04f17bfc69..b73ac6d43c 100644 --- a/ts/test/test-utils/utils/pubkey.ts +++ b/ts/test/test-utils/utils/pubkey.ts @@ -11,6 +11,15 @@ export function generateFakePubKey(): PubKey { return new PubKey(pubkeyString); } +export function generateFakePubKeyStr(): string { + // Generates a mock pubkey for testing + const numBytes = PubKey.PUBKEY_LEN / 2 - 1; + const hexBuffer = crypto.randomBytes(numBytes).toString('hex'); + const pubkeyString = `05${hexBuffer}`; + + return pubkeyString; +} + export function generateFakeECKeyPair(): ECKeyPair { const pubkey = generateFakePubKey().toArray(); const privKey = new Uint8Array(crypto.randomBytes(64)); diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts index abdfc4b06d..d16399d5d8 100644 --- a/ts/util/blockedNumberController.ts +++ b/ts/util/blockedNumberController.ts @@ -1,4 +1,4 @@ -import { createOrUpdateItem, getItemById } from '../../ts/data/data'; +import { createOrUpdateItem, getItemById } from '../data/data'; import { PubKey } from '../session/types'; import { UserUtils } from '../session/utils'; diff --git a/tslint.json b/tslint.json index 4f804d0152..79c9acaf19 100644 --- a/tslint.json +++ b/tslint.json @@ -65,10 +65,10 @@ "function-name": [ true, { - "function-regex": "^[a-z][\\w\\d]+$", - "method-regex": "^[a-z][\\w\\d]+$", - "private-method-regex": "^[a-z][\\w\\d]+$", - "protected-method-regex": "^[a-z][\\w\\d]+$", + "function-regex": "^(TEST_)?(_)?[a-z][\\w\\d]+$", + "method-regex": "^(TEST_)?(_)?[a-z][\\w\\d]+$", + "private-method-regex": "^(TEST_)?(_)?[a-z][\\w\\d]+$", + "protected-method-regex": "^(TEST_)?(_)?[a-z][\\w\\d]+$", "static-method-regex": "^[a-zA-Z][\\w\\d]+$" } ],