Change order of syncs during linking
This commit is contained in:
parent
4f869e7900
commit
3b4106d9dd
5 changed files with 142 additions and 90 deletions
150
ts/background.ts
150
ts/background.ts
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { webFrame } from 'electron';
|
import { webFrame } from 'electron';
|
||||||
import { isNumber, noop } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { batch as batchDispatch } from 'react-redux';
|
import { batch as batchDispatch } from 'react-redux';
|
||||||
|
@ -13,7 +13,7 @@ import type {
|
||||||
ProcessedDataMessage,
|
ProcessedDataMessage,
|
||||||
} from './textsecure/Types.d';
|
} from './textsecure/Types.d';
|
||||||
import { HTTPError } from './textsecure/Errors';
|
import { HTTPError } from './textsecure/Errors';
|
||||||
import {
|
import createTaskWithTimeout, {
|
||||||
suspendTasksWithTimeout,
|
suspendTasksWithTimeout,
|
||||||
resumeTasksWithTimeout,
|
resumeTasksWithTimeout,
|
||||||
} from './textsecure/TaskWithTimeout';
|
} from './textsecure/TaskWithTimeout';
|
||||||
|
@ -34,7 +34,6 @@ import * as durations from './util/durations';
|
||||||
import { explodePromise } from './util/explodePromise';
|
import { explodePromise } from './util/explodePromise';
|
||||||
import { isWindowDragElement } from './util/isWindowDragElement';
|
import { isWindowDragElement } from './util/isWindowDragElement';
|
||||||
import { assert, strictAssert } from './util/assert';
|
import { assert, strictAssert } from './util/assert';
|
||||||
import { dropNull } from './util/dropNull';
|
|
||||||
import { normalizeUuid } from './util/normalizeUuid';
|
import { normalizeUuid } from './util/normalizeUuid';
|
||||||
import { filter } from './util/iterables';
|
import { filter } from './util/iterables';
|
||||||
import { isNotNil } from './util/isNotNil';
|
import { isNotNil } from './util/isNotNil';
|
||||||
|
@ -80,11 +79,11 @@ import type {
|
||||||
SentEventData,
|
SentEventData,
|
||||||
StickerPackEvent,
|
StickerPackEvent,
|
||||||
TypingEvent,
|
TypingEvent,
|
||||||
|
VerifiedEvent,
|
||||||
ViewEvent,
|
ViewEvent,
|
||||||
ViewOnceOpenSyncEvent,
|
ViewOnceOpenSyncEvent,
|
||||||
ViewSyncEvent,
|
ViewSyncEvent,
|
||||||
} from './textsecure/messageReceiverEvents';
|
} from './textsecure/messageReceiverEvents';
|
||||||
import { VerifiedEvent } from './textsecure/messageReceiverEvents';
|
|
||||||
import type { WebAPIType } from './textsecure/WebAPI';
|
import type { WebAPIType } from './textsecure/WebAPI';
|
||||||
import * as KeyChangeListener from './textsecure/KeyChangeListener';
|
import * as KeyChangeListener from './textsecure/KeyChangeListener';
|
||||||
import { RotateSignedPreKeyListener } from './textsecure/RotateSignedPreKeyListener';
|
import { RotateSignedPreKeyListener } from './textsecure/RotateSignedPreKeyListener';
|
||||||
|
@ -1709,12 +1708,6 @@ export async function startApp(): Promise<void> {
|
||||||
window.reduxActions.app.openInstaller();
|
window.reduxActions.app.openInstaller();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.events.on('contactsync', () => {
|
|
||||||
if (window.reduxStore.getState().app.appView === AppViewType.Installer) {
|
|
||||||
window.reduxActions.app.openInbox();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.registerForActive(() => notificationService.clear());
|
window.registerForActive(() => notificationService.clear());
|
||||||
window.addEventListener('unload', () => notificationService.fastClear());
|
window.addEventListener('unload', () => notificationService.fastClear());
|
||||||
|
|
||||||
|
@ -2082,6 +2075,7 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstRun === true && deviceId !== 1) {
|
if (firstRun === true && deviceId !== 1) {
|
||||||
|
const { messaging } = window.textsecure;
|
||||||
const hasThemeSetting = Boolean(window.storage.get('theme-setting'));
|
const hasThemeSetting = Boolean(window.storage.get('theme-setting'));
|
||||||
if (
|
if (
|
||||||
!hasThemeSetting &&
|
!hasThemeSetting &&
|
||||||
|
@ -2093,19 +2087,71 @@ export async function startApp(): Promise<void> {
|
||||||
);
|
);
|
||||||
themeChanged();
|
themeChanged();
|
||||||
}
|
}
|
||||||
const syncRequest = window.getSyncRequest();
|
|
||||||
window.Whisper.events.trigger('contactsync:begin');
|
const waitForEvent = createTaskWithTimeout(
|
||||||
syncRequest.addEventListener('success', () => {
|
(event: string): Promise<void> => {
|
||||||
log.info('sync successful');
|
const { promise, resolve } = explodePromise<void>();
|
||||||
window.storage.put('synced_at', Date.now());
|
window.Whisper.events.once(event, () => resolve());
|
||||||
window.Whisper.events.trigger('contactsync');
|
return promise;
|
||||||
runStorageService();
|
},
|
||||||
});
|
'firstRun:waitForEvent'
|
||||||
syncRequest.addEventListener('timeout', () => {
|
);
|
||||||
log.error('sync timed out');
|
|
||||||
window.Whisper.events.trigger('contactsync');
|
let storageServiceSyncComplete: Promise<void>;
|
||||||
runStorageService();
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
});
|
storageServiceSyncComplete = Promise.resolve();
|
||||||
|
} else {
|
||||||
|
storageServiceSyncComplete = waitForEvent(
|
||||||
|
'storageService:syncComplete'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactSyncComplete = waitForEvent('contactSync:complete');
|
||||||
|
|
||||||
|
log.info('firstRun: requesting initial sync');
|
||||||
|
|
||||||
|
// Request configuration, block, GV1 sync messages, contacts
|
||||||
|
// (only avatars and inboxPosition),and Storage Service sync.
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
singleProtoJobQueue.add(
|
||||||
|
messaging.getRequestConfigurationSyncMessage()
|
||||||
|
),
|
||||||
|
singleProtoJobQueue.add(messaging.getRequestBlockSyncMessage()),
|
||||||
|
singleProtoJobQueue.add(messaging.getRequestGroupSyncMessage()),
|
||||||
|
singleProtoJobQueue.add(messaging.getRequestContactSyncMessage()),
|
||||||
|
runStorageService(),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'connect: Failed to request initial syncs',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('firstRun: waiting for storage service and contact sync');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([storageServiceSyncComplete, contactSyncComplete]);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'connect: Failed to run storage service and contact syncs',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('firstRun: disabling post link experience');
|
||||||
|
window.Signal.Util.postLinkExperience.stop();
|
||||||
|
|
||||||
|
// Switch to inbox view even if contact sync is still running
|
||||||
|
if (
|
||||||
|
window.reduxStore.getState().app.appView === AppViewType.Installer
|
||||||
|
) {
|
||||||
|
log.info('firstRun: opening inbox');
|
||||||
|
window.reduxActions.app.openInbox();
|
||||||
|
} else {
|
||||||
|
log.info('firstRun: not opening inbox');
|
||||||
|
}
|
||||||
|
|
||||||
const installedStickerPacks = Stickers.getInstalledStickerPacks();
|
const installedStickerPacks = Stickers.getInstalledStickerPacks();
|
||||||
if (installedStickerPacks.length) {
|
if (installedStickerPacks.length) {
|
||||||
|
@ -2122,9 +2168,10 @@ export async function startApp(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info('firstRun: requesting stickers', operations.length);
|
||||||
try {
|
try {
|
||||||
await singleProtoJobQueue.add(
|
await singleProtoJobQueue.add(
|
||||||
window.textsecure.messaging.getStickerPackSync(operations)
|
messaging.getStickerPackSync(operations)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -2134,16 +2181,7 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
log.info('firstRun: done');
|
||||||
await singleProtoJobQueue.add(
|
|
||||||
window.textsecure.messaging.getRequestKeySyncMessage()
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'Failed to queue request key sync message',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.storage.onready(async () => {
|
window.storage.onready(async () => {
|
||||||
|
@ -2486,24 +2524,12 @@ export async function startApp(): Promise<void> {
|
||||||
async function onContactSyncComplete() {
|
async function onContactSyncComplete() {
|
||||||
log.info('onContactSyncComplete');
|
log.info('onContactSyncComplete');
|
||||||
await window.storage.put('synced_at', Date.now());
|
await window.storage.put('synced_at', Date.now());
|
||||||
|
window.Whisper.events.trigger('contactSync:complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onContactReceived(ev: ContactEvent) {
|
async function onContactReceived(ev: ContactEvent) {
|
||||||
const details = ev.contactDetails;
|
const details = ev.contactDetails;
|
||||||
|
|
||||||
if (
|
|
||||||
(details.number &&
|
|
||||||
details.number === window.textsecure.storage.user.getNumber()) ||
|
|
||||||
(details.uuid &&
|
|
||||||
details.uuid === window.textsecure.storage.user.getUuid()?.toString())
|
|
||||||
) {
|
|
||||||
// special case for syncing details about ourselves
|
|
||||||
if (details.profileKey) {
|
|
||||||
log.info('Got sync message with our own profile key');
|
|
||||||
ourProfileKeyService.set(details.profileKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const c = new window.Whisper.Conversation({
|
const c = new window.Whisper.Conversation({
|
||||||
e164: details.number,
|
e164: details.number,
|
||||||
uuid: details.uuid,
|
uuid: details.uuid,
|
||||||
|
@ -2528,19 +2554,6 @@ export async function startApp(): Promise<void> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const conversation = window.ConversationController.get(detailsId)!;
|
const conversation = window.ConversationController.get(detailsId)!;
|
||||||
|
|
||||||
if (details.profileKey && details.profileKey.length > 0) {
|
|
||||||
const profileKey = Bytes.toBase64(details.profileKey);
|
|
||||||
conversation.setProfileKey(profileKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof details.blocked !== 'undefined') {
|
|
||||||
if (details.blocked) {
|
|
||||||
conversation.block();
|
|
||||||
} else {
|
|
||||||
conversation.unblock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conversation.set({
|
conversation.set({
|
||||||
name: details.name,
|
name: details.name,
|
||||||
inbox_position: details.inboxPosition,
|
inbox_position: details.inboxPosition,
|
||||||
|
@ -2569,6 +2582,8 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
|
||||||
|
// expireTimer isn't stored in Storage Service so we have to rely on the
|
||||||
|
// contact sync.
|
||||||
const { expireTimer } = details;
|
const { expireTimer } = details;
|
||||||
const isValidExpireTimer = typeof expireTimer === 'number';
|
const isValidExpireTimer = typeof expireTimer === 'number';
|
||||||
if (isValidExpireTimer) {
|
if (isValidExpireTimer) {
|
||||||
|
@ -2585,21 +2600,6 @@ export async function startApp(): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details.verified) {
|
|
||||||
const { verified } = details;
|
|
||||||
const verifiedEvent = new VerifiedEvent(
|
|
||||||
{
|
|
||||||
state: dropNull(verified.state),
|
|
||||||
destination: dropNull(verified.destination),
|
|
||||||
destinationUuid: dropNull(verified.destinationUuid),
|
|
||||||
identityKey: dropNull(verified.identityKey),
|
|
||||||
viaContactSync: true,
|
|
||||||
},
|
|
||||||
noop
|
|
||||||
);
|
|
||||||
await onVerified(verifiedEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.Signal.Util.postLinkExperience.isActive()) {
|
if (window.Signal.Util.postLinkExperience.isActive()) {
|
||||||
log.info(
|
log.info(
|
||||||
'onContactReceived: Adding the message history disclaimer on link'
|
'onContactReceived: Adding the message history disclaimer on link'
|
||||||
|
|
|
@ -1312,7 +1312,6 @@ async function sync(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Signal.Util.postLinkExperience.stop();
|
|
||||||
log.info('storageService.sync: complete');
|
log.info('storageService.sync: complete');
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
|
@ -1453,6 +1452,9 @@ export const runStorageServiceSyncJob = debounce(() => {
|
||||||
ourProfileKeyService.blockGetWithPromise(
|
ourProfileKeyService.blockGetWithPromise(
|
||||||
storageJobQueue(async () => {
|
storageJobQueue(async () => {
|
||||||
await sync();
|
await sync();
|
||||||
|
|
||||||
|
// Notify listeners about sync completion
|
||||||
|
window.Whisper.events.trigger('storageService:syncComplete');
|
||||||
}, `sync v${window.storage.get('manifestVersion')}`)
|
}, `sync v${window.storage.get('manifestVersion')}`)
|
||||||
);
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
waitThenRespondToGroupV2Migration,
|
waitThenRespondToGroupV2Migration,
|
||||||
} from '../groups';
|
} from '../groups';
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
|
import { dropNull } from '../util/dropNull';
|
||||||
import { normalizeUuid } from '../util/normalizeUuid';
|
import { normalizeUuid } from '../util/normalizeUuid';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import {
|
import {
|
||||||
|
@ -768,8 +769,8 @@ export async function mergeContactRecord(
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const e164 = contactRecord.serviceE164 || undefined;
|
const e164 = dropNull(contactRecord.serviceE164);
|
||||||
const uuid = contactRecord.serviceUuid || undefined;
|
const uuid = dropNull(contactRecord.serviceUuid);
|
||||||
|
|
||||||
// All contacts must have UUID
|
// All contacts must have UUID
|
||||||
if (!uuid) {
|
if (!uuid) {
|
||||||
|
@ -802,6 +803,25 @@ export async function mergeContactRecord(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const remoteName = dropNull(contactRecord.givenName);
|
||||||
|
const remoteFamilyName = dropNull(contactRecord.familyName);
|
||||||
|
const localName = conversation.get('profileName');
|
||||||
|
const localFamilyName = conversation.get('profileFamilyName');
|
||||||
|
if (
|
||||||
|
remoteName &&
|
||||||
|
(localName !== remoteName || localFamilyName !== remoteFamilyName)
|
||||||
|
) {
|
||||||
|
// Local name doesn't match remote name, fetch profile
|
||||||
|
if (localName) {
|
||||||
|
conversation.getProfiles();
|
||||||
|
} else {
|
||||||
|
conversation.set({
|
||||||
|
profileName: remoteName,
|
||||||
|
profileFamilyName: remoteFamilyName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const verified = await conversation.safeGetVerified();
|
const verified = await conversation.safeGetVerified();
|
||||||
const storageServiceVerified = contactRecord.identityState || 0;
|
const storageServiceVerified = contactRecord.identityState || 0;
|
||||||
if (verified !== storageServiceVerified) {
|
if (verified !== storageServiceVerified) {
|
||||||
|
@ -1065,6 +1085,16 @@ export async function mergeAccountRecord(
|
||||||
|
|
||||||
remotelyPinnedConversations.forEach(conversation => {
|
remotelyPinnedConversations.forEach(conversation => {
|
||||||
conversation.set({ isPinned: true, isArchived: false });
|
conversation.set({ isPinned: true, isArchived: false });
|
||||||
|
|
||||||
|
if (
|
||||||
|
window.Signal.Util.postLinkExperience.isActive() &&
|
||||||
|
isGroupV2(conversation.attributes)
|
||||||
|
) {
|
||||||
|
log.info(
|
||||||
|
'mergeAccountRecord: Adding the message history disclaimer on link'
|
||||||
|
);
|
||||||
|
conversation.addMessageHistoryDisclaimer();
|
||||||
|
}
|
||||||
updateConversation(conversation.attributes);
|
updateConversation(conversation.attributes);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,8 @@ describe('storage service', function needsName() {
|
||||||
it('should handle message request state changes', async () => {
|
it('should handle message request state changes', async () => {
|
||||||
const { phone, desktop, server } = bootstrap;
|
const { phone, desktop, server } = bootstrap;
|
||||||
|
|
||||||
|
const initialState = await phone.expectStorageState('initial state');
|
||||||
|
|
||||||
debug('Creating stranger');
|
debug('Creating stranger');
|
||||||
const stranger = await server.createPrimaryDevice({
|
const stranger = await server.createPrimaryDevice({
|
||||||
profileName: 'Mysterious Stranger',
|
profileName: 'Mysterious Stranger',
|
||||||
|
@ -52,9 +54,23 @@ describe('storage service', function needsName() {
|
||||||
)
|
)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const initialState = await phone.expectStorageState('initial state');
|
debug("Verify that we stored stranger's profile key");
|
||||||
assert.strictEqual(initialState.version, 1);
|
const postMessageState = await phone.waitForStorageState({
|
||||||
assert.isUndefined(initialState.getContact(stranger));
|
after: initialState,
|
||||||
|
});
|
||||||
|
{
|
||||||
|
assert.strictEqual(postMessageState.version, 2);
|
||||||
|
assert.isFalse(postMessageState.getContact(stranger)?.whitelisted);
|
||||||
|
assert.strictEqual(
|
||||||
|
postMessageState.getContact(stranger)?.profileKey?.length,
|
||||||
|
32
|
||||||
|
);
|
||||||
|
|
||||||
|
// ContactRecord
|
||||||
|
const { added, removed } = postMessageState.diff(initialState);
|
||||||
|
assert.strictEqual(added.length, 1, 'only one record must be added');
|
||||||
|
assert.strictEqual(removed.length, 0, 'no records should be removed');
|
||||||
|
}
|
||||||
|
|
||||||
debug('Accept conversation from a stranger');
|
debug('Accept conversation from a stranger');
|
||||||
await conversationStack
|
await conversationStack
|
||||||
|
@ -64,15 +80,19 @@ describe('storage service', function needsName() {
|
||||||
debug('Verify that storage state was updated');
|
debug('Verify that storage state was updated');
|
||||||
{
|
{
|
||||||
const nextState = await phone.waitForStorageState({
|
const nextState = await phone.waitForStorageState({
|
||||||
after: initialState,
|
after: postMessageState,
|
||||||
});
|
});
|
||||||
assert.strictEqual(nextState.version, 2);
|
assert.strictEqual(nextState.version, 3);
|
||||||
assert.isTrue(nextState.getContact(stranger)?.whitelisted);
|
assert.isTrue(nextState.getContact(stranger)?.whitelisted);
|
||||||
|
|
||||||
// ContactRecord
|
// ContactRecord
|
||||||
const { added, removed } = nextState.diff(initialState);
|
const { added, removed } = nextState.diff(postMessageState);
|
||||||
assert.strictEqual(added.length, 1, 'only one record must be added');
|
assert.strictEqual(added.length, 1, 'only one record must be added');
|
||||||
assert.strictEqual(removed.length, 0, 'no records should be removed');
|
assert.strictEqual(
|
||||||
|
removed.length,
|
||||||
|
1,
|
||||||
|
'only one record should be removed'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stranger should receive our profile key
|
// Stranger should receive our profile key
|
||||||
|
@ -110,6 +130,6 @@ describe('storage service', function needsName() {
|
||||||
|
|
||||||
debug('Verifying the final manifest version');
|
debug('Verifying the final manifest version');
|
||||||
const finalState = await phone.expectStorageState('consistency check');
|
const finalState = await phone.expectStorageState('consistency check');
|
||||||
assert.strictEqual(finalState.version, 2);
|
assert.strictEqual(finalState.version, 3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2610,7 +2610,7 @@ export default class MessageReceiver
|
||||||
envelope: ProcessedEnvelope,
|
envelope: ProcessedEnvelope,
|
||||||
contacts: Proto.SyncMessage.IContacts
|
contacts: Proto.SyncMessage.IContacts
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
log.info('contact sync');
|
log.info('MessageReceiver: handleContacts');
|
||||||
const { blob } = contacts;
|
const { blob } = contacts;
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
throw new Error('MessageReceiver.handleContacts: blob field was missing');
|
throw new Error('MessageReceiver.handleContacts: blob field was missing');
|
||||||
|
@ -2631,11 +2631,11 @@ export default class MessageReceiver
|
||||||
contactDetails = contactBuffer.next();
|
contactDetails = contactBuffer.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalEvent = new ContactSyncEvent();
|
|
||||||
results.push(this.dispatchAndWait(finalEvent));
|
|
||||||
|
|
||||||
await Promise.all(results);
|
await Promise.all(results);
|
||||||
|
|
||||||
|
const finalEvent = new ContactSyncEvent();
|
||||||
|
await this.dispatchAndWait(finalEvent);
|
||||||
|
|
||||||
log.info('handleContacts: finished');
|
log.info('handleContacts: finished');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue