Migrate to private class properties/methods

This commit is contained in:
Jamie Kyle 2025-01-14 11:11:52 -08:00 committed by GitHub
parent 7dbe57084b
commit aa9f53df57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
100 changed files with 3795 additions and 3944 deletions

View file

@ -34,9 +34,9 @@ type EmojiEntryType = Readonly<{
type SheetCacheEntry = Map<string, Uint8Array>;
export class EmojiService {
private readonly emojiMap = new Map<string, EmojiEntryType>();
readonly #emojiMap = new Map<string, EmojiEntryType>();
private readonly sheetCache = new LRUCache<string, SheetCacheEntry>({
readonly #sheetCache = new LRUCache<string, SheetCacheEntry>({
// Each sheet is roughly 500kb
max: 10,
});
@ -52,12 +52,12 @@ export class EmojiService {
return new Response('invalid', { status: 400 });
}
return this.fetch(emoji);
return this.#fetch(emoji);
});
for (const [sheet, emojiList] of Object.entries(manifest)) {
for (const utf16 of emojiList) {
this.emojiMap.set(utf16ToEmoji(utf16), { sheet, utf16 });
this.#emojiMap.set(utf16ToEmoji(utf16), { sheet, utf16 });
}
}
}
@ -71,15 +71,15 @@ export class EmojiService {
return new EmojiService(resourceService, manifest);
}
private async fetch(emoji: string): Promise<Response> {
const entry = this.emojiMap.get(emoji);
async #fetch(emoji: string): Promise<Response> {
const entry = this.#emojiMap.get(emoji);
if (!entry) {
return new Response('entry not found', { status: 404 });
}
const { sheet, utf16 } = entry;
let imageMap = this.sheetCache.get(sheet);
let imageMap = this.#sheetCache.get(sheet);
if (!imageMap) {
const proto = await this.resourceService.getData(
`emoji-sheet-${sheet}.proto`
@ -96,7 +96,7 @@ export class EmojiService {
image || new Uint8Array(0),
])
);
this.sheetCache.set(sheet, imageMap);
this.#sheetCache.set(sheet, imageMap);
}
const image = imageMap.get(utf16);

View file

@ -29,22 +29,22 @@ const RESOURCES_DICT_PATH = join(
const MAX_CACHE_SIZE = 50 * 1024 * 1024;
export class OptionalResourceService {
private maybeDeclaration: OptionalResourcesDictType | undefined;
#maybeDeclaration: OptionalResourcesDictType | undefined;
private readonly cache = new LRUCache<string, Buffer>({
readonly #cache = new LRUCache<string, Buffer>({
maxSize: MAX_CACHE_SIZE,
sizeCalculation: buf => buf.length,
});
private readonly fileQueues = new Map<string, PQueue>();
readonly #fileQueues = new Map<string, PQueue>();
private constructor(private readonly resourcesDir: string) {
ipcMain.handle('OptionalResourceService:getData', (_event, name) =>
this.getData(name)
);
drop(this.lazyInit());
drop(this.#lazyInit());
}
public static create(resourcesDir: string): OptionalResourceService {
@ -52,20 +52,20 @@ export class OptionalResourceService {
}
public async getData(name: string): Promise<Buffer | undefined> {
await this.lazyInit();
await this.#lazyInit();
const decl = this.declaration[name];
const decl = this.#declaration[name];
if (!decl) {
return undefined;
}
const inMemory = this.cache.get(name);
const inMemory = this.#cache.get(name);
if (inMemory) {
return inMemory;
}
const filePath = join(this.resourcesDir, name);
return this.queueFileWork(filePath, async () => {
return this.#queueFileWork(filePath, async () => {
try {
const onDisk = await readFile(filePath);
const digest = createHash('sha512').update(onDisk).digest();
@ -76,7 +76,7 @@ export class OptionalResourceService {
onDisk.length === decl.size
) {
log.warn(`OptionalResourceService: loaded ${name} from disk`);
this.cache.set(name, onDisk);
this.#cache.set(name, onDisk);
return onDisk;
}
@ -94,7 +94,7 @@ export class OptionalResourceService {
// Just do our best effort and move forward
}
return this.fetch(name, decl, filePath);
return this.#fetch(name, decl, filePath);
});
}
@ -102,15 +102,15 @@ export class OptionalResourceService {
// Private
//
private async lazyInit(): Promise<void> {
if (this.maybeDeclaration !== undefined) {
async #lazyInit(): Promise<void> {
if (this.#maybeDeclaration !== undefined) {
return;
}
const json: unknown = JSON.parse(
await readFile(RESOURCES_DICT_PATH, 'utf8')
);
this.maybeDeclaration = parseUnknown(OptionalResourcesDictSchema, json);
this.#maybeDeclaration = parseUnknown(OptionalResourcesDictSchema, json);
// Clean unknown resources
let subPaths: Array<string>;
@ -126,7 +126,7 @@ export class OptionalResourceService {
await Promise.all(
subPaths.map(async subPath => {
if (this.declaration[subPath]) {
if (this.#declaration[subPath]) {
return;
}
@ -144,39 +144,39 @@ export class OptionalResourceService {
);
}
private get declaration(): OptionalResourcesDictType {
if (this.maybeDeclaration === undefined) {
get #declaration(): OptionalResourcesDictType {
if (this.#maybeDeclaration === undefined) {
throw new Error('optional-resources.json not loaded yet');
}
return this.maybeDeclaration;
return this.#maybeDeclaration;
}
private async queueFileWork<R>(
async #queueFileWork<R>(
filePath: string,
body: () => Promise<R>
): Promise<R> {
let queue = this.fileQueues.get(filePath);
let queue = this.#fileQueues.get(filePath);
if (!queue) {
queue = new PQueue({ concurrency: 1 });
this.fileQueues.set(filePath, queue);
this.#fileQueues.set(filePath, queue);
}
try {
return await queue.add(body);
} finally {
if (queue.size === 0) {
this.fileQueues.delete(filePath);
this.#fileQueues.delete(filePath);
}
}
}
private async fetch(
async #fetch(
name: string,
decl: OptionalResourceType,
destPath: string
): Promise<Buffer> {
const result = await got(decl.url, await getGotOptions()).buffer();
this.cache.set(name, result);
this.#cache.set(name, result);
try {
await mkdir(dirname(destPath), { recursive: true });

View file

@ -17,20 +17,20 @@ export class PreventDisplaySleepService {
);
if (isEnabled) {
this.enable();
this.#enable();
} else {
this.disable();
this.#disable();
}
}
private enable(): void {
#enable(): void {
if (this.blockerId !== undefined) {
return;
}
this.blockerId = this.powerSaveBlocker.start('prevent-display-sleep');
}
private disable(): void {
#disable(): void {
if (this.blockerId === undefined) {
return;
}

View file

@ -24,29 +24,20 @@ export type SystemTrayServiceOptionsType = Readonly<{
* [0]: https://www.electronjs.org/docs/api/tray
*/
export class SystemTrayService {
private browserWindow?: BrowserWindow;
private readonly i18n: LocalizerType;
private tray?: Tray;
private isEnabled = false;
private isQuitting = false;
private unreadCount = 0;
private boundRender: typeof SystemTrayService.prototype.render;
private createTrayInstance: (icon: NativeImage) => Tray;
#browserWindow?: BrowserWindow;
readonly #i18n: LocalizerType;
#tray?: Tray;
#isEnabled = false;
#isQuitting = false;
#unreadCount = 0;
#createTrayInstance: (icon: NativeImage) => Tray;
constructor({ i18n, createTrayInstance }: SystemTrayServiceOptionsType) {
log.info('System tray service: created');
this.i18n = i18n;
this.boundRender = this.render.bind(this);
this.createTrayInstance = createTrayInstance || (icon => new Tray(icon));
this.#i18n = i18n;
this.#createTrayInstance = createTrayInstance || (icon => new Tray(icon));
nativeTheme.on('updated', this.boundRender);
nativeTheme.on('updated', this.#render);
}
/**
@ -55,7 +46,7 @@ export class SystemTrayService {
* toggle in the tray's context menu.
*/
setMainWindow(newBrowserWindow: undefined | BrowserWindow): void {
const oldBrowserWindow = this.browserWindow;
const oldBrowserWindow = this.#browserWindow;
if (oldBrowserWindow === newBrowserWindow) {
return;
}
@ -67,18 +58,18 @@ export class SystemTrayService {
);
if (oldBrowserWindow) {
oldBrowserWindow.off('show', this.boundRender);
oldBrowserWindow.off('hide', this.boundRender);
oldBrowserWindow.off('show', this.#render);
oldBrowserWindow.off('hide', this.#render);
}
if (newBrowserWindow) {
newBrowserWindow.on('show', this.boundRender);
newBrowserWindow.on('hide', this.boundRender);
newBrowserWindow.on('show', this.#render);
newBrowserWindow.on('hide', this.#render);
}
this.browserWindow = newBrowserWindow;
this.#browserWindow = newBrowserWindow;
this.render();
this.#render();
}
/**
@ -86,27 +77,27 @@ export class SystemTrayService {
* `setMainWindow`), the tray icon will not be shown, even if enabled.
*/
setEnabled(isEnabled: boolean): void {
if (this.isEnabled === isEnabled) {
if (this.#isEnabled === isEnabled) {
return;
}
log.info(`System tray service: ${isEnabled ? 'enabling' : 'disabling'}`);
this.isEnabled = isEnabled;
this.#isEnabled = isEnabled;
this.render();
this.#render();
}
/**
* Update the unread count, which updates the tray icon if it's visible.
*/
setUnreadCount(unreadCount: number): void {
if (this.unreadCount === unreadCount) {
if (this.#unreadCount === unreadCount) {
return;
}
log.info(`System tray service: setting unread count to ${unreadCount}`);
this.unreadCount = unreadCount;
this.render();
this.#unreadCount = unreadCount;
this.#render();
}
/**
@ -118,35 +109,36 @@ export class SystemTrayService {
markShouldQuit(): void {
log.info('System tray service: markShouldQuit');
this.tray = undefined;
this.isQuitting = true;
this.#tray = undefined;
this.#isQuitting = true;
}
isVisible(): boolean {
return this.tray !== undefined;
return this.#tray !== undefined;
}
private render(): void {
if (this.isEnabled && this.browserWindow) {
this.renderEnabled();
#render = (): void => {
if (this.#isEnabled && this.#browserWindow) {
this.#renderEnabled();
return;
}
this.renderDisabled();
}
this.#renderDisabled();
};
private renderEnabled() {
if (this.isQuitting) {
#renderEnabled() {
if (this.#isQuitting) {
log.info('System tray service: not rendering the tray, quitting');
return;
}
log.info('System tray service: rendering the tray');
this.tray ??= this.createTray();
const { browserWindow, tray } = this;
this.#tray ??= this.#createTray();
const tray = this.#tray;
const browserWindow = this.#browserWindow;
try {
tray.setImage(getIcon(this.unreadCount));
tray.setImage(getIcon(this.#unreadCount));
} catch (err: unknown) {
log.warn(
'System tray service: failed to set preferred image. Falling back...'
@ -164,7 +156,7 @@ export class SystemTrayService {
id: 'toggleWindowVisibility',
...(browserWindow?.isVisible()
? {
label: this.i18n('icu:hide'),
label: this.#i18n('icu:hide'),
click: () => {
log.info(
'System tray service: hiding the window from the context menu'
@ -172,25 +164,25 @@ export class SystemTrayService {
// We re-fetch `this.browserWindow` here just in case the browser window
// has changed while the context menu was open. Same applies in the
// "show" case below.
this.browserWindow?.hide();
this.#browserWindow?.hide();
},
}
: {
label: this.i18n('icu:show'),
label: this.#i18n('icu:show'),
click: () => {
log.info(
'System tray service: showing the window from the context menu'
);
if (this.browserWindow) {
this.browserWindow.show();
focusAndForceToTop(this.browserWindow);
if (this.#browserWindow) {
this.#browserWindow.show();
focusAndForceToTop(this.#browserWindow);
}
},
}),
},
{
id: 'quit',
label: this.i18n('icu:quit'),
label: this.#i18n('icu:quit'),
click: () => {
log.info(
'System tray service: quitting the app from the context menu'
@ -202,21 +194,21 @@ export class SystemTrayService {
);
}
private renderDisabled() {
#renderDisabled() {
log.info('System tray service: rendering no tray');
if (!this.tray) {
if (!this.#tray) {
return;
}
this.tray.destroy();
this.tray = undefined;
this.#tray.destroy();
this.#tray = undefined;
}
private createTray(): Tray {
#createTray(): Tray {
log.info('System tray service: creating the tray');
// This icon may be swiftly overwritten.
const result = this.createTrayInstance(getDefaultIcon());
const result = this.#createTrayInstance(getDefaultIcon());
// Note: "When app indicator is used on Linux, the click event is ignored." This
// doesn't mean that the click event is always ignored on Linux; it depends on how
@ -224,7 +216,7 @@ export class SystemTrayService {
//
// See <https://github.com/electron/electron/blob/v13.1.3/docs/api/tray.md#class-tray>.
result.on('click', () => {
const { browserWindow } = this;
const browserWindow = this.#browserWindow;
if (!browserWindow) {
return;
}
@ -236,7 +228,7 @@ export class SystemTrayService {
}
});
result.setToolTip(this.i18n('icu:signalDesktop'));
result.setToolTip(this.#i18n('icu:signalDesktop'));
return result;
}
@ -246,7 +238,7 @@ export class SystemTrayService {
* into the existing tray instances. It should not be used by "real" code.
*/
_getTray(): undefined | Tray {
return this.tray;
return this.#tray;
}
}

View file

@ -15,9 +15,8 @@ import type { ConfigType } from './base_config';
* process.
*/
export class SystemTraySettingCache {
private cachedValue: undefined | SystemTraySetting;
private getPromise: undefined | Promise<SystemTraySetting>;
#cachedValue: undefined | SystemTraySetting;
#getPromise: undefined | Promise<SystemTraySetting>;
constructor(
private readonly ephemeralConfig: Pick<ConfigType, 'get' | 'set'>,
@ -25,19 +24,19 @@ export class SystemTraySettingCache {
) {}
async get(): Promise<SystemTraySetting> {
if (this.cachedValue !== undefined) {
return this.cachedValue;
if (this.#cachedValue !== undefined) {
return this.#cachedValue;
}
this.getPromise = this.getPromise || this.doFirstGet();
return this.getPromise;
this.#getPromise = this.#getPromise || this.#doFirstGet();
return this.#getPromise;
}
set(value: SystemTraySetting): void {
this.cachedValue = value;
this.#cachedValue = value;
}
private async doFirstGet(): Promise<SystemTraySetting> {
async #doFirstGet(): Promise<SystemTraySetting> {
let result: SystemTraySetting;
// These command line flags are not officially supported, but many users rely on them.
@ -76,15 +75,15 @@ export class SystemTraySettingCache {
);
}
return this.updateCachedValue(result);
return this.#updateCachedValue(result);
}
private updateCachedValue(value: SystemTraySetting): SystemTraySetting {
#updateCachedValue(value: SystemTraySetting): SystemTraySetting {
// If there's a value in the cache, someone has updated the value "out from under us",
// so we should return that because it's newer.
this.cachedValue =
this.cachedValue === undefined ? value : this.cachedValue;
this.#cachedValue =
this.#cachedValue === undefined ? value : this.#cachedValue;
return this.cachedValue;
return this.#cachedValue;
}
}

View file

@ -149,17 +149,14 @@ export function start(): void {
}
export class ConversationController {
private _initialFetchComplete = false;
#_initialFetchComplete = false;
private _initialPromise: undefined | Promise<void>;
private _conversationOpenStart = new Map<string, number>();
private _hasQueueEmptied = false;
private _combineConversationsQueue = new PQueue({ concurrency: 1 });
private _signalConversationId: undefined | string;
#_conversationOpenStart = new Map<string, number>();
#_hasQueueEmptied = false;
#_combineConversationsQueue = new PQueue({ concurrency: 1 });
#_signalConversationId: undefined | string;
constructor(private _conversations: ConversationModelCollectionType) {
const debouncedUpdateUnreadCount = debounce(
@ -192,7 +189,7 @@ export class ConversationController {
}
updateUnreadCount(): void {
if (!this._hasQueueEmptied) {
if (!this.#_hasQueueEmptied) {
return;
}
@ -238,12 +235,12 @@ export class ConversationController {
}
onEmpty(): void {
this._hasQueueEmptied = true;
this.#_hasQueueEmptied = true;
this.updateUnreadCount();
}
get(id?: string | null): ConversationModel | undefined {
if (!this._initialFetchComplete) {
if (!this.#_initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
@ -283,7 +280,7 @@ export class ConversationController {
);
}
if (!this._initialFetchComplete) {
if (!this.#_initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
@ -460,13 +457,13 @@ export class ConversationController {
await updateConversation(conversation.attributes);
}
this._signalConversationId = conversation.id;
this.#_signalConversationId = conversation.id;
return conversation;
}
isSignalConversationId(conversationId: string): boolean {
return this._signalConversationId === conversationId;
return this.#_signalConversationId === conversationId;
}
areWePrimaryDevice(): boolean {
@ -841,14 +838,14 @@ export class ConversationController {
}
checkForConflicts(): Promise<void> {
return this._combineConversationsQueue.add(() =>
this.doCheckForConflicts()
return this.#_combineConversationsQueue.add(() =>
this.#doCheckForConflicts()
);
}
// Note: `doCombineConversations` is directly used within this function since both
// run on `_combineConversationsQueue` queue and we don't want deadlocks.
private async doCheckForConflicts(): Promise<void> {
async #doCheckForConflicts(): Promise<void> {
log.info('ConversationController.checkForConflicts: starting...');
const byServiceId = Object.create(null);
const byE164 = Object.create(null);
@ -884,7 +881,7 @@ export class ConversationController {
if (conversation.get('e164')) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
await this.doCombineConversations({
await this.#doCombineConversations({
current: conversation,
obsolete: existing,
});
@ -892,7 +889,7 @@ export class ConversationController {
} else {
// Keep existing - note that this applies if neither had an e164
// eslint-disable-next-line no-await-in-loop
await this.doCombineConversations({
await this.#doCombineConversations({
current: existing,
obsolete: conversation,
});
@ -918,7 +915,7 @@ export class ConversationController {
if (conversation.get('e164') || conversation.getPni()) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
await this.doCombineConversations({
await this.#doCombineConversations({
current: conversation,
obsolete: existing,
});
@ -926,7 +923,7 @@ export class ConversationController {
} else {
// Keep existing - note that this applies if neither had an e164
// eslint-disable-next-line no-await-in-loop
await this.doCombineConversations({
await this.#doCombineConversations({
current: existing,
obsolete: conversation,
});
@ -964,7 +961,7 @@ export class ConversationController {
if (conversation.getServiceId()) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
await this.doCombineConversations({
await this.#doCombineConversations({
current: conversation,
obsolete: existing,
});
@ -972,7 +969,7 @@ export class ConversationController {
} else {
// Keep existing - note that this applies if neither had a service id
// eslint-disable-next-line no-await-in-loop
await this.doCombineConversations({
await this.#doCombineConversations({
current: existing,
obsolete: conversation,
});
@ -1010,14 +1007,14 @@ export class ConversationController {
!isGroupV2(existing.attributes)
) {
// eslint-disable-next-line no-await-in-loop
await this.doCombineConversations({
await this.#doCombineConversations({
current: conversation,
obsolete: existing,
});
byGroupV2Id[groupV2Id] = conversation;
} else {
// eslint-disable-next-line no-await-in-loop
await this.doCombineConversations({
await this.#doCombineConversations({
current: existing,
obsolete: conversation,
});
@ -1032,12 +1029,12 @@ export class ConversationController {
async combineConversations(
options: CombineConversationsParams
): Promise<void> {
return this._combineConversationsQueue.add(() =>
this.doCombineConversations(options)
return this.#_combineConversationsQueue.add(() =>
this.#doCombineConversations(options)
);
}
private async doCombineConversations({
async #doCombineConversations({
current,
obsolete,
obsoleteTitleInfo,
@ -1304,12 +1301,12 @@ export class ConversationController {
reset(): void {
delete this._initialPromise;
this._initialFetchComplete = false;
this.#_initialFetchComplete = false;
this._conversations.reset([]);
}
load(): Promise<void> {
this._initialPromise ||= this.doLoad();
this._initialPromise ||= this.#doLoad();
return this._initialPromise;
}
@ -1346,16 +1343,16 @@ export class ConversationController {
}
onConvoOpenStart(conversationId: string): void {
this._conversationOpenStart.set(conversationId, Date.now());
this.#_conversationOpenStart.set(conversationId, Date.now());
}
onConvoMessageMount(conversationId: string): void {
const loadStart = this._conversationOpenStart.get(conversationId);
const loadStart = this.#_conversationOpenStart.get(conversationId);
if (loadStart === undefined) {
return;
}
this._conversationOpenStart.delete(conversationId);
this.#_conversationOpenStart.delete(conversationId);
this.get(conversationId)?.onOpenComplete(loadStart);
}
@ -1424,7 +1421,7 @@ export class ConversationController {
}
}
private async doLoad(): Promise<void> {
async #doLoad(): Promise<void> {
log.info('ConversationController: starting initial fetch');
if (this._conversations.length) {
@ -1460,7 +1457,7 @@ export class ConversationController {
// It is alright to call it first because the 'add'/'update' events are
// triggered after updating the collection.
this._initialFetchComplete = true;
this.#_initialFetchComplete = true;
// Hydrate the final set of conversations
batchDispatch(() => {

View file

@ -14,7 +14,7 @@ export class IdleDetector extends EventEmitter {
public start(): void {
log.info('Start idle detector');
this.scheduleNextCallback();
this.#scheduleNextCallback();
}
public stop(): void {
@ -23,10 +23,10 @@ export class IdleDetector extends EventEmitter {
}
log.info('Stop idle detector');
this.clearScheduledCallbacks();
this.#clearScheduledCallbacks();
}
private clearScheduledCallbacks() {
#clearScheduledCallbacks() {
if (this.handle) {
cancelIdleCallback(this.handle);
delete this.handle;
@ -36,14 +36,14 @@ export class IdleDetector extends EventEmitter {
delete this.timeoutId;
}
private scheduleNextCallback() {
this.clearScheduledCallbacks();
#scheduleNextCallback() {
this.#clearScheduledCallbacks();
this.handle = window.requestIdleCallback(deadline => {
const { didTimeout } = deadline;
const timeRemaining = deadline.timeRemaining();
const isIdle = timeRemaining >= IDLE_THRESHOLD_MS;
this.timeoutId = setTimeout(
() => this.scheduleNextCallback(),
() => this.#scheduleNextCallback(),
POLL_INTERVAL_MS
);
if (isIdle || didTimeout) {

View file

@ -51,15 +51,14 @@ export type SessionsOptions = Readonly<{
}>;
export class Sessions extends SessionStore {
private readonly ourServiceId: ServiceIdString;
private readonly zone: Zone | undefined;
readonly #ourServiceId: ServiceIdString;
readonly #zone: Zone | undefined;
constructor({ ourServiceId, zone }: SessionsOptions) {
super();
this.ourServiceId = ourServiceId;
this.zone = zone;
this.#ourServiceId = ourServiceId;
this.#zone = zone;
}
async saveSession(
@ -67,17 +66,17 @@ export class Sessions extends SessionStore {
record: SessionRecord
): Promise<void> {
await window.textsecure.storage.protocol.storeSession(
toQualifiedAddress(this.ourServiceId, address),
toQualifiedAddress(this.#ourServiceId, address),
record,
{ zone: this.zone }
{ zone: this.#zone }
);
}
async getSession(name: ProtocolAddress): Promise<SessionRecord | null> {
const encodedAddress = toQualifiedAddress(this.ourServiceId, name);
const encodedAddress = toQualifiedAddress(this.#ourServiceId, name);
const record = await window.textsecure.storage.protocol.loadSession(
encodedAddress,
{ zone: this.zone }
{ zone: this.#zone }
);
return record || null;
@ -87,10 +86,10 @@ export class Sessions extends SessionStore {
addresses: Array<ProtocolAddress>
): Promise<Array<SessionRecord>> {
const encodedAddresses = addresses.map(addr =>
toQualifiedAddress(this.ourServiceId, addr)
toQualifiedAddress(this.#ourServiceId, addr)
);
return window.textsecure.storage.protocol.loadSessions(encodedAddresses, {
zone: this.zone,
zone: this.#zone,
});
}
}
@ -101,20 +100,19 @@ export type IdentityKeysOptions = Readonly<{
}>;
export class IdentityKeys extends IdentityKeyStore {
private readonly ourServiceId: ServiceIdString;
private readonly zone: Zone | undefined;
readonly #ourServiceId: ServiceIdString;
readonly #zone: Zone | undefined;
constructor({ ourServiceId, zone }: IdentityKeysOptions) {
super();
this.ourServiceId = ourServiceId;
this.zone = zone;
this.#ourServiceId = ourServiceId;
this.#zone = zone;
}
async getIdentityKey(): Promise<PrivateKey> {
const keyPair = window.textsecure.storage.protocol.getIdentityKeyPair(
this.ourServiceId
this.#ourServiceId
);
if (!keyPair) {
throw new Error('IdentityKeyStore/getIdentityKey: No identity key!');
@ -124,7 +122,7 @@ export class IdentityKeys extends IdentityKeyStore {
async getLocalRegistrationId(): Promise<number> {
const id = await window.textsecure.storage.protocol.getLocalRegistrationId(
this.ourServiceId
this.#ourServiceId
);
if (!isNumber(id)) {
throw new Error(
@ -157,7 +155,7 @@ export class IdentityKeys extends IdentityKeyStore {
encodedAddress,
publicKey,
false,
{ zone: this.zone }
{ zone: this.#zone }
);
}
@ -182,11 +180,11 @@ export type PreKeysOptions = Readonly<{
}>;
export class PreKeys extends PreKeyStore {
private readonly ourServiceId: ServiceIdString;
readonly #ourServiceId: ServiceIdString;
constructor({ ourServiceId }: PreKeysOptions) {
super();
this.ourServiceId = ourServiceId;
this.#ourServiceId = ourServiceId;
}
async savePreKey(): Promise<void> {
@ -195,7 +193,7 @@ export class PreKeys extends PreKeyStore {
async getPreKey(id: number): Promise<PreKeyRecord> {
const preKey = await window.textsecure.storage.protocol.loadPreKey(
this.ourServiceId,
this.#ourServiceId,
id
);
@ -207,18 +205,18 @@ export class PreKeys extends PreKeyStore {
}
async removePreKey(id: number): Promise<void> {
await window.textsecure.storage.protocol.removePreKeys(this.ourServiceId, [
await window.textsecure.storage.protocol.removePreKeys(this.#ourServiceId, [
id,
]);
}
}
export class KyberPreKeys extends KyberPreKeyStore {
private readonly ourServiceId: ServiceIdString;
readonly #ourServiceId: ServiceIdString;
constructor({ ourServiceId }: PreKeysOptions) {
super();
this.ourServiceId = ourServiceId;
this.#ourServiceId = ourServiceId;
}
async saveKyberPreKey(): Promise<void> {
@ -228,7 +226,7 @@ export class KyberPreKeys extends KyberPreKeyStore {
async getKyberPreKey(id: number): Promise<KyberPreKeyRecord> {
const kyberPreKey =
await window.textsecure.storage.protocol.loadKyberPreKey(
this.ourServiceId,
this.#ourServiceId,
id
);
@ -241,7 +239,7 @@ export class KyberPreKeys extends KyberPreKeyStore {
async markKyberPreKeyUsed(id: number): Promise<void> {
await window.textsecure.storage.protocol.maybeRemoveKyberPreKey(
this.ourServiceId,
this.#ourServiceId,
id
);
}
@ -253,13 +251,13 @@ export type SenderKeysOptions = Readonly<{
}>;
export class SenderKeys extends SenderKeyStore {
private readonly ourServiceId: ServiceIdString;
readonly #ourServiceId: ServiceIdString;
readonly zone: Zone | undefined;
constructor({ ourServiceId, zone }: SenderKeysOptions) {
super();
this.ourServiceId = ourServiceId;
this.#ourServiceId = ourServiceId;
this.zone = zone;
}
@ -268,7 +266,7 @@ export class SenderKeys extends SenderKeyStore {
distributionId: Uuid,
record: SenderKeyRecord
): Promise<void> {
const encodedAddress = toQualifiedAddress(this.ourServiceId, sender);
const encodedAddress = toQualifiedAddress(this.#ourServiceId, sender);
await window.textsecure.storage.protocol.saveSenderKey(
encodedAddress,
@ -282,7 +280,7 @@ export class SenderKeys extends SenderKeyStore {
sender: ProtocolAddress,
distributionId: Uuid
): Promise<SenderKeyRecord | null> {
const encodedAddress = toQualifiedAddress(this.ourServiceId, sender);
const encodedAddress = toQualifiedAddress(this.#ourServiceId, sender);
const senderKey = await window.textsecure.storage.protocol.getSenderKey(
encodedAddress,
@ -299,11 +297,11 @@ export type SignedPreKeysOptions = Readonly<{
}>;
export class SignedPreKeys extends SignedPreKeyStore {
private readonly ourServiceId: ServiceIdString;
readonly #ourServiceId: ServiceIdString;
constructor({ ourServiceId }: SignedPreKeysOptions) {
super();
this.ourServiceId = ourServiceId;
this.#ourServiceId = ourServiceId;
}
async saveSignedPreKey(): Promise<void> {
@ -313,7 +311,7 @@ export class SignedPreKeys extends SignedPreKeyStore {
async getSignedPreKey(id: number): Promise<SignedPreKeyRecord> {
const signedPreKey =
await window.textsecure.storage.protocol.loadSignedPreKey(
this.ourServiceId,
this.#ourServiceId,
id
);

View file

@ -240,11 +240,10 @@ export class SignalProtocolStore extends EventEmitter {
// Cached values
private ourIdentityKeys = new Map<ServiceIdString, KeyPairType>();
#ourIdentityKeys = new Map<ServiceIdString, KeyPairType>();
private ourRegistrationIds = new Map<ServiceIdString, number>();
private cachedPniSignatureMessage: PniSignatureMessageType | undefined;
#ourRegistrationIds = new Map<ServiceIdString, number>();
#cachedPniSignatureMessage: PniSignatureMessageType | undefined;
identityKeys?: Map<
IdentityKeyIdType,
@ -273,24 +272,18 @@ export class SignalProtocolStore extends EventEmitter {
sessionQueueJobCounter = 0;
private readonly identityQueues = new Map<ServiceIdString, PQueue>();
private currentZone?: Zone;
private currentZoneDepth = 0;
private readonly zoneQueue: Array<ZoneQueueEntryType> = [];
private pendingSessions = new Map<SessionIdType, SessionCacheEntry>();
private pendingSenderKeys = new Map<SenderKeyIdType, SenderKeyCacheEntry>();
private pendingUnprocessed = new Map<string, UnprocessedType>();
readonly #identityQueues = new Map<ServiceIdString, PQueue>();
#currentZone?: Zone;
#currentZoneDepth = 0;
readonly #zoneQueue: Array<ZoneQueueEntryType> = [];
#pendingSessions = new Map<SessionIdType, SessionCacheEntry>();
#pendingSenderKeys = new Map<SenderKeyIdType, SenderKeyCacheEntry>();
#pendingUnprocessed = new Map<string, UnprocessedType>();
async hydrateCaches(): Promise<void> {
await Promise.all([
(async () => {
this.ourIdentityKeys.clear();
this.#ourIdentityKeys.clear();
const map = (await DataReader.getItemById(
'identityKeyMap'
)) as unknown as ItemType<'identityKeyMap'>;
@ -304,14 +297,14 @@ export class SignalProtocolStore extends EventEmitter {
'Invalid identity key serviceId'
);
const { privKey, pubKey } = map.value[serviceId];
this.ourIdentityKeys.set(serviceId, {
this.#ourIdentityKeys.set(serviceId, {
privKey,
pubKey,
});
}
})(),
(async () => {
this.ourRegistrationIds.clear();
this.#ourRegistrationIds.clear();
const map = (await DataReader.getItemById(
'registrationIdMap'
)) as unknown as ItemType<'registrationIdMap'>;
@ -324,7 +317,7 @@ export class SignalProtocolStore extends EventEmitter {
isServiceIdString(serviceId),
'Invalid registration id serviceId'
);
this.ourRegistrationIds.set(serviceId, map.value[serviceId]);
this.#ourRegistrationIds.set(serviceId, map.value[serviceId]);
}
})(),
_fillCaches<string, IdentityKeyType, PublicKey>(
@ -361,25 +354,22 @@ export class SignalProtocolStore extends EventEmitter {
}
getIdentityKeyPair(ourServiceId: ServiceIdString): KeyPairType | undefined {
return this.ourIdentityKeys.get(ourServiceId);
return this.#ourIdentityKeys.get(ourServiceId);
}
async getLocalRegistrationId(
ourServiceId: ServiceIdString
): Promise<number | undefined> {
return this.ourRegistrationIds.get(ourServiceId);
return this.#ourRegistrationIds.get(ourServiceId);
}
private _getKeyId(
ourServiceId: ServiceIdString,
keyId: number
): PreKeyIdType {
#_getKeyId(ourServiceId: ServiceIdString, keyId: number): PreKeyIdType {
return `${ourServiceId}:${keyId}`;
}
// KyberPreKeys
private _getKyberPreKeyEntry(
#_getKyberPreKeyEntry(
id: PreKeyIdType,
logContext: string
):
@ -420,8 +410,8 @@ export class SignalProtocolStore extends EventEmitter {
ourServiceId: ServiceIdString,
keyId: number
): Promise<KyberPreKeyRecord | undefined> {
const id: PreKeyIdType = this._getKeyId(ourServiceId, keyId);
const entry = this._getKyberPreKeyEntry(id, 'loadKyberPreKey');
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
const entry = this.#_getKyberPreKeyEntry(id, 'loadKyberPreKey');
return entry?.item;
}
@ -457,7 +447,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('storeKyberPreKey: this.kyberPreKeys not yet cached!');
}
const id: PreKeyIdType = this._getKeyId(ourServiceId, keyId);
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
const item = kyberPreKeyCache.get(id);
if (!item) {
throw new Error(`confirmKyberPreKey: missing kyber prekey ${id}!`);
@ -487,7 +477,7 @@ export class SignalProtocolStore extends EventEmitter {
const toSave: Array<KyberPreKeyType> = [];
keys.forEach(key => {
const id: PreKeyIdType = this._getKeyId(ourServiceId, key.keyId);
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, key.keyId);
if (kyberPreKeyCache.has(id)) {
throw new Error(`storeKyberPreKey: kyber prekey ${id} already exists!`);
}
@ -519,8 +509,8 @@ export class SignalProtocolStore extends EventEmitter {
ourServiceId: ServiceIdString,
keyId: number
): Promise<void> {
const id: PreKeyIdType = this._getKeyId(ourServiceId, keyId);
const entry = this._getKyberPreKeyEntry(id, 'maybeRemoveKyberPreKey');
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
const entry = this.#_getKyberPreKeyEntry(id, 'maybeRemoveKyberPreKey');
if (!entry) {
return;
@ -544,7 +534,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('removeKyberPreKeys: this.kyberPreKeys not yet cached!');
}
const ids = keyIds.map(keyId => this._getKeyId(ourServiceId, keyId));
const ids = keyIds.map(keyId => this.#_getKeyId(ourServiceId, keyId));
log.info('removeKyberPreKeys: Removing kyber prekeys:', formatKeys(keyIds));
const changes = await DataWriter.removeKyberPreKeyById(ids);
@ -554,7 +544,7 @@ export class SignalProtocolStore extends EventEmitter {
});
if (kyberPreKeyCache.size < LOW_KEYS_THRESHOLD) {
this.emitLowKeys(
this.#emitLowKeys(
ourServiceId,
`removeKyberPreKeys@${kyberPreKeyCache.size}`
);
@ -579,7 +569,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('loadPreKey: this.preKeys not yet cached!');
}
const id: PreKeyIdType = this._getKeyId(ourServiceId, keyId);
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
const entry = this.preKeys.get(id);
if (!entry) {
log.error('Failed to fetch prekey:', id);
@ -628,7 +618,7 @@ export class SignalProtocolStore extends EventEmitter {
const now = Date.now();
const toSave: Array<PreKeyType> = [];
keys.forEach(key => {
const id: PreKeyIdType = this._getKeyId(ourServiceId, key.keyId);
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, key.keyId);
if (preKeyCache.has(id)) {
throw new Error(`storePreKeys: prekey ${id} already exists!`);
@ -665,7 +655,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('removePreKeys: this.preKeys not yet cached!');
}
const ids = keyIds.map(keyId => this._getKeyId(ourServiceId, keyId));
const ids = keyIds.map(keyId => this.#_getKeyId(ourServiceId, keyId));
log.info('removePreKeys: Removing prekeys:', formatKeys(keyIds));
@ -676,7 +666,7 @@ export class SignalProtocolStore extends EventEmitter {
});
if (preKeyCache.size < LOW_KEYS_THRESHOLD) {
this.emitLowKeys(ourServiceId, `removePreKeys@${preKeyCache.size}`);
this.#emitLowKeys(ourServiceId, `removePreKeys@${preKeyCache.size}`);
}
}
@ -756,7 +746,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('storeKyberPreKey: this.signedPreKeys not yet cached!');
}
const id: PreKeyIdType = this._getKeyId(ourServiceId, keyId);
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
const item = signedPreKeyCache.get(id);
if (!item) {
throw new Error(`confirmSignedPreKey: missing prekey ${id}!`);
@ -785,7 +775,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!');
}
const id: SignedPreKeyIdType = this._getKeyId(ourServiceId, keyId);
const id: SignedPreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
const fromDB = {
id,
@ -813,7 +803,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!');
}
const ids = keyIds.map(keyId => this._getKeyId(ourServiceId, keyId));
const ids = keyIds.map(keyId => this.#_getKeyId(ourServiceId, keyId));
log.info(
'removeSignedPreKeys: Removing signed prekeys:',
@ -855,13 +845,13 @@ export class SignalProtocolStore extends EventEmitter {
zone = GLOBAL_ZONE
): Promise<T> {
return this.withZone(zone, 'enqueueSenderKeyJob', async () => {
const queue = this._getSenderKeyQueue(qualifiedAddress);
const queue = this.#_getSenderKeyQueue(qualifiedAddress);
return queue.add<T>(task);
});
}
private _createSenderKeyQueue(): PQueue {
#_createSenderKeyQueue(): PQueue {
return new PQueue({
concurrency: 1,
timeout: MINUTE * 30,
@ -869,18 +859,18 @@ export class SignalProtocolStore extends EventEmitter {
});
}
private _getSenderKeyQueue(senderId: QualifiedAddress): PQueue {
#_getSenderKeyQueue(senderId: QualifiedAddress): PQueue {
const cachedQueue = this.senderKeyQueues.get(senderId.toString());
if (cachedQueue) {
return cachedQueue;
}
const freshQueue = this._createSenderKeyQueue();
const freshQueue = this.#_createSenderKeyQueue();
this.senderKeyQueues.set(senderId.toString(), freshQueue);
return freshQueue;
}
private getSenderKeyId(
#getSenderKeyId(
senderKeyId: QualifiedAddress,
distributionId: string
): SenderKeyIdType {
@ -901,7 +891,7 @@ export class SignalProtocolStore extends EventEmitter {
const senderId = qualifiedAddress.toString();
try {
const id = this.getSenderKeyId(qualifiedAddress, distributionId);
const id = this.#getSenderKeyId(qualifiedAddress, distributionId);
const fromDB: SenderKeyType = {
id,
@ -911,7 +901,7 @@ export class SignalProtocolStore extends EventEmitter {
lastUpdatedDate: Date.now(),
};
this.pendingSenderKeys.set(id, {
this.#pendingSenderKeys.set(id, {
hydrated: true,
fromDB,
item: record,
@ -919,7 +909,7 @@ export class SignalProtocolStore extends EventEmitter {
// Current zone doesn't support pending sessions - commit immediately
if (!zone.supportsPendingSenderKeys()) {
await this.commitZoneChanges('saveSenderKey');
await this.#commitZoneChanges('saveSenderKey');
}
} catch (error) {
const errorString = Errors.toLogFormat(error);
@ -943,10 +933,10 @@ export class SignalProtocolStore extends EventEmitter {
const senderId = qualifiedAddress.toString();
try {
const id = this.getSenderKeyId(qualifiedAddress, distributionId);
const id = this.#getSenderKeyId(qualifiedAddress, distributionId);
const map = this.pendingSenderKeys.has(id)
? this.pendingSenderKeys
const map = this.#pendingSenderKeys.has(id)
? this.#pendingSenderKeys
: this.senderKeys;
const entry = map.get(id);
@ -991,7 +981,7 @@ export class SignalProtocolStore extends EventEmitter {
const senderId = qualifiedAddress.toString();
try {
const id = this.getSenderKeyId(qualifiedAddress, distributionId);
const id = this.#getSenderKeyId(qualifiedAddress, distributionId);
await DataWriter.removeSenderKeyById(id);
@ -1009,8 +999,8 @@ export class SignalProtocolStore extends EventEmitter {
if (this.senderKeys) {
this.senderKeys.clear();
}
if (this.pendingSenderKeys) {
this.pendingSenderKeys.clear();
if (this.#pendingSenderKeys) {
this.#pendingSenderKeys.clear();
}
await DataWriter.removeAllSenderKeys();
});
@ -1030,7 +1020,7 @@ export class SignalProtocolStore extends EventEmitter {
const waitStart = Date.now();
return this.withZone(zone, 'enqueueSessionJob', async () => {
const queue = this._getSessionQueue(qualifiedAddress);
const queue = this.#_getSessionQueue(qualifiedAddress);
const waitTime = Date.now() - waitStart;
log.info(
@ -1048,7 +1038,7 @@ export class SignalProtocolStore extends EventEmitter {
});
}
private _createSessionQueue(): PQueue {
#_createSessionQueue(): PQueue {
return new PQueue({
concurrency: 1,
timeout: MINUTE * 30,
@ -1056,20 +1046,20 @@ export class SignalProtocolStore extends EventEmitter {
});
}
private _getSessionQueue(id: QualifiedAddress): PQueue {
#_getSessionQueue(id: QualifiedAddress): PQueue {
const cachedQueue = this.sessionQueues.get(id.toString());
if (cachedQueue) {
return cachedQueue;
}
const freshQueue = this._createSessionQueue();
const freshQueue = this.#_createSessionQueue();
this.sessionQueues.set(id.toString(), freshQueue);
return freshQueue;
}
// Identity Queue
private _createIdentityQueue(): PQueue {
#_createIdentityQueue(): PQueue {
return new PQueue({
concurrency: 1,
timeout: MINUTE * 30,
@ -1077,7 +1067,7 @@ export class SignalProtocolStore extends EventEmitter {
});
}
private _runOnIdentityQueue<T>(
#_runOnIdentityQueue<T>(
serviceId: ServiceIdString,
zone: Zone,
name: string,
@ -1085,12 +1075,12 @@ export class SignalProtocolStore extends EventEmitter {
): Promise<T> {
let queue: PQueue;
const cachedQueue = this.identityQueues.get(serviceId);
const cachedQueue = this.#identityQueues.get(serviceId);
if (cachedQueue) {
queue = cachedQueue;
} else {
queue = this._createIdentityQueue();
this.identityQueues.set(serviceId, queue);
queue = this.#_createIdentityQueue();
this.#identityQueues.set(serviceId, queue);
}
// We run the identity queue task in zone because `saveIdentity` needs to
@ -1124,10 +1114,10 @@ export class SignalProtocolStore extends EventEmitter {
const debugName = `withZone(${zone.name}:${name})`;
// Allow re-entering from LibSignalStores
if (this.currentZone && this.currentZone !== zone) {
if (this.#currentZone && this.#currentZone !== zone) {
const start = Date.now();
log.info(`${debugName}: locked by ${this.currentZone.name}, waiting`);
log.info(`${debugName}: locked by ${this.#currentZone.name}, waiting`);
return new Promise<T>((resolve, reject) => {
const callback = async () => {
@ -1143,33 +1133,35 @@ export class SignalProtocolStore extends EventEmitter {
}
};
this.zoneQueue.push({ zone, callback });
this.#zoneQueue.push({ zone, callback });
});
}
this.enterZone(zone, name);
this.#enterZone(zone, name);
let result: T;
try {
result = await body();
} catch (error) {
if (this.isInTopLevelZone()) {
await this.revertZoneChanges(name, error);
if (this.#isInTopLevelZone()) {
await this.#revertZoneChanges(name, error);
}
this.leaveZone(zone);
this.#leaveZone(zone);
throw error;
}
if (this.isInTopLevelZone()) {
await this.commitZoneChanges(name);
if (this.#isInTopLevelZone()) {
await this.#commitZoneChanges(name);
}
this.leaveZone(zone);
this.#leaveZone(zone);
return result;
}
private async commitZoneChanges(name: string): Promise<void> {
const { pendingSenderKeys, pendingSessions, pendingUnprocessed } = this;
async #commitZoneChanges(name: string): Promise<void> {
const pendingUnprocessed = this.#pendingUnprocessed;
const pendingSenderKeys = this.#pendingSenderKeys;
const pendingSessions = this.#pendingSessions;
if (
pendingSenderKeys.size === 0 &&
@ -1186,9 +1178,9 @@ export class SignalProtocolStore extends EventEmitter {
`pending unprocessed ${pendingUnprocessed.size}`
);
this.pendingSenderKeys = new Map();
this.pendingSessions = new Map();
this.pendingUnprocessed = new Map();
this.#pendingSenderKeys = new Map();
this.#pendingSessions = new Map();
this.#pendingUnprocessed = new Map();
// Commit both sender keys, sessions and unprocessed in the same database transaction
// to unroll both on error.
@ -1223,28 +1215,28 @@ export class SignalProtocolStore extends EventEmitter {
});
}
private async revertZoneChanges(name: string, error: Error): Promise<void> {
async #revertZoneChanges(name: string, error: Error): Promise<void> {
log.info(
`revertZoneChanges(${name}): ` +
`pending sender keys size ${this.pendingSenderKeys.size}, ` +
`pending sessions size ${this.pendingSessions.size}, ` +
`pending unprocessed size ${this.pendingUnprocessed.size}`,
`pending sender keys size ${this.#pendingSenderKeys.size}, ` +
`pending sessions size ${this.#pendingSessions.size}, ` +
`pending unprocessed size ${this.#pendingUnprocessed.size}`,
Errors.toLogFormat(error)
);
this.pendingSenderKeys.clear();
this.pendingSessions.clear();
this.pendingUnprocessed.clear();
this.#pendingSenderKeys.clear();
this.#pendingSessions.clear();
this.#pendingUnprocessed.clear();
}
private isInTopLevelZone(): boolean {
return this.currentZoneDepth === 1;
#isInTopLevelZone(): boolean {
return this.#currentZoneDepth === 1;
}
private enterZone(zone: Zone, name: string): void {
this.currentZoneDepth += 1;
if (this.currentZoneDepth === 1) {
assertDev(this.currentZone === undefined, 'Should not be in the zone');
this.currentZone = zone;
#enterZone(zone: Zone, name: string): void {
this.#currentZoneDepth += 1;
if (this.#currentZoneDepth === 1) {
assertDev(this.#currentZone === undefined, 'Should not be in the zone');
this.#currentZone = zone;
if (zone !== GLOBAL_ZONE) {
log.info(`SignalProtocolStore.enterZone(${zone.name}:${name})`);
@ -1252,19 +1244,19 @@ export class SignalProtocolStore extends EventEmitter {
}
}
private leaveZone(zone: Zone): void {
assertDev(this.currentZone === zone, 'Should be in the correct zone');
#leaveZone(zone: Zone): void {
assertDev(this.#currentZone === zone, 'Should be in the correct zone');
this.currentZoneDepth -= 1;
this.#currentZoneDepth -= 1;
assertDev(
this.currentZoneDepth >= 0,
this.#currentZoneDepth >= 0,
'Unmatched number of leaveZone calls'
);
// Since we allow re-entering zones we might actually be in two overlapping
// async calls. Leave the zone and yield to another one only if there are
// no active zone users anymore.
if (this.currentZoneDepth !== 0) {
if (this.#currentZoneDepth !== 0) {
return;
}
@ -1272,17 +1264,17 @@ export class SignalProtocolStore extends EventEmitter {
log.info(`SignalProtocolStore.leaveZone(${zone.name})`);
}
this.currentZone = undefined;
this.#currentZone = undefined;
const next = this.zoneQueue.shift();
const next = this.#zoneQueue.shift();
if (!next) {
return;
}
const toEnter = [next];
while (this.zoneQueue[0]?.zone === next.zone) {
const elem = this.zoneQueue.shift();
while (this.#zoneQueue[0]?.zone === next.zone) {
const elem = this.#zoneQueue.shift();
assertDev(elem, 'Zone element should be present');
toEnter.push(elem);
@ -1313,8 +1305,8 @@ export class SignalProtocolStore extends EventEmitter {
const id = qualifiedAddress.toString();
try {
const map = this.pendingSessions.has(id)
? this.pendingSessions
const map = this.#pendingSessions.has(id)
? this.#pendingSessions
: this.sessions;
const entry = map.get(id);
@ -1399,13 +1391,13 @@ export class SignalProtocolStore extends EventEmitter {
item: record,
};
assertDev(this.currentZone, 'Must run in the zone');
assertDev(this.#currentZone, 'Must run in the zone');
this.pendingSessions.set(id, newSession);
this.#pendingSessions.set(id, newSession);
// Current zone doesn't support pending sessions - commit immediately
if (!zone.supportsPendingSessions()) {
await this.commitZoneChanges('storeSession');
await this.#commitZoneChanges('storeSession');
}
} catch (error) {
const errorString = Errors.toLogFormat(error);
@ -1421,7 +1413,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('getOpenDevices: this.sessions not yet cached!');
}
return this._getAllSessions().some(
return this.#_getAllSessions().some(
({ fromDB }) => fromDB.serviceId === serviceId
);
});
@ -1446,7 +1438,7 @@ export class SignalProtocolStore extends EventEmitter {
try {
const serviceIdSet = new Set(serviceIds);
const allSessions = this._getAllSessions();
const allSessions = this.#_getAllSessions();
const entries = allSessions.filter(
({ fromDB }) =>
fromDB.ourServiceId === ourServiceId &&
@ -1537,7 +1529,7 @@ export class SignalProtocolStore extends EventEmitter {
try {
await DataWriter.removeSessionById(id);
this.sessions.delete(id);
this.pendingSessions.delete(id);
this.#pendingSessions.delete(id);
} catch (e) {
log.error(`removeSession: Failed to delete session for ${id}`);
}
@ -1578,7 +1570,7 @@ export class SignalProtocolStore extends EventEmitter {
const entry = entries[i];
if (entry.fromDB.conversationId === id) {
this.sessions.delete(entry.fromDB.id);
this.pendingSessions.delete(entry.fromDB.id);
this.#pendingSessions.delete(entry.fromDB.id);
}
}
@ -1603,7 +1595,7 @@ export class SignalProtocolStore extends EventEmitter {
const entry = entries[i];
if (entry.fromDB.serviceId === serviceId) {
this.sessions.delete(entry.fromDB.id);
this.pendingSessions.delete(entry.fromDB.id);
this.#pendingSessions.delete(entry.fromDB.id);
}
}
@ -1611,7 +1603,7 @@ export class SignalProtocolStore extends EventEmitter {
});
}
private async _archiveSession(entry?: SessionCacheEntry, zone?: Zone) {
async #_archiveSession(entry?: SessionCacheEntry, zone?: Zone) {
if (!entry) {
return;
}
@ -1646,9 +1638,9 @@ export class SignalProtocolStore extends EventEmitter {
log.info(`archiveSession: session for ${id}`);
const entry = this.pendingSessions.get(id) || this.sessions.get(id);
const entry = this.#pendingSessions.get(id) || this.sessions.get(id);
await this._archiveSession(entry);
await this.#_archiveSession(entry);
});
}
@ -1670,7 +1662,7 @@ export class SignalProtocolStore extends EventEmitter {
const { serviceId, deviceId } = encodedAddress;
const allEntries = this._getAllSessions();
const allEntries = this.#_getAllSessions();
const entries = allEntries.filter(
entry =>
entry.fromDB.serviceId === serviceId &&
@ -1679,7 +1671,7 @@ export class SignalProtocolStore extends EventEmitter {
await Promise.all(
entries.map(async entry => {
await this._archiveSession(entry, zone);
await this.#_archiveSession(entry, zone);
})
);
});
@ -1693,14 +1685,14 @@ export class SignalProtocolStore extends EventEmitter {
log.info('archiveAllSessions: archiving all sessions for', serviceId);
const allEntries = this._getAllSessions();
const allEntries = this.#_getAllSessions();
const entries = allEntries.filter(
entry => entry.fromDB.serviceId === serviceId
);
await Promise.all(
entries.map(async entry => {
await this._archiveSession(entry);
await this.#_archiveSession(entry);
})
);
});
@ -1711,7 +1703,7 @@ export class SignalProtocolStore extends EventEmitter {
if (this.sessions) {
this.sessions.clear();
}
this.pendingSessions.clear();
this.#pendingSessions.clear();
const changes = await DataWriter.removeAllSessions();
log.info(`clearSessionStore: Removed ${changes} sessions`);
});
@ -1830,7 +1822,7 @@ export class SignalProtocolStore extends EventEmitter {
`to ${newRecord.id}`
);
await this._saveIdentityKey(newRecord);
await this.#_saveIdentityKey(newRecord);
this.identityKeys.delete(record.fromDB.id);
const changes = await DataWriter.removeIdentityKeyById(record.fromDB.id);
@ -1931,7 +1923,7 @@ export class SignalProtocolStore extends EventEmitter {
log.error('isTrustedForSending: Needs unverified approval!');
return false;
}
if (this.isNonBlockingApprovalRequired(identityRecord)) {
if (this.#isNonBlockingApprovalRequired(identityRecord)) {
log.error('isTrustedForSending: Needs non-blocking approval!');
return false;
}
@ -1973,7 +1965,7 @@ export class SignalProtocolStore extends EventEmitter {
return Bytes.toBase64(fingerprint);
}
private async _saveIdentityKey(data: IdentityKeyType): Promise<void> {
async #_saveIdentityKey(data: IdentityKeyType): Promise<void> {
if (!this.identityKeys) {
throw new Error('_saveIdentityKey: this.identityKeys not yet cached!');
}
@ -2010,7 +2002,7 @@ export class SignalProtocolStore extends EventEmitter {
nonblockingApproval = false;
}
return this._runOnIdentityQueue(
return this.#_runOnIdentityQueue(
encodedAddress.serviceId,
zone,
'saveIdentity',
@ -2025,7 +2017,7 @@ export class SignalProtocolStore extends EventEmitter {
if (!identityRecord || !identityRecord.publicKey) {
// Lookup failed, or the current key was removed, so save this one.
log.info(`${logId}: Saving new identity...`);
await this._saveIdentityKey({
await this.#_saveIdentityKey({
id,
publicKey,
firstUse: true,
@ -2074,7 +2066,7 @@ export class SignalProtocolStore extends EventEmitter {
verifiedStatus = VerifiedStatus.DEFAULT;
}
await this._saveIdentityKey({
await this.#_saveIdentityKey({
id,
publicKey,
firstUse: false,
@ -2106,11 +2098,11 @@ export class SignalProtocolStore extends EventEmitter {
return true;
}
if (this.isNonBlockingApprovalRequired(identityRecord)) {
if (this.#isNonBlockingApprovalRequired(identityRecord)) {
log.info(`${logId}: Setting approval status...`);
identityRecord.nonblockingApproval = nonblockingApproval;
await this._saveIdentityKey(identityRecord);
await this.#_saveIdentityKey(identityRecord);
return false;
}
@ -2121,9 +2113,7 @@ export class SignalProtocolStore extends EventEmitter {
}
// https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L257
private isNonBlockingApprovalRequired(
identityRecord: IdentityKeyType
): boolean {
#isNonBlockingApprovalRequired(identityRecord: IdentityKeyType): boolean {
return (
!identityRecord.firstUse &&
isMoreRecentThan(identityRecord.timestamp, TIMESTAMP_THRESHOLD) &&
@ -2135,17 +2125,17 @@ export class SignalProtocolStore extends EventEmitter {
serviceId: ServiceIdString,
attributes: Partial<IdentityKeyType>
): Promise<void> {
return this._runOnIdentityQueue(
return this.#_runOnIdentityQueue(
serviceId,
GLOBAL_ZONE,
'saveIdentityWithAttributes',
async () => {
return this.saveIdentityWithAttributesOnQueue(serviceId, attributes);
return this.#saveIdentityWithAttributesOnQueue(serviceId, attributes);
}
);
}
private async saveIdentityWithAttributesOnQueue(
async #saveIdentityWithAttributesOnQueue(
serviceId: ServiceIdString,
attributes: Partial<IdentityKeyType>
): Promise<void> {
@ -2172,7 +2162,7 @@ export class SignalProtocolStore extends EventEmitter {
};
if (validateIdentityKey(updates)) {
await this._saveIdentityKey(updates);
await this.#_saveIdentityKey(updates);
}
}
@ -2187,7 +2177,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('setApproval: Invalid approval status');
}
return this._runOnIdentityQueue(
return this.#_runOnIdentityQueue(
serviceId,
GLOBAL_ZONE,
'setApproval',
@ -2199,7 +2189,7 @@ export class SignalProtocolStore extends EventEmitter {
}
identityRecord.nonblockingApproval = nonblockingApproval;
await this._saveIdentityKey(identityRecord);
await this.#_saveIdentityKey(identityRecord);
}
);
}
@ -2218,7 +2208,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('setVerified: Invalid verified status');
}
return this._runOnIdentityQueue(
return this.#_runOnIdentityQueue(
serviceId,
GLOBAL_ZONE,
'setVerified',
@ -2230,7 +2220,7 @@ export class SignalProtocolStore extends EventEmitter {
}
if (validateIdentityKey(identityRecord)) {
await this._saveIdentityKey({
await this.#_saveIdentityKey({
...identityRecord,
...extra,
verified: verifiedStatus,
@ -2304,7 +2294,7 @@ export class SignalProtocolStore extends EventEmitter {
`Invalid verified status: ${verifiedStatus}`
);
return this._runOnIdentityQueue(
return this.#_runOnIdentityQueue(
serviceId,
GLOBAL_ZONE,
'updateIdentityAfterSync',
@ -2319,7 +2309,7 @@ export class SignalProtocolStore extends EventEmitter {
keyMatches && verifiedStatus === identityRecord?.verified;
if (!keyMatches || !statusMatches) {
await this.saveIdentityWithAttributesOnQueue(serviceId, {
await this.#saveIdentityWithAttributesOnQueue(serviceId, {
publicKey,
verified: verifiedStatus,
firstUse: !hadEntry,
@ -2440,11 +2430,11 @@ export class SignalProtocolStore extends EventEmitter {
{ zone = GLOBAL_ZONE }: SessionTransactionOptions = {}
): Promise<void> {
return this.withZone(zone, 'addUnprocessed', async () => {
this.pendingUnprocessed.set(data.id, data);
this.#pendingUnprocessed.set(data.id, data);
// Current zone doesn't support pending unprocessed - commit immediately
if (!zone.supportsPendingUnprocessed()) {
await this.commitZoneChanges('addUnprocessed');
await this.#commitZoneChanges('addUnprocessed');
}
});
}
@ -2455,11 +2445,11 @@ export class SignalProtocolStore extends EventEmitter {
): Promise<void> {
return this.withZone(zone, 'addMultipleUnprocessed', async () => {
for (const elem of array) {
this.pendingUnprocessed.set(elem.id, elem);
this.#pendingUnprocessed.set(elem.id, elem);
}
// Current zone doesn't support pending unprocessed - commit immediately
if (!zone.supportsPendingUnprocessed()) {
await this.commitZoneChanges('addMultipleUnprocessed');
await this.#commitZoneChanges('addMultipleUnprocessed');
}
});
}
@ -2505,8 +2495,8 @@ export class SignalProtocolStore extends EventEmitter {
log.info(`SignalProtocolStore.removeOurOldPni(${oldPni})`);
// Update caches
this.ourIdentityKeys.delete(oldPni);
this.ourRegistrationIds.delete(oldPni);
this.#ourIdentityKeys.delete(oldPni);
this.#ourRegistrationIds.delete(oldPni);
const preKeyPrefix = `${oldPni}:`;
if (this.preKeys) {
@ -2575,11 +2565,11 @@ export class SignalProtocolStore extends EventEmitter {
const pniPrivateKey = identityKeyPair.privateKey.serialize();
// Update caches
this.ourIdentityKeys.set(pni, {
this.#ourIdentityKeys.set(pni, {
pubKey: pniPublicKey,
privKey: pniPrivateKey,
});
this.ourRegistrationIds.set(pni, registrationId);
this.#ourRegistrationIds.set(pni, registrationId);
// Update database
await Promise.all<void>([
@ -2670,8 +2660,8 @@ export class SignalProtocolStore extends EventEmitter {
return undefined;
}
if (this.cachedPniSignatureMessage?.pni === ourPni) {
return this.cachedPniSignatureMessage;
if (this.#cachedPniSignatureMessage?.pni === ourPni) {
return this.#cachedPniSignatureMessage;
}
const aciKeyPair = this.getIdentityKeyPair(ourAci);
@ -2690,12 +2680,12 @@ export class SignalProtocolStore extends EventEmitter {
PrivateKey.deserialize(Buffer.from(pniKeyPair.privKey))
);
const aciPubKey = PublicKey.deserialize(Buffer.from(aciKeyPair.pubKey));
this.cachedPniSignatureMessage = {
this.#cachedPniSignatureMessage = {
pni: ourPni,
signature: pniIdentity.signAlternateIdentity(aciPubKey),
};
return this.cachedPniSignatureMessage;
return this.#cachedPniSignatureMessage;
}
async verifyAlternateIdentity({
@ -2725,20 +2715,20 @@ export class SignalProtocolStore extends EventEmitter {
);
}
private _getAllSessions(): Array<SessionCacheEntry> {
#_getAllSessions(): Array<SessionCacheEntry> {
const union = new Map<string, SessionCacheEntry>();
this.sessions?.forEach((value, key) => {
union.set(key, value);
});
this.pendingSessions.forEach((value, key) => {
this.#pendingSessions.forEach((value, key) => {
union.set(key, value);
});
return Array.from(union.values());
}
private emitLowKeys(ourServiceId: ServiceIdString, source: string) {
#emitLowKeys(ourServiceId: ServiceIdString, source: string) {
const logId = `SignalProtocolStore.emitLowKeys/${source}:`;
try {
log.info(`${logId}: Emitting event`);

View file

@ -20,12 +20,12 @@ type OptionsType = {
};
export class WebAudioRecorder {
private buffer: Array<Float32Array>;
private options: OptionsType;
private context: BaseAudioContext;
private input: GainNode;
private onComplete: (recorder: WebAudioRecorder, blob: Blob) => unknown;
private onError: (recorder: WebAudioRecorder, error: string) => unknown;
#buffer: Array<Float32Array>;
#options: OptionsType;
#context: BaseAudioContext;
#input: GainNode;
#onComplete: (recorder: WebAudioRecorder, blob: Blob) => unknown;
#onError: (recorder: WebAudioRecorder, error: string) => unknown;
private processor?: ScriptProcessorNode;
public worker?: Worker;
@ -37,19 +37,19 @@ export class WebAudioRecorder {
onError: (recorder: WebAudioRecorder, error: string) => unknown;
}
) {
this.options = {
this.#options = {
...DEFAULT_OPTIONS,
...options,
};
this.context = sourceNode.context;
this.input = this.context.createGain();
sourceNode.connect(this.input);
this.buffer = [];
this.initWorker();
this.#context = sourceNode.context;
this.#input = this.#context.createGain();
sourceNode.connect(this.#input);
this.#buffer = [];
this.#initWorker();
this.onComplete = callbacks.onComplete;
this.onError = callbacks.onError;
this.#onComplete = callbacks.onComplete;
this.#onError = callbacks.onError;
}
isRecording(): boolean {
@ -62,21 +62,22 @@ export class WebAudioRecorder {
return;
}
const { buffer, worker } = this;
const { bufferSize, numChannels } = this.options;
const { worker } = this;
const buffer = this.#buffer;
const { bufferSize, numChannels } = this.#options;
if (!worker) {
this.error('startRecording: worker not initialized');
return;
}
this.processor = this.context.createScriptProcessor(
this.processor = this.#context.createScriptProcessor(
bufferSize,
numChannels,
numChannels
);
this.input.connect(this.processor);
this.processor.connect(this.context.destination);
this.#input.connect(this.processor);
this.processor.connect(this.#context.destination);
this.processor.onaudioprocess = event => {
// eslint-disable-next-line no-plusplus
for (let ch = 0; ch < numChannels; ++ch) {
@ -101,7 +102,7 @@ export class WebAudioRecorder {
return;
}
this.input.disconnect();
this.#input.disconnect();
this.processor.disconnect();
delete this.processor;
this.worker.postMessage({ command: 'cancel' });
@ -118,13 +119,13 @@ export class WebAudioRecorder {
return;
}
this.input.disconnect();
this.#input.disconnect();
this.processor.disconnect();
delete this.processor;
this.worker.postMessage({ command: 'finish' });
}
private initWorker(): void {
#initWorker(): void {
if (this.worker != null) {
this.worker.terminate();
}
@ -134,7 +135,7 @@ export class WebAudioRecorder {
const { data } = event;
switch (data.command) {
case 'complete':
this.onComplete(this, data.blob);
this.#onComplete(this, data.blob);
break;
case 'error':
this.error(data.message);
@ -146,14 +147,14 @@ export class WebAudioRecorder {
this.worker.postMessage({
command: 'init',
config: {
sampleRate: this.context.sampleRate,
numChannels: this.options.numChannels,
sampleRate: this.#context.sampleRate,
numChannels: this.#options.numChannels,
},
options: this.options,
options: this.#options,
});
}
error(message: string): void {
this.onError(this, `WebAudioRecorder.js: ${message}`);
this.#onError(this, `WebAudioRecorder.js: ${message}`);
}
}

View file

@ -15,12 +15,11 @@ enum BadgeDownloaderState {
}
class BadgeImageFileDownloader {
private state = BadgeDownloaderState.Idle;
private queue = new PQueue({ concurrency: 3 });
#state = BadgeDownloaderState.Idle;
#queue = new PQueue({ concurrency: 3 });
public async checkForFilesToDownload(): Promise<void> {
switch (this.state) {
switch (this.#state) {
case BadgeDownloaderState.CheckingWithAnotherCheckEnqueued:
log.info(
'BadgeDownloader#checkForFilesToDownload: not enqueuing another check'
@ -30,10 +29,10 @@ class BadgeImageFileDownloader {
log.info(
'BadgeDownloader#checkForFilesToDownload: enqueuing another check'
);
this.state = BadgeDownloaderState.CheckingWithAnotherCheckEnqueued;
this.#state = BadgeDownloaderState.CheckingWithAnotherCheckEnqueued;
return;
case BadgeDownloaderState.Idle: {
this.state = BadgeDownloaderState.Checking;
this.#state = BadgeDownloaderState.Checking;
const urlsToDownload = getUrlsToDownload();
log.info(
@ -41,7 +40,7 @@ class BadgeImageFileDownloader {
);
try {
await this.queue.addAll(
await this.#queue.addAll(
urlsToDownload.map(url => () => downloadBadgeImageFile(url))
);
} catch (err: unknown) {
@ -53,8 +52,8 @@ class BadgeImageFileDownloader {
// issue][0].
//
// [0]: https://github.com/microsoft/TypeScript/issues/9998
const previousState = this.state as BadgeDownloaderState;
this.state = BadgeDownloaderState.Idle;
const previousState = this.#state as BadgeDownloaderState;
this.#state = BadgeDownloaderState.Idle;
if (
previousState ===
BadgeDownloaderState.CheckingWithAnotherCheckEnqueued
@ -64,7 +63,7 @@ class BadgeImageFileDownloader {
return;
}
default:
throw missingCaseError(this.state);
throw missingCaseError(this.#state);
}
}
}

View file

@ -123,37 +123,30 @@ export function getChallengeURL(type: 'chat' | 'registration'): string {
// `ChallengeHandler` should be in memory at the same time because they could
// overwrite each others storage data.
export class ChallengeHandler {
private solving = 0;
#solving = 0;
#isLoaded = false;
#challengeToken: string | undefined;
#seq = 0;
#isOnline = false;
#challengeRateLimitRetryAt: undefined | number;
readonly #responseHandlers = new Map<number, Handler>();
private isLoaded = false;
private challengeToken: string | undefined;
private seq = 0;
private isOnline = false;
private challengeRateLimitRetryAt: undefined | number;
private readonly responseHandlers = new Map<number, Handler>();
private readonly registeredConversations = new Map<
readonly #registeredConversations = new Map<
string,
RegisteredChallengeType
>();
private readonly startTimers = new Map<string, NodeJS.Timeout>();
private readonly pendingStarts = new Set<string>();
readonly #startTimers = new Map<string, NodeJS.Timeout>();
readonly #pendingStarts = new Set<string>();
constructor(private readonly options: Options) {}
public async load(): Promise<void> {
if (this.isLoaded) {
if (this.#isLoaded) {
return;
}
this.isLoaded = true;
this.#isLoaded = true;
const challenges: ReadonlyArray<RegisteredChallengeType> =
this.options.storage.get(STORAGE_KEY) || [];
@ -182,39 +175,39 @@ export class ChallengeHandler {
}
public async onOffline(): Promise<void> {
this.isOnline = false;
this.#isOnline = false;
log.info('challenge: offline');
}
public async onOnline(): Promise<void> {
this.isOnline = true;
this.#isOnline = true;
const pending = Array.from(this.pendingStarts.values());
this.pendingStarts.clear();
const pending = Array.from(this.#pendingStarts.values());
this.#pendingStarts.clear();
log.info(`challenge: online, starting ${pending.length} queues`);
// Start queues for challenges that matured while we were offline
await this.startAllQueues();
await this.#startAllQueues();
}
public maybeSolve({ conversationId, reason }: MaybeSolveOptionsType): void {
const challenge = this.registeredConversations.get(conversationId);
const challenge = this.#registeredConversations.get(conversationId);
if (!challenge) {
return;
}
if (this.solving > 0) {
if (this.#solving > 0) {
return;
}
if (this.challengeRateLimitRetryAt) {
if (this.#challengeRateLimitRetryAt) {
return;
}
if (challenge.token) {
drop(this.solve({ reason, token: challenge.token }));
drop(this.#solve({ reason, token: challenge.token }));
}
}
@ -224,18 +217,18 @@ export class ChallengeHandler {
reason: string
): void {
const waitTime = Math.max(0, retryAt - Date.now());
const oldTimer = this.startTimers.get(conversationId);
const oldTimer = this.#startTimers.get(conversationId);
if (oldTimer) {
clearTimeoutIfNecessary(oldTimer);
}
this.startTimers.set(
this.#startTimers.set(
conversationId,
setTimeout(() => {
this.startTimers.delete(conversationId);
this.#startTimers.delete(conversationId);
this.challengeRateLimitRetryAt = undefined;
this.#challengeRateLimitRetryAt = undefined;
drop(this.startQueue(conversationId));
drop(this.#startQueue(conversationId));
}, waitTime)
);
log.info(
@ -244,14 +237,14 @@ export class ChallengeHandler {
}
public forceWaitOnAll(retryAt: number): void {
this.challengeRateLimitRetryAt = retryAt;
this.#challengeRateLimitRetryAt = retryAt;
for (const conversationId of this.registeredConversations.keys()) {
const existing = this.registeredConversations.get(conversationId);
for (const conversationId of this.#registeredConversations.keys()) {
const existing = this.#registeredConversations.get(conversationId);
if (!existing) {
continue;
}
this.registeredConversations.set(conversationId, {
this.#registeredConversations.set(conversationId, {
...existing,
retryAt,
});
@ -271,20 +264,20 @@ export class ChallengeHandler {
return;
}
this.registeredConversations.set(conversationId, challenge);
await this.persist();
this.#registeredConversations.set(conversationId, challenge);
await this.#persist();
// Challenge is already retryable - start the queue
if (shouldStartQueue(challenge)) {
log.info(`${logId}: starting conversation ${conversationId} immediately`);
await this.startQueue(conversationId);
await this.#startQueue(conversationId);
return;
}
if (this.challengeRateLimitRetryAt) {
if (this.#challengeRateLimitRetryAt) {
this.scheduleRetry(
conversationId,
this.challengeRateLimitRetryAt,
this.#challengeRateLimitRetryAt,
'register-challengeRateLimit'
);
} else if (challenge.retryAt) {
@ -310,17 +303,17 @@ export class ChallengeHandler {
}
if (!challenge.silent) {
drop(this.solve({ token: challenge.token, reason }));
drop(this.#solve({ token: challenge.token, reason }));
}
}
public onResponse(response: IPCResponse): void {
const handler = this.responseHandlers.get(response.seq);
const handler = this.#responseHandlers.get(response.seq);
if (!handler) {
return;
}
this.responseHandlers.delete(response.seq);
this.#responseHandlers.delete(response.seq);
handler.resolve(response.data);
}
@ -331,72 +324,72 @@ export class ChallengeHandler {
log.info(
`challenge: unregistered conversation ${conversationId} via ${source}`
);
this.registeredConversations.delete(conversationId);
this.pendingStarts.delete(conversationId);
this.#registeredConversations.delete(conversationId);
this.#pendingStarts.delete(conversationId);
const timer = this.startTimers.get(conversationId);
this.startTimers.delete(conversationId);
const timer = this.#startTimers.get(conversationId);
this.#startTimers.delete(conversationId);
clearTimeoutIfNecessary(timer);
await this.persist();
await this.#persist();
}
public async requestCaptcha({
reason,
token = '',
}: RequestCaptchaOptionsType): Promise<string> {
const request: IPCRequest = { seq: this.seq, reason };
this.seq += 1;
const request: IPCRequest = { seq: this.#seq, reason };
this.#seq += 1;
this.options.requestChallenge(request);
const response = await new Promise<ChallengeResponse>((resolve, reject) => {
this.responseHandlers.set(request.seq, { token, resolve, reject });
this.#responseHandlers.set(request.seq, { token, resolve, reject });
});
return response.captcha;
}
private async persist(): Promise<void> {
async #persist(): Promise<void> {
assertDev(
this.isLoaded,
this.#isLoaded,
'ChallengeHandler has to be loaded before persisting new data'
);
await this.options.storage.put(
STORAGE_KEY,
Array.from(this.registeredConversations.values())
Array.from(this.#registeredConversations.values())
);
}
public areAnyRegistered(): boolean {
return this.registeredConversations.size > 0;
return this.#registeredConversations.size > 0;
}
public isRegistered(conversationId: string): boolean {
return this.registeredConversations.has(conversationId);
return this.#registeredConversations.has(conversationId);
}
private startAllQueues({
#startAllQueues({
force = false,
}: {
force?: boolean;
} = {}): void {
log.info(`challenge: startAllQueues force=${force}`);
Array.from(this.registeredConversations.values())
Array.from(this.#registeredConversations.values())
.filter(challenge => force || shouldStartQueue(challenge))
.forEach(challenge => this.startQueue(challenge.conversationId));
.forEach(challenge => this.#startQueue(challenge.conversationId));
}
private async startQueue(conversationId: string): Promise<void> {
if (!this.isOnline) {
this.pendingStarts.add(conversationId);
async #startQueue(conversationId: string): Promise<void> {
if (!this.#isOnline) {
this.#pendingStarts.add(conversationId);
return;
}
await this.unregister(conversationId, 'startQueue');
if (this.registeredConversations.size === 0) {
if (this.#registeredConversations.size === 0) {
this.options.setChallengeStatus('idle');
}
@ -404,21 +397,21 @@ export class ChallengeHandler {
this.options.startQueue(conversationId);
}
private async solve({ reason, token }: SolveOptionsType): Promise<void> {
this.solving += 1;
async #solve({ reason, token }: SolveOptionsType): Promise<void> {
this.#solving += 1;
this.options.setChallengeStatus('required');
this.challengeToken = token;
this.#challengeToken = token;
const captcha = await this.requestCaptcha({ reason, token });
// Another `.solve()` has completed earlier than us
if (this.challengeToken === undefined) {
this.solving -= 1;
if (this.#challengeToken === undefined) {
this.#solving -= 1;
return;
}
const lastToken = this.challengeToken;
this.challengeToken = undefined;
const lastToken = this.#challengeToken;
this.#challengeToken = undefined;
this.options.setChallengeStatus('pending');
@ -465,13 +458,13 @@ export class ChallengeHandler {
this.forceWaitOnAll(retryAt);
return;
} finally {
this.solving -= 1;
this.#solving -= 1;
}
log.info(`challenge(${reason}): challenge success. force sending`);
this.options.setChallengeStatus('idle');
this.options.onChallengeSolved();
this.startAllQueues({ force: true });
this.#startAllQueues({ force: true });
}
}

View file

@ -47,8 +47,8 @@ export class ErrorBoundary extends React.PureComponent<Props, State> {
return (
<div
className={CSS_MODULE}
onClick={this.onClick.bind(this)}
onKeyDown={this.onKeyDown.bind(this)}
onClick={this.#onClick.bind(this)}
onKeyDown={this.#onKeyDown.bind(this)}
role="button"
tabIndex={0}
>
@ -62,24 +62,24 @@ export class ErrorBoundary extends React.PureComponent<Props, State> {
);
}
private onClick(event: React.MouseEvent): void {
#onClick(event: React.MouseEvent): void {
event.stopPropagation();
event.preventDefault();
this.onAction();
this.#onAction();
}
private onKeyDown(event: React.KeyboardEvent): void {
#onKeyDown(event: React.KeyboardEvent): void {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
event.stopPropagation();
event.preventDefault();
this.onAction();
this.#onAction();
}
private onAction(): void {
#onAction(): void {
const { showDebugLog } = this.props;
showDebugLog();
}

View file

@ -410,11 +410,11 @@ export class Message extends React.PureComponent<Props, State> {
public reactionsContainerRef: React.RefObject<HTMLDivElement> =
React.createRef();
private hasSelectedTextRef: React.MutableRefObject<boolean> = {
#hasSelectedTextRef: React.MutableRefObject<boolean> = {
current: false,
};
private metadataRef: React.RefObject<HTMLDivElement> = React.createRef();
#metadataRef: React.RefObject<HTMLDivElement> = React.createRef();
public reactionsContainerRefMerger = createRefMerger();
@ -432,7 +432,7 @@ export class Message extends React.PureComponent<Props, State> {
super(props);
this.state = {
metadataWidth: this.guessMetadataWidth(),
metadataWidth: this.#guessMetadataWidth(),
expiring: false,
expired: false,
@ -447,7 +447,7 @@ export class Message extends React.PureComponent<Props, State> {
showOutgoingGiftBadgeModal: false,
hasDeleteForEveryoneTimerExpired:
this.getTimeRemainingForDeleteForEveryone() <= 0,
this.#getTimeRemainingForDeleteForEveryone() <= 0,
};
}
@ -474,7 +474,7 @@ export class Message extends React.PureComponent<Props, State> {
return state;
}
private hasReactions(): boolean {
#hasReactions(): boolean {
const { reactions } = this.props;
return Boolean(reactions && reactions.length);
}
@ -518,7 +518,7 @@ export class Message extends React.PureComponent<Props, State> {
window.ConversationController?.onConvoMessageMount(conversationId);
this.startTargetedTimer();
this.startDeleteForEveryoneTimerIfApplicable();
this.#startDeleteForEveryoneTimerIfApplicable();
this.startGiftBadgeInterval();
const { isTargeted } = this.props;
@ -543,7 +543,7 @@ export class Message extends React.PureComponent<Props, State> {
checkForAccount(contact.firstNumber);
}
document.addEventListener('selectionchange', this.handleSelectionChange);
document.addEventListener('selectionchange', this.#handleSelectionChange);
}
public override componentWillUnmount(): void {
@ -553,14 +553,17 @@ export class Message extends React.PureComponent<Props, State> {
clearTimeoutIfNecessary(this.deleteForEveryoneTimeout);
clearTimeoutIfNecessary(this.giftBadgeInterval);
this.toggleReactionViewer(true);
document.removeEventListener('selectionchange', this.handleSelectionChange);
document.removeEventListener(
'selectionchange',
this.#handleSelectionChange
);
}
public override componentDidUpdate(prevProps: Readonly<Props>): void {
const { isTargeted, status, timestamp } = this.props;
this.startTargetedTimer();
this.startDeleteForEveryoneTimerIfApplicable();
this.#startDeleteForEveryoneTimerIfApplicable();
if (!prevProps.isTargeted && isTargeted) {
this.setFocus();
@ -586,7 +589,7 @@ export class Message extends React.PureComponent<Props, State> {
}
}
private getMetadataPlacement(
#getMetadataPlacement(
{
attachments,
attachmentDroppedDueToSize,
@ -634,11 +637,11 @@ export class Message extends React.PureComponent<Props, State> {
return MetadataPlacement.InlineWithText;
}
if (this.canRenderStickerLikeEmoji()) {
if (this.#canRenderStickerLikeEmoji()) {
return MetadataPlacement.Bottom;
}
if (this.shouldShowJoinButton()) {
if (this.#shouldShowJoinButton()) {
return MetadataPlacement.Bottom;
}
@ -653,7 +656,7 @@ export class Message extends React.PureComponent<Props, State> {
* This will probably guess wrong, but it's valuable to get close to the real value
* because it can reduce layout jumpiness.
*/
private guessMetadataWidth(): number {
#guessMetadataWidth(): number {
const { direction, expirationLength, isSMS, status, isEditedMessage } =
this.props;
@ -714,12 +717,12 @@ export class Message extends React.PureComponent<Props, State> {
}));
}
private getTimeRemainingForDeleteForEveryone(): number {
#getTimeRemainingForDeleteForEveryone(): number {
const { timestamp } = this.props;
return Math.max(timestamp - Date.now() + DAY, 0);
}
private startDeleteForEveryoneTimerIfApplicable(): void {
#startDeleteForEveryoneTimerIfApplicable(): void {
const { canDeleteForEveryone } = this.props;
const { hasDeleteForEveryoneTimerExpired } = this.state;
if (
@ -733,7 +736,7 @@ export class Message extends React.PureComponent<Props, State> {
this.deleteForEveryoneTimeout = setTimeout(() => {
this.setState({ hasDeleteForEveryoneTimerExpired: true });
delete this.deleteForEveryoneTimeout;
}, this.getTimeRemainingForDeleteForEveryone());
}, this.#getTimeRemainingForDeleteForEveryone());
}
public checkExpired(): void {
@ -761,12 +764,12 @@ export class Message extends React.PureComponent<Props, State> {
}
}
private areLinksEnabled(): boolean {
#areLinksEnabled(): boolean {
const { isMessageRequestAccepted, isBlocked } = this.props;
return isMessageRequestAccepted && !isBlocked;
}
private shouldRenderAuthor(): boolean {
#shouldRenderAuthor(): boolean {
const { author, conversationType, direction, shouldCollapseAbove } =
this.props;
return Boolean(
@ -777,7 +780,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private canRenderStickerLikeEmoji(): boolean {
#canRenderStickerLikeEmoji(): boolean {
const {
attachments,
bodyRanges,
@ -799,7 +802,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private updateMetadataWidth = (newMetadataWidth: number): void => {
#updateMetadataWidth = (newMetadataWidth: number): void => {
this.setState(({ metadataWidth }) => ({
// We don't want text to jump around if the metadata shrinks, but we want to make
// sure we have enough room.
@ -807,16 +810,16 @@ export class Message extends React.PureComponent<Props, State> {
}));
};
private handleSelectionChange = () => {
#handleSelectionChange = () => {
const selection = document.getSelection();
if (selection != null && !selection.isCollapsed) {
this.hasSelectedTextRef.current = true;
this.#hasSelectedTextRef.current = true;
}
};
private renderMetadata(): ReactNode {
#renderMetadata(): ReactNode {
let isInline: boolean;
const metadataPlacement = this.getMetadataPlacement();
const metadataPlacement = this.#getMetadataPlacement();
switch (metadataPlacement) {
case MetadataPlacement.NotRendered:
case MetadataPlacement.RenderedByMessageAudioComponent:
@ -854,7 +857,7 @@ export class Message extends React.PureComponent<Props, State> {
timestamp,
} = this.props;
const isStickerLike = isSticker || this.canRenderStickerLikeEmoji();
const isStickerLike = isSticker || this.#canRenderStickerLikeEmoji();
return (
<MessageMetadata
@ -874,9 +877,9 @@ export class Message extends React.PureComponent<Props, State> {
isShowingImage={this.isShowingImage()}
isSticker={isStickerLike}
isTapToViewExpired={isTapToViewExpired}
onWidthMeasured={isInline ? this.updateMetadataWidth : undefined}
onWidthMeasured={isInline ? this.#updateMetadataWidth : undefined}
pushPanelForConversation={pushPanelForConversation}
ref={this.metadataRef}
ref={this.#metadataRef}
retryMessageSend={retryMessageSend}
showEditHistoryModal={showEditHistoryModal}
status={status}
@ -886,7 +889,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private renderAuthor(): ReactNode {
#renderAuthor(): ReactNode {
const {
author,
contactNameColor,
@ -896,7 +899,7 @@ export class Message extends React.PureComponent<Props, State> {
isTapToViewExpired,
} = this.props;
if (!this.shouldRenderAuthor()) {
if (!this.#shouldRenderAuthor()) {
return null;
}
@ -951,7 +954,7 @@ export class Message extends React.PureComponent<Props, State> {
const { imageBroken } = this.state;
const collapseMetadata =
this.getMetadataPlacement() === MetadataPlacement.NotRendered;
this.#getMetadataPlacement() === MetadataPlacement.NotRendered;
if (!attachments || !attachments[0]) {
return null;
@ -960,7 +963,7 @@ export class Message extends React.PureComponent<Props, State> {
// For attachments which aren't full-frame
const withContentBelow = Boolean(text || attachmentDroppedDueToSize);
const withContentAbove = Boolean(quote) || this.shouldRenderAuthor();
const withContentAbove = Boolean(quote) || this.#shouldRenderAuthor();
const displayImage = canDisplayImage(attachments);
if (displayImage && !imageBroken) {
@ -1203,7 +1206,7 @@ export class Message extends React.PureComponent<Props, State> {
? i18n('icu:message--call-link-description')
: undefined);
const isClickable = this.areLinksEnabled();
const isClickable = this.#areLinksEnabled();
const className = classNames(
'module-message__link-preview',
@ -1371,7 +1374,7 @@ export class Message extends React.PureComponent<Props, State> {
const maybeSpacer = text
? undefined
: this.getMetadataPlacement() === MetadataPlacement.InlineWithText && (
: this.#getMetadataPlacement() === MetadataPlacement.InlineWithText && (
<MessageTextMetadataSpacer metadataWidth={metadataWidth} />
);
@ -1456,12 +1459,12 @@ export class Message extends React.PureComponent<Props, State> {
)}
>
{description}
{this.getMetadataPlacement() ===
{this.#getMetadataPlacement() ===
MetadataPlacement.InlineWithText && (
<MessageTextMetadataSpacer metadataWidth={metadataWidth} />
)}
</div>
{this.renderMetadata()}
{this.#renderMetadata()}
</div>
</div>
);
@ -1569,7 +1572,7 @@ export class Message extends React.PureComponent<Props, State> {
{buttonContents}
</div>
</button>
{this.renderMetadata()}
{this.#renderMetadata()}
{showOutgoingGiftBadgeModal ? (
<OutgoingGiftBadgeModal
i18n={i18n}
@ -1763,7 +1766,7 @@ export class Message extends React.PureComponent<Props, State> {
conversationType === 'group' && direction === 'incoming';
const withContentBelow =
withCaption ||
this.getMetadataPlacement() !== MetadataPlacement.NotRendered;
this.#getMetadataPlacement() !== MetadataPlacement.NotRendered;
const otherContent =
(contact && contact.firstNumber && contact.serviceId) || withCaption;
@ -1833,7 +1836,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private renderAvatar(): ReactNode {
#renderAvatar(): ReactNode {
const {
author,
conversationId,
@ -1854,7 +1857,7 @@ export class Message extends React.PureComponent<Props, State> {
<div
className={classNames('module-message__author-avatar-container', {
'module-message__author-avatar-container--with-reactions':
this.hasReactions(),
this.#hasReactions(),
})}
>
{shouldCollapseBelow ? (
@ -1887,7 +1890,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private getContents(): string | undefined {
#getContents(): string | undefined {
const { deletedForEveryone, direction, i18n, status, text } = this.props;
if (deletedForEveryone) {
@ -1920,7 +1923,7 @@ export class Message extends React.PureComponent<Props, State> {
} = this.props;
const { metadataWidth } = this.state;
const contents = this.getContents();
const contents = this.#getContents();
if (!contents) {
return null;
@ -1950,10 +1953,10 @@ export class Message extends React.PureComponent<Props, State> {
const range = window.getSelection()?.getRangeAt(0);
if (
clickCount === 3 &&
this.metadataRef.current &&
range?.intersectsNode(this.metadataRef.current)
this.#metadataRef.current &&
range?.intersectsNode(this.#metadataRef.current)
) {
range.setEndBefore(this.metadataRef.current);
range.setEndBefore(this.#metadataRef.current);
}
}}
onDoubleClick={(event: React.MouseEvent) => {
@ -1965,7 +1968,7 @@ export class Message extends React.PureComponent<Props, State> {
<MessageBodyReadMore
bodyRanges={bodyRanges}
direction={direction}
disableLinks={!this.areLinksEnabled()}
disableLinks={!this.#areLinksEnabled()}
displayLimit={displayLimit}
i18n={i18n}
id={id}
@ -1988,14 +1991,14 @@ export class Message extends React.PureComponent<Props, State> {
text={contents || ''}
textAttachment={textAttachment}
/>
{this.getMetadataPlacement() === MetadataPlacement.InlineWithText && (
{this.#getMetadataPlacement() === MetadataPlacement.InlineWithText && (
<MessageTextMetadataSpacer metadataWidth={metadataWidth} />
)}
</div>
);
}
private shouldShowJoinButton(): boolean {
#shouldShowJoinButton(): boolean {
const { previews } = this.props;
if (previews?.length !== 1) {
@ -2006,10 +2009,10 @@ export class Message extends React.PureComponent<Props, State> {
return Boolean(onlyPreview.isCallLink);
}
private renderAction(): JSX.Element | null {
#renderAction(): JSX.Element | null {
const { direction, activeCallConversationId, i18n, previews } = this.props;
if (this.shouldShowJoinButton()) {
if (this.#shouldShowJoinButton()) {
const firstPreview = previews[0];
const inAnotherCall = Boolean(
activeCallConversationId &&
@ -2044,7 +2047,7 @@ export class Message extends React.PureComponent<Props, State> {
return null;
}
private renderError(): ReactNode {
#renderError(): ReactNode {
const { status, direction } = this.props;
if (
@ -2205,7 +2208,7 @@ export class Message extends React.PureComponent<Props, State> {
} = this.props;
const collapseMetadata =
this.getMetadataPlacement() === MetadataPlacement.NotRendered;
this.#getMetadataPlacement() === MetadataPlacement.NotRendered;
const withContentBelow = !collapseMetadata;
const withContentAbove =
!collapseMetadata &&
@ -2243,7 +2246,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private popperPreventOverflowModifier(): Partial<PreventOverflowModifier> {
#popperPreventOverflowModifier(): Partial<PreventOverflowModifier> {
const { containerElementRef } = this.props;
return {
name: 'preventOverflow',
@ -2302,7 +2305,7 @@ export class Message extends React.PureComponent<Props, State> {
public renderReactions(outgoing: boolean): JSX.Element | null {
const { getPreferredBadge, reactions = [], i18n, theme } = this.props;
if (!this.hasReactions()) {
if (!this.#hasReactions()) {
return null;
}
@ -2465,7 +2468,7 @@ export class Message extends React.PureComponent<Props, State> {
<Popper
placement={popperPlacement}
strategy="fixed"
modifiers={[this.popperPreventOverflowModifier()]}
modifiers={[this.#popperPreventOverflowModifier()]}
>
{({ ref, style }) => (
<ReactionViewer
@ -2495,7 +2498,7 @@ export class Message extends React.PureComponent<Props, State> {
return (
<>
{this.renderText()}
{this.renderMetadata()}
{this.#renderMetadata()}
</>
);
}
@ -2508,7 +2511,7 @@ export class Message extends React.PureComponent<Props, State> {
return (
<>
{this.renderTapToView()}
{this.renderMetadata()}
{this.#renderMetadata()}
</>
);
}
@ -2523,8 +2526,8 @@ export class Message extends React.PureComponent<Props, State> {
{this.renderPayment()}
{this.renderEmbeddedContact()}
{this.renderText()}
{this.renderAction()}
{this.renderMetadata()}
{this.#renderAction()}
{this.#renderMetadata()}
{this.renderSendMessageButton()}
</>
);
@ -2740,7 +2743,7 @@ export class Message extends React.PureComponent<Props, State> {
const isAttachmentPending = this.isAttachmentPending();
const width = this.getWidth();
const isEmojiOnly = this.canRenderStickerLikeEmoji();
const isEmojiOnly = this.#canRenderStickerLikeEmoji();
const isStickerLike = isSticker || isEmojiOnly;
// If it's a mostly-normal gray incoming text box, we don't want to darken it as much
@ -2773,7 +2776,7 @@ export class Message extends React.PureComponent<Props, State> {
isTapToViewError
? 'module-message__container--with-tap-to-view-error'
: null,
this.hasReactions() ? 'module-message__container--with-reactions' : null,
this.#hasReactions() ? 'module-message__container--with-reactions' : null,
deletedForEveryone
? 'module-message__container--deleted-for-everyone'
: null
@ -2806,7 +2809,7 @@ export class Message extends React.PureComponent<Props, State> {
}}
tabIndex={-1}
>
{this.renderAuthor()}
{this.#renderAuthor()}
<div dir={TextDirectionToDirAttribute[textDirection]}>
{this.renderContents()}
</div>
@ -2890,13 +2893,13 @@ export class Message extends React.PureComponent<Props, State> {
} else {
wrapperProps = {
onMouseDown: () => {
this.hasSelectedTextRef.current = false;
this.#hasSelectedTextRef.current = false;
},
// We use `onClickCapture` here and preven default/stop propagation to
// prevent other click handlers from firing.
onClickCapture: event => {
if (isMacOS ? event.metaKey : event.ctrlKey) {
if (this.hasSelectedTextRef.current) {
if (this.#hasSelectedTextRef.current) {
return;
}
@ -2964,8 +2967,8 @@ export class Message extends React.PureComponent<Props, State> {
// eslint-disable-next-line react/no-unknown-property
inert={isSelectMode ? '' : undefined}
>
{this.renderError()}
{this.renderAvatar()}
{this.#renderError()}
{this.#renderAvatar()}
{this.renderContainer()}
{renderMenu?.()}
</div>

View file

@ -189,18 +189,18 @@ export class Timeline extends React.Component<
StateType,
SnapshotType
> {
private readonly containerRef = React.createRef<HTMLDivElement>();
private readonly messagesRef = React.createRef<HTMLDivElement>();
private readonly atBottomDetectorRef = React.createRef<HTMLDivElement>();
private readonly lastSeenIndicatorRef = React.createRef<HTMLDivElement>();
private intersectionObserver?: IntersectionObserver;
readonly #containerRef = React.createRef<HTMLDivElement>();
readonly #messagesRef = React.createRef<HTMLDivElement>();
readonly #atBottomDetectorRef = React.createRef<HTMLDivElement>();
readonly #lastSeenIndicatorRef = React.createRef<HTMLDivElement>();
#intersectionObserver?: IntersectionObserver;
// This is a best guess. It will likely be overridden when the timeline is measured.
private maxVisibleRows = Math.ceil(window.innerHeight / MIN_ROW_HEIGHT);
#maxVisibleRows = Math.ceil(window.innerHeight / MIN_ROW_HEIGHT);
private hasRecentlyScrolledTimeout?: NodeJS.Timeout;
private delayedPeekTimeout?: NodeJS.Timeout;
private peekInterval?: NodeJS.Timeout;
#hasRecentlyScrolledTimeout?: NodeJS.Timeout;
#delayedPeekTimeout?: NodeJS.Timeout;
#peekInterval?: NodeJS.Timeout;
// eslint-disable-next-line react/state-in-constructor
override state: StateType = {
@ -213,31 +213,28 @@ export class Timeline extends React.Component<
widthBreakpoint: WidthBreakpoint.Wide,
};
private onScrollLockChange = (): void => {
#onScrollLockChange = (): void => {
this.setState({
scrollLocked: this.scrollerLock.isLocked(),
scrollLocked: this.#scrollerLock.isLocked(),
});
};
private scrollerLock = createScrollerLock(
'Timeline',
this.onScrollLockChange
);
#scrollerLock = createScrollerLock('Timeline', this.#onScrollLockChange);
private onScroll = (event: UIEvent): void => {
#onScroll = (event: UIEvent): void => {
// When content is removed from the viewport, such as typing indicators leaving
// or messages being edited smaller or deleted, scroll events are generated and
// they are marked as user-generated (isTrusted === true). Actual user generated
// scroll events with movement must scroll a nonbottom state at some point.
const isAtBottom = this.isAtBottom();
const isAtBottom = this.#isAtBottom();
if (event.isTrusted && !isAtBottom) {
this.scrollerLock.onUserInterrupt('onScroll');
this.#scrollerLock.onUserInterrupt('onScroll');
}
// hasRecentlyScrolled is used to show the floating date header, which we only
// want to show when scrolling through history or on conversation first open.
// Checking bottom prevents new messages and typing from showing the header.
if (!this.state.hasRecentlyScrolled && this.isAtBottom()) {
if (!this.state.hasRecentlyScrolled && this.#isAtBottom()) {
return;
}
@ -248,24 +245,24 @@ export class Timeline extends React.Component<
// [0]: https://github.com/facebook/react/blob/29b7b775f2ecf878eaf605be959d959030598b07/packages/react-reconciler/src/ReactUpdateQueue.js#L401-L404
oldState.hasRecentlyScrolled ? null : { hasRecentlyScrolled: true }
);
clearTimeoutIfNecessary(this.hasRecentlyScrolledTimeout);
this.hasRecentlyScrolledTimeout = setTimeout(() => {
clearTimeoutIfNecessary(this.#hasRecentlyScrolledTimeout);
this.#hasRecentlyScrolledTimeout = setTimeout(() => {
this.setState({ hasRecentlyScrolled: false });
}, 3000);
};
private scrollToItemIndex(itemIndex: number): void {
if (this.scrollerLock.isLocked()) {
#scrollToItemIndex(itemIndex: number): void {
if (this.#scrollerLock.isLocked()) {
return;
}
this.messagesRef.current
this.#messagesRef.current
?.querySelector(`[data-item-index="${itemIndex}"]`)
?.scrollIntoViewIfNeeded();
}
private scrollToBottom = (setFocus?: boolean): void => {
if (this.scrollerLock.isLocked()) {
#scrollToBottom = (setFocus?: boolean): void => {
if (this.#scrollerLock.isLocked()) {
return;
}
@ -276,20 +273,20 @@ export class Timeline extends React.Component<
const lastMessageId = items[lastIndex];
targetMessage(lastMessageId, id);
} else {
const containerEl = this.containerRef.current;
const containerEl = this.#containerRef.current;
if (containerEl) {
scrollToBottom(containerEl);
}
}
};
private onClickScrollDownButton = (): void => {
this.scrollerLock.onUserInterrupt('onClickScrollDownButton');
this.scrollDown(false);
#onClickScrollDownButton = (): void => {
this.#scrollerLock.onUserInterrupt('onClickScrollDownButton');
this.#scrollDown(false);
};
private scrollDown = (setFocus?: boolean): void => {
if (this.scrollerLock.isLocked()) {
#scrollDown = (setFocus?: boolean): void => {
if (this.#scrollerLock.isLocked()) {
return;
}
@ -309,7 +306,7 @@ export class Timeline extends React.Component<
}
if (messageLoadingState) {
this.scrollToBottom(setFocus);
this.#scrollToBottom(setFocus);
return;
}
@ -323,10 +320,10 @@ export class Timeline extends React.Component<
const messageId = items[oldestUnseenIndex];
targetMessage(messageId, id);
} else {
this.scrollToItemIndex(oldestUnseenIndex);
this.#scrollToItemIndex(oldestUnseenIndex);
}
} else if (haveNewest) {
this.scrollToBottom(setFocus);
this.#scrollToBottom(setFocus);
} else {
const lastId = last(items);
if (lastId) {
@ -335,8 +332,8 @@ export class Timeline extends React.Component<
}
};
private isAtBottom(): boolean {
const containerEl = this.containerRef.current;
#isAtBottom(): boolean {
const containerEl = this.#containerRef.current;
if (!containerEl) {
return false;
}
@ -346,10 +343,10 @@ export class Timeline extends React.Component<
return isScrolledNearBottom || !hasScrollbars;
}
private updateIntersectionObserver(): void {
const containerEl = this.containerRef.current;
const messagesEl = this.messagesRef.current;
const atBottomDetectorEl = this.atBottomDetectorRef.current;
#updateIntersectionObserver(): void {
const containerEl = this.#containerRef.current;
const messagesEl = this.#messagesRef.current;
const atBottomDetectorEl = this.#atBottomDetectorRef.current;
if (!containerEl || !messagesEl || !atBottomDetectorEl) {
return;
}
@ -368,7 +365,7 @@ export class Timeline extends React.Component<
// We re-initialize the `IntersectionObserver`. We don't want stale references to old
// props, and we care about the order of `IntersectionObserverEntry`s. (We could do
// this another way, but this approach works.)
this.intersectionObserver?.disconnect();
this.#intersectionObserver?.disconnect();
const intersectionRatios = new Map<Element, number>();
@ -445,7 +442,7 @@ export class Timeline extends React.Component<
setIsNearBottom(id, newIsNearBottom);
if (newestBottomVisibleMessageId) {
this.markNewestBottomVisibleMessageRead();
this.#markNewestBottomVisibleMessageRead();
const rowIndex = getRowIndexFromElement(newestBottomVisible);
const maxRowIndex = items.length - 1;
@ -471,15 +468,15 @@ export class Timeline extends React.Component<
}
};
this.intersectionObserver = new IntersectionObserver(
this.#intersectionObserver = new IntersectionObserver(
(entries, observer) => {
assertDev(
this.intersectionObserver === observer,
this.#intersectionObserver === observer,
'observer.disconnect() should prevent callbacks from firing'
);
// Observer was updated from under us
if (this.intersectionObserver !== observer) {
if (this.#intersectionObserver !== observer) {
return;
}
@ -493,13 +490,13 @@ export class Timeline extends React.Component<
for (const child of messagesEl.children) {
if ((child as HTMLElement).dataset.messageId) {
this.intersectionObserver.observe(child);
this.#intersectionObserver.observe(child);
}
}
this.intersectionObserver.observe(atBottomDetectorEl);
this.#intersectionObserver.observe(atBottomDetectorEl);
}
private markNewestBottomVisibleMessageRead = throttle((): void => {
#markNewestBottomVisibleMessageRead = throttle((): void => {
const { id, markMessageRead } = this.props;
const { newestBottomVisibleMessageId } = this.state;
if (newestBottomVisibleMessageId) {
@ -507,36 +504,37 @@ export class Timeline extends React.Component<
}
}, 500);
private setupGroupCallPeekTimeouts(): void {
this.cleanupGroupCallPeekTimeouts();
#setupGroupCallPeekTimeouts(): void {
this.#cleanupGroupCallPeekTimeouts();
this.delayedPeekTimeout = setTimeout(() => {
this.#delayedPeekTimeout = setTimeout(() => {
const { id, peekGroupCallForTheFirstTime } = this.props;
this.delayedPeekTimeout = undefined;
this.#delayedPeekTimeout = undefined;
peekGroupCallForTheFirstTime(id);
}, 500);
this.peekInterval = setInterval(() => {
this.#peekInterval = setInterval(() => {
const { id, peekGroupCallIfItHasMembers } = this.props;
peekGroupCallIfItHasMembers(id);
}, MINUTE);
}
private cleanupGroupCallPeekTimeouts(): void {
const { delayedPeekTimeout, peekInterval } = this;
#cleanupGroupCallPeekTimeouts(): void {
const peekInterval = this.#peekInterval;
const delayedPeekTimeout = this.#delayedPeekTimeout;
clearTimeoutIfNecessary(delayedPeekTimeout);
this.delayedPeekTimeout = undefined;
this.#delayedPeekTimeout = undefined;
if (peekInterval) {
clearInterval(peekInterval);
this.peekInterval = undefined;
this.#peekInterval = undefined;
}
}
public override componentDidMount(): void {
const containerEl = this.containerRef.current;
const messagesEl = this.messagesRef.current;
const containerEl = this.#containerRef.current;
const messagesEl = this.#messagesRef.current;
const { conversationType, isConversationSelected } = this.props;
strictAssert(
// We don't render anything unless the conversation is selected
@ -544,31 +542,31 @@ export class Timeline extends React.Component<
'<Timeline> mounted without some refs'
);
this.updateIntersectionObserver();
this.#updateIntersectionObserver();
window.SignalContext.activeWindowService.registerForActive(
this.markNewestBottomVisibleMessageRead
this.#markNewestBottomVisibleMessageRead
);
if (conversationType === 'group') {
this.setupGroupCallPeekTimeouts();
this.#setupGroupCallPeekTimeouts();
}
}
public override componentWillUnmount(): void {
window.SignalContext.activeWindowService.unregisterForActive(
this.markNewestBottomVisibleMessageRead
this.#markNewestBottomVisibleMessageRead
);
this.intersectionObserver?.disconnect();
this.cleanupGroupCallPeekTimeouts();
this.#intersectionObserver?.disconnect();
this.#cleanupGroupCallPeekTimeouts();
this.props.updateVisibleMessages?.([]);
}
public override getSnapshotBeforeUpdate(
prevProps: Readonly<PropsType>
): SnapshotType {
const containerEl = this.containerRef.current;
const containerEl = this.#containerRef.current;
if (!containerEl) {
return null;
}
@ -579,7 +577,7 @@ export class Timeline extends React.Component<
const scrollAnchor = getScrollAnchorBeforeUpdate(
prevProps,
props,
this.isAtBottom()
this.#isAtBottom()
);
switch (scrollAnchor) {
@ -627,10 +625,10 @@ export class Timeline extends React.Component<
messageLoadingState,
} = this.props;
const containerEl = this.containerRef.current;
if (!this.scrollerLock.isLocked() && containerEl && snapshot) {
const containerEl = this.#containerRef.current;
if (!this.#scrollerLock.isLocked() && containerEl && snapshot) {
if (snapshot === scrollToUnreadIndicator) {
const lastSeenIndicatorEl = this.lastSeenIndicatorRef.current;
const lastSeenIndicatorEl = this.#lastSeenIndicatorRef.current;
if (lastSeenIndicatorEl) {
lastSeenIndicatorEl.scrollIntoView();
} else {
@ -641,7 +639,7 @@ export class Timeline extends React.Component<
);
}
} else if ('scrollToIndex' in snapshot) {
this.scrollToItemIndex(snapshot.scrollToIndex);
this.#scrollToItemIndex(snapshot.scrollToIndex);
} else if ('scrollTop' in snapshot) {
containerEl.scrollTop = snapshot.scrollTop;
} else {
@ -657,12 +655,12 @@ export class Timeline extends React.Component<
oldItems.at(-1) !== newItems.at(-1);
if (haveItemsChanged) {
this.updateIntersectionObserver();
this.#updateIntersectionObserver();
// This condition is somewhat arbitrary.
const numberToKeepAtBottom = this.maxVisibleRows * 2;
const numberToKeepAtBottom = this.#maxVisibleRows * 2;
const shouldDiscardOlderMessages: boolean =
this.isAtBottom() && newItems.length > numberToKeepAtBottom;
this.#isAtBottom() && newItems.length > numberToKeepAtBottom;
if (shouldDiscardOlderMessages) {
discardMessages({
conversationId: id,
@ -676,9 +674,9 @@ export class Timeline extends React.Component<
!messageLoadingState && previousMessageLoadingState
? previousMessageLoadingState
: undefined;
const numberToKeepAtTop = this.maxVisibleRows * 5;
const numberToKeepAtTop = this.#maxVisibleRows * 5;
const shouldDiscardNewerMessages: boolean =
!this.isAtBottom() &&
!this.#isAtBottom() &&
loadingStateThatJustFinished ===
TimelineMessageLoadingState.LoadingOlderMessages &&
newItems.length > numberToKeepAtTop;
@ -691,18 +689,18 @@ export class Timeline extends React.Component<
}
}
if (previousMessageChangeCounter !== messageChangeCounter) {
this.markNewestBottomVisibleMessageRead();
this.#markNewestBottomVisibleMessageRead();
}
if (previousConversationType !== conversationType) {
this.cleanupGroupCallPeekTimeouts();
this.#cleanupGroupCallPeekTimeouts();
if (conversationType === 'group') {
this.setupGroupCallPeekTimeouts();
this.#setupGroupCallPeekTimeouts();
}
}
}
private handleBlur = (event: React.FocusEvent): void => {
#handleBlur = (event: React.FocusEvent): void => {
const { clearTargetedMessage } = this.props;
const { currentTarget } = event;
@ -726,9 +724,7 @@ export class Timeline extends React.Component<
}, 0);
};
private handleKeyDown = (
event: React.KeyboardEvent<HTMLDivElement>
): void => {
#handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
const { targetMessage, targetedMessageId, items, id } = this.props;
const commandKey = get(window, 'platform') === 'darwin' && event.metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey;
@ -803,7 +799,7 @@ export class Timeline extends React.Component<
}
if (event.key === 'End' || (commandOrCtrl && event.key === 'ArrowDown')) {
this.scrollDown(true);
this.#scrollDown(true);
event.preventDefault();
event.stopPropagation();
}
@ -939,7 +935,7 @@ export class Timeline extends React.Component<
key="last seen indicator"
count={totalUnseen}
i18n={i18n}
ref={this.lastSeenIndicatorRef}
ref={this.#lastSeenIndicatorRef}
/>
);
} else if (oldestUnseenIndex === nextItemIndex) {
@ -964,7 +960,7 @@ export class Timeline extends React.Component<
>
<ErrorBoundary i18n={i18n} showDebugLog={showDebugLog}>
{renderItem({
containerElementRef: this.containerRef,
containerElementRef: this.#containerRef,
containerWidthBreakpoint: widthBreakpoint,
conversationId: id,
isBlocked,
@ -1098,7 +1094,7 @@ export class Timeline extends React.Component<
}
return (
<ScrollerLockContext.Provider value={this.scrollerLock}>
<ScrollerLockContext.Provider value={this.#scrollerLock}>
<SizeObserver
onSizeChange={size => {
const { isNearBottom } = this.props;
@ -1107,9 +1103,9 @@ export class Timeline extends React.Component<
widthBreakpoint: getWidthBreakpoint(size.width),
});
this.maxVisibleRows = Math.ceil(size.height / MIN_ROW_HEIGHT);
this.#maxVisibleRows = Math.ceil(size.height / MIN_ROW_HEIGHT);
const containerEl = this.containerRef.current;
const containerEl = this.#containerRef.current;
if (containerEl && isNearBottom) {
scrollToBottom(containerEl);
}
@ -1124,8 +1120,8 @@ export class Timeline extends React.Component<
)}
role="presentation"
tabIndex={-1}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
onBlur={this.#handleBlur}
onKeyDown={this.#handleKeyDown}
ref={ref}
>
{headerElements}
@ -1134,8 +1130,8 @@ export class Timeline extends React.Component<
<main
className="module-timeline__messages__container"
onScroll={this.onScroll}
ref={this.containerRef}
onScroll={this.#onScroll}
ref={this.#containerRef}
>
<div
className={classNames(
@ -1144,7 +1140,7 @@ export class Timeline extends React.Component<
haveOldest && 'module-timeline__messages--have-oldest',
scrollLocked && 'module-timeline__messages--scroll-locked'
)}
ref={this.messagesRef}
ref={this.#messagesRef}
role="list"
>
{haveOldest && (
@ -1162,7 +1158,7 @@ export class Timeline extends React.Component<
<div
className="module-timeline__messages__at-bottom-detector"
ref={this.atBottomDetectorRef}
ref={this.#atBottomDetectorRef}
style={AT_BOTTOM_DETECTOR_STYLE}
/>
</div>
@ -1181,7 +1177,7 @@ export class Timeline extends React.Component<
<ScrollDownButton
variant={ScrollDownButtonVariant.UNREAD_MESSAGES}
count={areUnreadBelowCurrentPosition ? unreadCount : 0}
onClick={this.onClickScrollDownButton}
onClick={this.#onClickScrollDownButton}
i18n={i18n}
/>
</div>

View file

@ -34,29 +34,24 @@ export type LeftPaneArchivePropsType =
| (LeftPaneArchiveBasePropsType & LeftPaneSearchPropsType);
export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsType> {
private readonly archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
private readonly isSearchingGlobally: boolean;
private readonly searchConversation: undefined | ConversationType;
private readonly searchTerm: string;
private readonly searchHelper: undefined | LeftPaneSearchHelper;
private readonly startSearchCounter: number;
readonly #archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
readonly #isSearchingGlobally: boolean;
readonly #searchConversation: undefined | ConversationType;
readonly #searchTerm: string;
readonly #searchHelper: undefined | LeftPaneSearchHelper;
readonly #startSearchCounter: number;
constructor(props: Readonly<LeftPaneArchivePropsType>) {
super();
this.archivedConversations = props.archivedConversations;
this.isSearchingGlobally = props.isSearchingGlobally;
this.searchConversation = props.searchConversation;
this.searchTerm = props.searchTerm;
this.startSearchCounter = props.startSearchCounter;
this.#archivedConversations = props.archivedConversations;
this.#isSearchingGlobally = props.isSearchingGlobally;
this.#searchConversation = props.searchConversation;
this.#searchTerm = props.searchTerm;
this.#startSearchCounter = props.startSearchCounter;
if ('conversationResults' in props) {
this.searchHelper = new LeftPaneSearchHelper(props);
this.#searchHelper = new LeftPaneSearchHelper(props);
}
}
@ -100,7 +95,7 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
updateSearchTerm: (searchTerm: string) => unknown;
showConversation: ShowConversationType;
}>): ReactChild | null {
if (!this.searchConversation) {
if (!this.#searchConversation) {
return null;
}
@ -111,11 +106,11 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
endConversationSearch={endConversationSearch}
endSearch={endSearch}
i18n={i18n}
isSearchingGlobally={this.isSearchingGlobally}
searchConversation={this.searchConversation}
searchTerm={this.searchTerm}
isSearchingGlobally={this.#isSearchingGlobally}
searchConversation={this.#searchConversation}
searchTerm={this.#searchTerm}
showConversation={showConversation}
startSearchCounter={this.startSearchCounter}
startSearchCounter={this.#startSearchCounter}
updateSearchTerm={updateSearchTerm}
/>
);
@ -128,8 +123,8 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
override getPreRowsNode({
i18n,
}: Readonly<{ i18n: LocalizerType }>): ReactChild | null {
if (this.searchHelper) {
return this.searchHelper.getPreRowsNode({ i18n });
if (this.#searchHelper) {
return this.#searchHelper.getPreRowsNode({ i18n });
}
return (
@ -143,16 +138,16 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
getRowCount(): number {
return (
this.searchHelper?.getRowCount() ?? this.archivedConversations.length
this.#searchHelper?.getRowCount() ?? this.#archivedConversations.length
);
}
getRow(rowIndex: number): undefined | Row {
if (this.searchHelper) {
return this.searchHelper.getRow(rowIndex);
if (this.#searchHelper) {
return this.#searchHelper.getRow(rowIndex);
}
const conversation = this.archivedConversations[rowIndex];
const conversation = this.#archivedConversations[rowIndex];
return conversation
? {
type: RowType.Conversation,
@ -164,14 +159,14 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
override getRowIndexToScrollTo(
selectedConversationId: undefined | string
): undefined | number {
if (this.searchHelper) {
return this.searchHelper.getRowIndexToScrollTo(selectedConversationId);
if (this.#searchHelper) {
return this.#searchHelper.getRowIndexToScrollTo(selectedConversationId);
}
if (!selectedConversationId) {
return undefined;
}
const result = this.archivedConversations.findIndex(
const result = this.#archivedConversations.findIndex(
conversation => conversation.id === selectedConversationId
);
return result === -1 ? undefined : result;
@ -180,7 +175,8 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
getConversationAndMessageAtIndex(
conversationIndex: number
): undefined | { conversationId: string } {
const { archivedConversations, searchHelper } = this;
const searchHelper = this.#searchHelper;
const archivedConversations = this.#archivedConversations;
if (searchHelper) {
return searchHelper.getConversationAndMessageAtIndex(conversationIndex);
@ -196,8 +192,8 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
selectedConversationId: undefined | string,
targetedMessageId: unknown
): undefined | { conversationId: string } {
if (this.searchHelper) {
return this.searchHelper.getConversationAndMessageInDirection(
if (this.#searchHelper) {
return this.#searchHelper.getConversationAndMessageInDirection(
toFind,
selectedConversationId,
targetedMessageId
@ -205,7 +201,7 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
}
return getConversationInDirection(
this.archivedConversations,
this.#archivedConversations,
toFind,
selectedConversationId
);
@ -213,13 +209,13 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
shouldRecomputeRowHeights(old: Readonly<LeftPaneArchivePropsType>): boolean {
const hasSearchingChanged =
'conversationResults' in old !== Boolean(this.searchHelper);
'conversationResults' in old !== Boolean(this.#searchHelper);
if (hasSearchingChanged) {
return true;
}
if ('conversationResults' in old && this.searchHelper) {
return this.searchHelper.shouldRecomputeRowHeights(old);
if ('conversationResults' in old && this.#searchHelper) {
return this.#searchHelper.shouldRecomputeRowHeights(old);
}
return false;
@ -251,7 +247,9 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
!commandAndCtrl &&
shiftKey &&
(key === 'f' || key === 'F') &&
this.archivedConversations.some(({ id }) => id === selectedConversationId)
this.#archivedConversations.some(
({ id }) => id === selectedConversationId
)
) {
searchInConversation(selectedConversationId);

View file

@ -42,31 +42,19 @@ export type LeftPaneChooseGroupMembersPropsType = {
};
export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneChooseGroupMembersPropsType> {
private readonly candidateContacts: ReadonlyArray<ConversationType>;
private readonly isPhoneNumberChecked: boolean;
private readonly isUsernameChecked: boolean;
private readonly isShowingMaximumGroupSizeModal: boolean;
private readonly isShowingRecommendedGroupSizeModal: boolean;
private readonly groupSizeRecommendedLimit: number;
private readonly groupSizeHardLimit: number;
private readonly searchTerm: string;
private readonly phoneNumber: ParsedE164Type | undefined;
private readonly username: string | undefined;
private readonly selectedContacts: Array<ConversationType>;
private readonly selectedConversationIdsSet: Set<string>;
private readonly uuidFetchState: UUIDFetchStateType;
readonly #candidateContacts: ReadonlyArray<ConversationType>;
readonly #isPhoneNumberChecked: boolean;
readonly #isUsernameChecked: boolean;
readonly #isShowingMaximumGroupSizeModal: boolean;
readonly #isShowingRecommendedGroupSizeModal: boolean;
readonly #groupSizeRecommendedLimit: number;
readonly #groupSizeHardLimit: number;
readonly #searchTerm: string;
readonly #phoneNumber: ParsedE164Type | undefined;
readonly #username: string | undefined;
readonly #selectedContacts: Array<ConversationType>;
readonly #selectedConversationIdsSet: Set<string>;
readonly #uuidFetchState: UUIDFetchStateType;
constructor({
candidateContacts,
@ -84,27 +72,27 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
}: Readonly<LeftPaneChooseGroupMembersPropsType>) {
super();
this.uuidFetchState = uuidFetchState;
this.groupSizeRecommendedLimit = groupSizeRecommendedLimit - 1;
this.groupSizeHardLimit = groupSizeHardLimit - 1;
this.#uuidFetchState = uuidFetchState;
this.#groupSizeRecommendedLimit = groupSizeRecommendedLimit - 1;
this.#groupSizeHardLimit = groupSizeHardLimit - 1;
this.candidateContacts = candidateContacts;
this.isShowingMaximumGroupSizeModal = isShowingMaximumGroupSizeModal;
this.isShowingRecommendedGroupSizeModal =
this.#candidateContacts = candidateContacts;
this.#isShowingMaximumGroupSizeModal = isShowingMaximumGroupSizeModal;
this.#isShowingRecommendedGroupSizeModal =
isShowingRecommendedGroupSizeModal;
this.searchTerm = searchTerm;
this.#searchTerm = searchTerm;
const isUsernameVisible =
username !== undefined &&
username !== ourUsername &&
this.candidateContacts.every(contact => contact.username !== username);
this.#candidateContacts.every(contact => contact.username !== username);
if (isUsernameVisible) {
this.username = username;
this.#username = username;
}
this.isUsernameChecked = selectedContacts.some(
contact => contact.username === this.username
this.#isUsernameChecked = selectedContacts.some(
contact => contact.username === this.#username
);
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
@ -114,22 +102,22 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
phoneNumber
) {
const { e164 } = phoneNumber;
this.isPhoneNumberChecked =
this.#isPhoneNumberChecked =
phoneNumber.isValid &&
selectedContacts.some(contact => contact.e164 === e164);
const isVisible =
e164 !== ourE164 &&
this.candidateContacts.every(contact => contact.e164 !== e164);
this.#candidateContacts.every(contact => contact.e164 !== e164);
if (isVisible) {
this.phoneNumber = phoneNumber;
this.#phoneNumber = phoneNumber;
}
} else {
this.isPhoneNumberChecked = false;
this.#isPhoneNumberChecked = false;
}
this.selectedContacts = selectedContacts;
this.#selectedContacts = selectedContacts;
this.selectedConversationIdsSet = new Set(
this.#selectedConversationIdsSet = new Set(
selectedContacts.map(contact => contact.id)
);
}
@ -183,7 +171,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
onChange={onChangeComposeSearchTerm}
placeholder={i18n('icu:contactSearchPlaceholder')}
ref={focusRef}
value={this.searchTerm}
value={this.#searchTerm}
/>
);
}
@ -200,20 +188,20 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
removeSelectedContact: (conversationId: string) => unknown;
}>): ReactChild {
let modalNode: undefined | ReactChild;
if (this.isShowingMaximumGroupSizeModal) {
if (this.#isShowingMaximumGroupSizeModal) {
modalNode = (
<AddGroupMemberErrorDialog
i18n={i18n}
maximumNumberOfContacts={this.groupSizeHardLimit}
maximumNumberOfContacts={this.#groupSizeHardLimit}
mode={AddGroupMemberErrorDialogMode.MaximumGroupSize}
onClose={closeMaximumGroupSizeModal}
/>
);
} else if (this.isShowingRecommendedGroupSizeModal) {
} else if (this.#isShowingRecommendedGroupSizeModal) {
modalNode = (
<AddGroupMemberErrorDialog
i18n={i18n}
recommendedMaximumNumberOfContacts={this.groupSizeRecommendedLimit}
recommendedMaximumNumberOfContacts={this.#groupSizeRecommendedLimit}
mode={AddGroupMemberErrorDialogMode.RecommendedMaximumGroupSize}
onClose={closeRecommendedGroupSizeModal}
/>
@ -222,9 +210,9 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
return (
<>
{Boolean(this.selectedContacts.length) && (
{Boolean(this.#selectedContacts.length) && (
<ContactPills>
{this.selectedContacts.map(contact => (
{this.#selectedContacts.map(contact => (
<ContactPill
key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest}
@ -264,10 +252,10 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
}>): ReactChild {
return (
<Button
disabled={this.hasExceededMaximumNumberOfContacts()}
disabled={this.#hasExceededMaximumNumberOfContacts()}
onClick={startSettingGroupMetadata}
>
{this.selectedContacts.length
{this.#selectedContacts.length
? i18n('icu:chooseGroupMembers__next')
: i18n('icu:chooseGroupMembers__skip')}
</Button>
@ -278,18 +266,18 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
let rowCount = 0;
// Header + Phone Number
if (this.phoneNumber) {
if (this.#phoneNumber) {
rowCount += 2;
}
// Header + Username
if (this.username) {
if (this.#username) {
rowCount += 2;
}
// Header + Contacts
if (this.candidateContacts.length) {
rowCount += 1 + this.candidateContacts.length;
if (this.#candidateContacts.length) {
rowCount += 1 + this.#candidateContacts.length;
}
// Footer
@ -301,7 +289,11 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
}
getRow(actualRowIndex: number): undefined | Row {
if (!this.candidateContacts.length && !this.phoneNumber && !this.username) {
if (
!this.#candidateContacts.length &&
!this.#phoneNumber &&
!this.#username
) {
return undefined;
}
@ -314,7 +306,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
let virtualRowIndex = actualRowIndex;
if (this.candidateContacts.length) {
if (this.#candidateContacts.length) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
@ -322,12 +314,12 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
};
}
if (virtualRowIndex <= this.candidateContacts.length) {
const contact = this.candidateContacts[virtualRowIndex - 1];
if (virtualRowIndex <= this.#candidateContacts.length) {
const contact = this.#candidateContacts[virtualRowIndex - 1];
const isChecked = this.selectedConversationIdsSet.has(contact.id);
const isChecked = this.#selectedConversationIdsSet.has(contact.id);
const disabledReason =
!isChecked && this.hasSelectedMaximumNumberOfContacts()
!isChecked && this.#hasSelectedMaximumNumberOfContacts()
? ContactCheckboxDisabledReason.MaximumContactsSelected
: undefined;
@ -339,10 +331,10 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
};
}
virtualRowIndex -= 1 + this.candidateContacts.length;
virtualRowIndex -= 1 + this.#candidateContacts.length;
}
if (this.phoneNumber) {
if (this.#phoneNumber) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
@ -352,18 +344,18 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
if (virtualRowIndex === 1) {
return {
type: RowType.PhoneNumberCheckbox,
isChecked: this.isPhoneNumberChecked,
isChecked: this.#isPhoneNumberChecked,
isFetching: isFetchingByE164(
this.uuidFetchState,
this.phoneNumber.e164
this.#uuidFetchState,
this.#phoneNumber.e164
),
phoneNumber: this.phoneNumber,
phoneNumber: this.#phoneNumber,
};
}
virtualRowIndex -= 2;
}
if (this.username) {
if (this.#username) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
@ -373,9 +365,12 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
if (virtualRowIndex === 1) {
return {
type: RowType.UsernameCheckbox,
isChecked: this.isUsernameChecked,
isFetching: isFetchingByUsername(this.uuidFetchState, this.username),
username: this.username,
isChecked: this.#isUsernameChecked,
isFetching: isFetchingByUsername(
this.#uuidFetchState,
this.#username
),
username: this.#username,
};
}
virtualRowIndex -= 2;
@ -402,13 +397,13 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
return false;
}
private hasSelectedMaximumNumberOfContacts(): boolean {
return this.selectedContacts.length >= this.groupSizeHardLimit;
#hasSelectedMaximumNumberOfContacts(): boolean {
return this.#selectedContacts.length >= this.#groupSizeHardLimit;
}
private hasExceededMaximumNumberOfContacts(): boolean {
#hasExceededMaximumNumberOfContacts(): boolean {
// It should be impossible to reach this state. This is here as a failsafe.
return this.selectedContacts.length > this.groupSizeHardLimit;
return this.#selectedContacts.length > this.#groupSizeHardLimit;
}
}

View file

@ -35,21 +35,14 @@ enum TopButtons {
}
export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsType> {
private readonly composeContacts: ReadonlyArray<ContactListItemConversationType>;
private readonly composeGroups: ReadonlyArray<GroupListItemConversationType>;
private readonly uuidFetchState: UUIDFetchStateType;
private readonly searchTerm: string;
private readonly phoneNumber: ParsedE164Type | undefined;
private readonly isPhoneNumberVisible: boolean;
private readonly username: string | undefined;
private readonly isUsernameVisible: boolean;
readonly #composeContacts: ReadonlyArray<ContactListItemConversationType>;
readonly #composeGroups: ReadonlyArray<GroupListItemConversationType>;
readonly #uuidFetchState: UUIDFetchStateType;
readonly #searchTerm: string;
readonly #phoneNumber: ParsedE164Type | undefined;
readonly #isPhoneNumberVisible: boolean;
readonly #username: string | undefined;
readonly #isUsernameVisible: boolean;
constructor({
composeContacts,
@ -61,24 +54,24 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
}: Readonly<LeftPaneComposePropsType>) {
super();
this.composeContacts = composeContacts;
this.composeGroups = composeGroups;
this.searchTerm = searchTerm;
this.uuidFetchState = uuidFetchState;
this.#composeContacts = composeContacts;
this.#composeGroups = composeGroups;
this.#searchTerm = searchTerm;
this.#uuidFetchState = uuidFetchState;
this.username = username;
this.isUsernameVisible =
this.#username = username;
this.#isUsernameVisible =
Boolean(username) &&
this.composeContacts.every(contact => contact.username !== username);
this.#composeContacts.every(contact => contact.username !== username);
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
if (!username && phoneNumber) {
this.phoneNumber = phoneNumber;
this.isPhoneNumberVisible = this.composeContacts.every(
this.#phoneNumber = phoneNumber;
this.#isPhoneNumberVisible = this.#composeContacts.every(
contact => contact.e164 !== phoneNumber.e164
);
} else {
this.isPhoneNumberVisible = false;
this.#isPhoneNumberVisible = false;
}
}
@ -125,7 +118,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
onChange={onChangeComposeSearchTerm}
placeholder={i18n('icu:contactSearchPlaceholder')}
ref={focusRef}
value={this.searchTerm}
value={this.#searchTerm}
/>
);
}
@ -143,20 +136,20 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
}
getRowCount(): number {
let result = this.composeContacts.length + this.composeGroups.length;
if (this.hasTopButtons()) {
let result = this.#composeContacts.length + this.#composeGroups.length;
if (this.#hasTopButtons()) {
result += 3;
}
if (this.hasContactsHeader()) {
if (this.#hasContactsHeader()) {
result += 1;
}
if (this.hasGroupsHeader()) {
if (this.#hasGroupsHeader()) {
result += 1;
}
if (this.isUsernameVisible) {
if (this.#isUsernameVisible) {
result += 2;
}
if (this.isPhoneNumberVisible) {
if (this.#isPhoneNumberVisible) {
result += 2;
}
@ -165,7 +158,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
getRow(actualRowIndex: number): undefined | Row {
let virtualRowIndex = actualRowIndex;
if (this.hasTopButtons()) {
if (this.#hasTopButtons()) {
if (virtualRowIndex === 0) {
return { type: RowType.CreateNewGroup };
}
@ -179,7 +172,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
virtualRowIndex -= 3;
}
if (this.hasContactsHeader()) {
if (this.#hasContactsHeader()) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
@ -189,7 +182,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
virtualRowIndex -= 1;
const contact = this.composeContacts[virtualRowIndex];
const contact = this.#composeContacts[virtualRowIndex];
if (contact) {
return {
type: RowType.Contact,
@ -198,10 +191,10 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
};
}
virtualRowIndex -= this.composeContacts.length;
virtualRowIndex -= this.#composeContacts.length;
}
if (this.hasGroupsHeader()) {
if (this.#hasGroupsHeader()) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
@ -211,7 +204,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
virtualRowIndex -= 1;
const group = this.composeGroups[virtualRowIndex];
const group = this.#composeGroups[virtualRowIndex];
if (group) {
return {
type: RowType.SelectSingleGroup,
@ -219,10 +212,10 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
};
}
virtualRowIndex -= this.composeGroups.length;
virtualRowIndex -= this.#composeGroups.length;
}
if (this.username && this.isUsernameVisible) {
if (this.#username && this.#isUsernameVisible) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
@ -235,16 +228,16 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
if (virtualRowIndex === 0) {
return {
type: RowType.UsernameSearchResult,
username: this.username,
username: this.#username,
isFetchingUsername: isFetchingByUsername(
this.uuidFetchState,
this.username
this.#uuidFetchState,
this.#username
),
};
}
}
if (this.phoneNumber && this.isPhoneNumberVisible) {
if (this.#phoneNumber && this.#isPhoneNumberVisible) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
@ -257,10 +250,10 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
if (virtualRowIndex === 0) {
return {
type: RowType.StartNewConversation,
phoneNumber: this.phoneNumber,
phoneNumber: this.#phoneNumber,
isFetching: isFetchingByE164(
this.uuidFetchState,
this.phoneNumber.e164
this.#uuidFetchState,
this.#phoneNumber.e164
),
};
}
@ -287,8 +280,8 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
exProps: Readonly<LeftPaneComposePropsType>
): boolean {
const prev = new LeftPaneComposeHelper(exProps);
const currHeaderIndices = this.getHeaderIndices();
const prevHeaderIndices = prev.getHeaderIndices();
const currHeaderIndices = this.#getHeaderIndices();
const prevHeaderIndices = prev.#getHeaderIndices();
return (
currHeaderIndices.top !== prevHeaderIndices.top ||
@ -299,26 +292,26 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
);
}
private getTopButtons(): TopButtons {
if (this.searchTerm) {
#getTopButtons(): TopButtons {
if (this.#searchTerm) {
return TopButtons.None;
}
return TopButtons.Visible;
}
private hasTopButtons(): boolean {
return this.getTopButtons() !== TopButtons.None;
#hasTopButtons(): boolean {
return this.#getTopButtons() !== TopButtons.None;
}
private hasContactsHeader(): boolean {
return Boolean(this.composeContacts.length);
#hasContactsHeader(): boolean {
return Boolean(this.#composeContacts.length);
}
private hasGroupsHeader(): boolean {
return Boolean(this.composeGroups.length);
#hasGroupsHeader(): boolean {
return Boolean(this.#composeGroups.length);
}
private getHeaderIndices(): {
#getHeaderIndices(): {
top?: number;
contact?: number;
group?: number;
@ -333,22 +326,22 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
let rowCount = 0;
if (this.hasTopButtons()) {
if (this.#hasTopButtons()) {
top = 0;
rowCount += 3;
}
if (this.hasContactsHeader()) {
if (this.#hasContactsHeader()) {
contact = rowCount;
rowCount += this.composeContacts.length;
rowCount += this.#composeContacts.length;
}
if (this.hasGroupsHeader()) {
if (this.#hasGroupsHeader()) {
group = rowCount;
rowCount += this.composeContacts.length;
rowCount += this.#composeContacts.length;
}
if (this.phoneNumber) {
if (this.#phoneNumber) {
phoneNumber = rowCount;
}
if (this.username) {
if (this.#username) {
username = rowCount;
}

View file

@ -36,17 +36,12 @@ type DoLookupActionsType = Readonly<{
LookupConversationWithoutServiceIdActionsType;
export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFindByPhoneNumberPropsType> {
private readonly searchTerm: string;
private readonly phoneNumber: ParsedE164Type | undefined;
private readonly regionCode: string | undefined;
private readonly uuidFetchState: UUIDFetchStateType;
private readonly countries: ReadonlyArray<CountryDataType>;
private readonly selectedRegion: string;
readonly #searchTerm: string;
readonly #phoneNumber: ParsedE164Type | undefined;
readonly #regionCode: string | undefined;
readonly #uuidFetchState: UUIDFetchStateType;
readonly #countries: ReadonlyArray<CountryDataType>;
readonly #selectedRegion: string;
constructor({
searchTerm,
@ -57,14 +52,14 @@ export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFind
}: Readonly<LeftPaneFindByPhoneNumberPropsType>) {
super();
this.searchTerm = searchTerm;
this.uuidFetchState = uuidFetchState;
this.regionCode = regionCode;
this.countries = countries;
this.selectedRegion = selectedRegion;
this.#searchTerm = searchTerm;
this.#uuidFetchState = uuidFetchState;
this.#regionCode = regionCode;
this.#countries = countries;
this.#selectedRegion = selectedRegion;
this.phoneNumber = parseAndFormatPhoneNumber(
this.searchTerm,
this.#phoneNumber = parseAndFormatPhoneNumber(
this.#searchTerm,
selectedRegion || regionCode
);
}
@ -83,7 +78,7 @@ export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFind
<button
aria-label={backButtonLabel}
className="module-left-pane__header__contents__back-button"
disabled={this.isFetching()}
disabled={this.#isFetching()}
onClick={this.getBackAction({ startComposing })}
title={backButtonLabel}
type="button"
@ -100,7 +95,7 @@ export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFind
}: {
startComposing: () => void;
}): undefined | (() => void) {
return this.isFetching() ? undefined : startComposing;
return this.#isFetching() ? undefined : startComposing;
}
override getSearchInput({
@ -122,25 +117,25 @@ export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFind
return (
<div className="LeftPaneFindByPhoneNumberHelper__container">
<CountryCodeSelect
countries={this.countries}
countries={this.#countries}
i18n={i18n}
defaultRegion={this.regionCode ?? ''}
value={this.selectedRegion}
defaultRegion={this.#regionCode ?? ''}
value={this.#selectedRegion}
onChange={onChangeComposeSelectedRegion}
/>
<SearchInput
hasSearchIcon={false}
disabled={this.isFetching()}
disabled={this.#isFetching()}
i18n={i18n}
moduleClassName="LeftPaneFindByPhoneNumberHelper__search-input"
onChange={onChangeComposeSearchTerm}
placeholder={placeholder}
ref={focusRef}
value={this.searchTerm}
value={this.#searchTerm}
onKeyDown={ev => {
if (ev.key === 'Enter') {
drop(this.doLookup(lookupActions));
drop(this.#doLookup(lookupActions));
}
}}
/>
@ -157,10 +152,10 @@ export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFind
DoLookupActionsType): ReactChild {
return (
<Button
disabled={this.isLookupDisabled()}
onClick={() => drop(this.doLookup(lookupActions))}
disabled={this.#isLookupDisabled()}
onClick={() => drop(this.#doLookup(lookupActions))}
>
{this.isFetching() ? (
{this.#isFetching() ? (
<span aria-label={i18n('icu:loading')} role="status">
<Spinner size="20px" svgSize="small" direction="on-avatar" />
</span>
@ -198,14 +193,14 @@ export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFind
return false;
}
private async doLookup({
async #doLookup({
lookupConversationWithoutServiceId,
showUserNotFoundModal,
setIsFetchingUUID,
showInbox,
showConversation,
}: DoLookupActionsType): Promise<void> {
if (!this.phoneNumber || this.isLookupDisabled()) {
if (!this.#phoneNumber || this.#isLookupDisabled()) {
return;
}
@ -213,8 +208,8 @@ export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFind
showUserNotFoundModal,
setIsFetchingUUID,
type: 'e164',
e164: this.phoneNumber.e164,
phoneNumber: this.searchTerm,
e164: this.#phoneNumber.e164,
phoneNumber: this.#searchTerm,
});
if (conversationId != null) {
@ -223,20 +218,20 @@ export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFind
}
}
private isFetching(): boolean {
if (this.phoneNumber != null) {
return isFetchingByE164(this.uuidFetchState, this.phoneNumber.e164);
#isFetching(): boolean {
if (this.#phoneNumber != null) {
return isFetchingByE164(this.#uuidFetchState, this.#phoneNumber.e164);
}
return false;
}
private isLookupDisabled(): boolean {
if (this.isFetching()) {
#isLookupDisabled(): boolean {
if (this.#isFetching()) {
return true;
}
return !this.phoneNumber?.isValid;
return !this.#phoneNumber?.isValid;
}
}

View file

@ -30,11 +30,9 @@ type DoLookupActionsType = Readonly<{
LookupConversationWithoutServiceIdActionsType;
export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByUsernamePropsType> {
private readonly searchTerm: string;
private readonly username: string | undefined;
private readonly uuidFetchState: UUIDFetchStateType;
readonly #searchTerm: string;
readonly #username: string | undefined;
readonly #uuidFetchState: UUIDFetchStateType;
constructor({
searchTerm,
@ -43,10 +41,10 @@ export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByU
}: Readonly<LeftPaneFindByUsernamePropsType>) {
super();
this.searchTerm = searchTerm;
this.uuidFetchState = uuidFetchState;
this.#searchTerm = searchTerm;
this.#uuidFetchState = uuidFetchState;
this.username = username;
this.#username = username;
}
override getHeaderContents({
@ -63,7 +61,7 @@ export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByU
<button
aria-label={backButtonLabel}
className="module-left-pane__header__contents__back-button"
disabled={this.isFetching()}
disabled={this.#isFetching()}
onClick={this.getBackAction({ startComposing })}
title={backButtonLabel}
type="button"
@ -80,7 +78,7 @@ export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByU
}: {
startComposing: () => void;
}): undefined | (() => void) {
return this.isFetching() ? undefined : startComposing;
return this.#isFetching() ? undefined : startComposing;
}
override getSearchInput({
@ -103,17 +101,17 @@ export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByU
return (
<SearchInput
hasSearchIcon={false}
disabled={this.isFetching()}
disabled={this.#isFetching()}
i18n={i18n}
moduleClassName="LeftPaneFindByUsernameHelper__search-input"
onChange={onChangeComposeSearchTerm}
placeholder={placeholder}
ref={focusRef}
value={this.searchTerm}
value={this.#searchTerm}
description={description}
onKeyDown={ev => {
if (ev.key === 'Enter') {
drop(this.doLookup(lookupActions));
drop(this.#doLookup(lookupActions));
}
}}
/>
@ -129,10 +127,10 @@ export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByU
DoLookupActionsType): ReactChild {
return (
<Button
disabled={this.isLookupDisabled()}
onClick={() => drop(this.doLookup(lookupActions))}
disabled={this.#isLookupDisabled()}
onClick={() => drop(this.#doLookup(lookupActions))}
>
{this.isFetching() ? (
{this.#isFetching() ? (
<span aria-label={i18n('icu:loading')} role="status">
<Spinner size="20px" svgSize="small" direction="on-avatar" />
</span>
@ -170,14 +168,14 @@ export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByU
return false;
}
private async doLookup({
async #doLookup({
lookupConversationWithoutServiceId,
showUserNotFoundModal,
setIsFetchingUUID,
showInbox,
showConversation,
}: DoLookupActionsType): Promise<void> {
if (!this.username || this.isLookupDisabled()) {
if (!this.#username || this.#isLookupDisabled()) {
return;
}
@ -185,7 +183,7 @@ export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByU
showUserNotFoundModal,
setIsFetchingUUID,
type: 'username',
username: this.username,
username: this.#username,
});
if (conversationId != null) {
@ -194,20 +192,20 @@ export class LeftPaneFindByUsernameHelper extends LeftPaneHelper<LeftPaneFindByU
}
}
private isFetching(): boolean {
if (this.username != null) {
return isFetchingByUsername(this.uuidFetchState, this.username);
#isFetching(): boolean {
if (this.#username != null) {
return isFetchingByUsername(this.#uuidFetchState, this.#username);
}
return false;
}
private isLookupDisabled(): boolean {
if (this.isFetching()) {
#isLookupDisabled(): boolean {
if (this.#isFetching()) {
return true;
}
return this.username == null;
return this.#username == null;
}
}

View file

@ -34,25 +34,16 @@ export type LeftPaneInboxPropsType = {
};
export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType> {
private readonly conversations: ReadonlyArray<ConversationListItemPropsType>;
private readonly archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
private readonly pinnedConversations: ReadonlyArray<ConversationListItemPropsType>;
private readonly isAboutToSearch: boolean;
private readonly isSearchingGlobally: boolean;
private readonly startSearchCounter: number;
private readonly searchDisabled: boolean;
private readonly searchTerm: string;
private readonly searchConversation: undefined | ConversationType;
private readonly filterByUnread: boolean;
readonly #conversations: ReadonlyArray<ConversationListItemPropsType>;
readonly #archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
readonly #pinnedConversations: ReadonlyArray<ConversationListItemPropsType>;
readonly #isAboutToSearch: boolean;
readonly #isSearchingGlobally: boolean;
readonly #startSearchCounter: number;
readonly #searchDisabled: boolean;
readonly #searchTerm: string;
readonly #searchConversation: undefined | ConversationType;
readonly #filterByUnread: boolean;
constructor({
conversations,
@ -68,25 +59,25 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
}: Readonly<LeftPaneInboxPropsType>) {
super();
this.conversations = conversations;
this.archivedConversations = archivedConversations;
this.pinnedConversations = pinnedConversations;
this.isAboutToSearch = isAboutToSearch;
this.isSearchingGlobally = isSearchingGlobally;
this.startSearchCounter = startSearchCounter;
this.searchDisabled = searchDisabled;
this.searchTerm = searchTerm;
this.searchConversation = searchConversation;
this.filterByUnread = filterByUnread;
this.#conversations = conversations;
this.#archivedConversations = archivedConversations;
this.#pinnedConversations = pinnedConversations;
this.#isAboutToSearch = isAboutToSearch;
this.#isSearchingGlobally = isSearchingGlobally;
this.#startSearchCounter = startSearchCounter;
this.#searchDisabled = searchDisabled;
this.#searchTerm = searchTerm;
this.#searchConversation = searchConversation;
this.#filterByUnread = filterByUnread;
}
getRowCount(): number {
const headerCount = this.hasPinnedAndNonpinned() ? 2 : 0;
const buttonCount = this.archivedConversations.length ? 1 : 0;
const headerCount = this.#hasPinnedAndNonpinned() ? 2 : 0;
const buttonCount = this.#archivedConversations.length ? 1 : 0;
return (
headerCount +
this.pinnedConversations.length +
this.conversations.length +
this.#pinnedConversations.length +
this.#conversations.length +
buttonCount
);
}
@ -116,17 +107,17 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
clearSearchQuery={clearSearchQuery}
endConversationSearch={endConversationSearch}
endSearch={endSearch}
disabled={this.searchDisabled}
disabled={this.#searchDisabled}
i18n={i18n}
isSearchingGlobally={this.isSearchingGlobally}
searchConversation={this.searchConversation}
searchTerm={this.searchTerm}
isSearchingGlobally={this.#isSearchingGlobally}
searchConversation={this.#searchConversation}
searchTerm={this.#searchTerm}
showConversation={showConversation}
startSearchCounter={this.startSearchCounter}
startSearchCounter={this.#startSearchCounter}
updateSearchTerm={updateSearchTerm}
onFilterClick={updateFilterByUnread}
filterButtonEnabled={!this.searchConversation}
filterPressed={this.filterByUnread}
filterButtonEnabled={!this.#searchConversation}
filterPressed={this.#filterByUnread}
/>
);
}
@ -149,11 +140,13 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
}
getRow(rowIndex: number): undefined | Row {
const { conversations, archivedConversations, pinnedConversations } = this;
const pinnedConversations = this.#pinnedConversations;
const archivedConversations = this.#archivedConversations;
const conversations = this.#conversations;
const archivedConversationsCount = archivedConversations.length;
if (this.hasPinnedAndNonpinned()) {
if (this.#hasPinnedAndNonpinned()) {
switch (rowIndex) {
case 0:
return {
@ -226,9 +219,9 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
const isConversationSelected = (
conversation: Readonly<ConversationListItemPropsType>
) => conversation.id === selectedConversationId;
const hasHeaders = this.hasPinnedAndNonpinned();
const hasHeaders = this.#hasPinnedAndNonpinned();
const pinnedConversationIndex = this.pinnedConversations.findIndex(
const pinnedConversationIndex = this.#pinnedConversations.findIndex(
isConversationSelected
);
if (pinnedConversationIndex !== -1) {
@ -236,11 +229,11 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
return pinnedConversationIndex + headerOffset;
}
const conversationIndex = this.conversations.findIndex(
const conversationIndex = this.#conversations.findIndex(
isConversationSelected
);
if (conversationIndex !== -1) {
const pinnedOffset = this.pinnedConversations.length;
const pinnedOffset = this.#pinnedConversations.length;
const headerOffset = hasHeaders ? 2 : 0;
return conversationIndex + pinnedOffset + headerOffset;
}
@ -250,20 +243,21 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
override requiresFullWidth(): boolean {
const hasNoConversations =
!this.conversations.length &&
!this.pinnedConversations.length &&
!this.archivedConversations.length;
return hasNoConversations || this.isAboutToSearch;
!this.#conversations.length &&
!this.#pinnedConversations.length &&
!this.#archivedConversations.length;
return hasNoConversations || this.#isAboutToSearch;
}
shouldRecomputeRowHeights(old: Readonly<LeftPaneInboxPropsType>): boolean {
return old.pinnedConversations.length !== this.pinnedConversations.length;
return old.pinnedConversations.length !== this.#pinnedConversations.length;
}
getConversationAndMessageAtIndex(
conversationIndex: number
): undefined | { conversationId: string } {
const { conversations, pinnedConversations } = this;
const pinnedConversations = this.#pinnedConversations;
const conversations = this.#conversations;
const conversation =
pinnedConversations[conversationIndex] ||
conversations[conversationIndex - pinnedConversations.length] ||
@ -278,7 +272,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
_targetedMessageId: unknown
): undefined | { conversationId: string } {
return getConversationInDirection(
[...this.pinnedConversations, ...this.conversations],
[...this.#pinnedConversations, ...this.#conversations],
toFind,
selectedConversationId
);
@ -295,9 +289,9 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
handleKeydownForSearch(event, options);
}
private hasPinnedAndNonpinned(): boolean {
#hasPinnedAndNonpinned(): boolean {
return Boolean(
this.pinnedConversations.length && this.conversations.length
this.#pinnedConversations.length && this.#conversations.length
);
}
}

View file

@ -50,36 +50,24 @@ export type LeftPaneSearchPropsType = {
searchConversation: undefined | ConversationType;
};
const searchResultKeys: Array<
'conversationResults' | 'contactResults' | 'messageResults'
> = ['conversationResults', 'contactResults', 'messageResults'];
export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType> {
private readonly conversationResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;
readonly #conversationResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;
readonly #contactResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;
readonly #isSearchingGlobally: boolean;
private readonly contactResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;
private readonly isSearchingGlobally: boolean;
private readonly messageResults: MaybeLoadedSearchResultsType<{
readonly #messageResults: MaybeLoadedSearchResultsType<{
id: string;
conversationId: string;
type: string;
}>;
private readonly searchConversationName?: string;
private readonly primarySendsSms: boolean;
private readonly searchTerm: string;
private readonly startSearchCounter: number;
private readonly searchDisabled: boolean;
private readonly searchConversation: undefined | ConversationType;
private readonly filterByUnread: boolean;
readonly #searchConversationName?: string;
readonly #primarySendsSms: boolean;
readonly #searchTerm: string;
readonly #startSearchCounter: number;
readonly #searchDisabled: boolean;
readonly #searchConversation: undefined | ConversationType;
readonly #filterByUnread: boolean;
constructor({
contactResults,
@ -96,18 +84,17 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
}: Readonly<LeftPaneSearchPropsType>) {
super();
this.contactResults = contactResults;
this.conversationResults = conversationResults;
this.isSearchingGlobally = isSearchingGlobally;
this.messageResults = messageResults;
this.primarySendsSms = primarySendsSms;
this.searchConversation = searchConversation;
this.searchConversationName = searchConversationName;
this.searchDisabled = searchDisabled;
this.searchTerm = searchTerm;
this.startSearchCounter = startSearchCounter;
this.filterByUnread = filterByUnread;
this.onEnterKeyDown = this.onEnterKeyDown.bind(this);
this.#contactResults = contactResults;
this.#conversationResults = conversationResults;
this.#isSearchingGlobally = isSearchingGlobally;
this.#messageResults = messageResults;
this.#primarySendsSms = primarySendsSms;
this.#searchConversation = searchConversation;
this.#searchConversationName = searchConversationName;
this.#searchDisabled = searchDisabled;
this.#searchTerm = searchTerm;
this.#startSearchCounter = startSearchCounter;
this.#filterByUnread = filterByUnread;
}
override getSearchInput({
@ -135,17 +122,17 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
clearSearchQuery={clearSearchQuery}
endConversationSearch={endConversationSearch}
endSearch={endSearch}
disabled={this.searchDisabled}
disabled={this.#searchDisabled}
i18n={i18n}
isSearchingGlobally={this.isSearchingGlobally}
onEnterKeyDown={this.onEnterKeyDown}
searchConversation={this.searchConversation}
searchTerm={this.searchTerm}
isSearchingGlobally={this.#isSearchingGlobally}
onEnterKeyDown={this.#onEnterKeyDown}
searchConversation={this.#searchConversation}
searchTerm={this.#searchTerm}
showConversation={showConversation}
startSearchCounter={this.startSearchCounter}
startSearchCounter={this.#startSearchCounter}
updateSearchTerm={updateSearchTerm}
filterButtonEnabled={!this.searchConversation}
filterPressed={this.filterByUnread}
filterButtonEnabled={!this.#searchConversation}
filterPressed={this.#filterByUnread}
onFilterClick={updateFilterByUnread}
/>
);
@ -156,7 +143,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
}: Readonly<{
i18n: LocalizerType;
}>): ReactChild | null {
const mightHaveSearchResults = this.allResults().some(
const mightHaveSearchResults = this.#allResults().some(
searchResult => searchResult.isLoading || searchResult.results.length
);
@ -164,7 +151,9 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
return null;
}
const { searchConversationName, primarySendsSms, searchTerm } = this;
const searchTerm = this.#searchTerm;
const primarySendsSms = this.#primarySendsSms;
const searchConversationName = this.#searchConversationName;
let noResults: ReactChild;
if (searchConversationName) {
@ -182,11 +171,11 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
);
} else {
let noResultsMessage: string;
if (this.filterByUnread && this.searchTerm.length > 0) {
if (this.#filterByUnread && this.#searchTerm.length > 0) {
noResultsMessage = i18n('icu:noSearchResultsWithUnreadFilter', {
searchTerm,
});
} else if (this.filterByUnread) {
} else if (this.#filterByUnread) {
noResultsMessage = i18n('icu:noSearchResultsOnlyUnreadFilter');
} else {
noResultsMessage = i18n('icu:noSearchResults', {
@ -195,7 +184,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
}
noResults = (
<>
{this.filterByUnread && (
{this.#filterByUnread && (
<div
className="module-conversation-list__item--header module-left-pane__no-search-results__unread-header"
aria-label={i18n('icu:conversationsUnreadHeader')}
@ -218,7 +207,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
// We need this for Ctrl-T shortcut cycling through parts of app
tabIndex={-1}
className={
this.filterByUnread
this.#filterByUnread
? 'module-left-pane__no-search-results--withHeader'
: 'module-left-pane__no-search-results'
}
@ -230,19 +219,19 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
}
getRowCount(): number {
if (this.isLoading()) {
if (this.#isLoading()) {
// 1 for the header.
return 1 + SEARCH_RESULTS_FAKE_ROW_COUNT;
}
let count = this.allResults().reduce(
let count = this.#allResults().reduce(
(result: number, searchResults) =>
result + getRowCountForLoadedSearchResults(searchResults),
0
);
// The clear unread filter button adds an extra row
if (this.filterByUnread) {
if (this.#filterByUnread) {
count += 1;
}
@ -257,9 +246,11 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
}
getRow(rowIndex: number): undefined | Row {
const { conversationResults, contactResults, messageResults } = this;
const messageResults = this.#messageResults;
const contactResults = this.#contactResults;
const conversationResults = this.#conversationResults;
if (this.isLoading()) {
if (this.#isLoading()) {
if (rowIndex === 0) {
return { type: RowType.SearchResultsLoadingFakeHeader };
}
@ -273,7 +264,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
getRowCountForLoadedSearchResults(conversationResults);
const contactRowCount = getRowCountForLoadedSearchResults(contactResults);
const messageRowCount = getRowCountForLoadedSearchResults(messageResults);
const clearFilterButtonRowCount = this.filterByUnread ? 1 : 0;
const clearFilterButtonRowCount = this.#filterByUnread ? 1 : 0;
let rowOffset = 0;
@ -283,7 +274,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
return {
type: RowType.Header,
getHeaderText: i18n =>
this.filterByUnread
this.#filterByUnread
? i18n('icu:conversationsUnreadHeader')
: i18n('icu:conversationsHeader'),
};
@ -350,7 +341,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
if (rowIndex < rowOffset) {
return {
type: RowType.ClearFilterButton,
isOnNoResultsPage: this.allResults().every(
isOnNoResultsPage: this.#allResults().every(
searchResult =>
searchResult.isLoading || searchResult.results.length === 0
),
@ -361,24 +352,30 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
}
override isScrollable(): boolean {
return !this.isLoading();
return !this.#isLoading();
}
shouldRecomputeRowHeights(old: Readonly<LeftPaneSearchPropsType>): boolean {
const oldSearchPaneHelper = new LeftPaneSearchHelper(old);
const oldIsLoading = oldSearchPaneHelper.isLoading();
const newIsLoading = this.isLoading();
const oldIsLoading = oldSearchPaneHelper.#isLoading();
const newIsLoading = this.#isLoading();
if (oldIsLoading && newIsLoading) {
return false;
}
if (oldIsLoading !== newIsLoading) {
return true;
}
return searchResultKeys.some(
key =>
getRowCountForLoadedSearchResults(old[key]) !==
getRowCountForLoadedSearchResults(this[key])
);
const searchResultsByKey = [
{ current: this.#conversationResults, prev: old.conversationResults },
{ current: this.#contactResults, prev: old.contactResults },
{ current: this.#messageResults, prev: old.messageResults },
];
return searchResultsByKey.some(item => {
return (
getRowCountForLoadedSearchResults(item.prev) !==
getRowCountForLoadedSearchResults(item.current)
);
});
}
getConversationAndMessageAtIndex(
@ -388,7 +385,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
return undefined;
}
let pointer = conversationIndex;
for (const list of this.allResults()) {
for (const list of this.#allResults()) {
if (list.isLoading) {
continue;
}
@ -426,25 +423,29 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
handleKeydownForSearch(event, options);
}
private allResults() {
return [this.conversationResults, this.contactResults, this.messageResults];
#allResults() {
return [
this.#conversationResults,
this.#contactResults,
this.#messageResults,
];
}
private isLoading(): boolean {
return this.allResults().some(results => results.isLoading);
#isLoading(): boolean {
return this.#allResults().some(results => results.isLoading);
}
private onEnterKeyDown(
#onEnterKeyDown = (
clearSearchQuery: () => unknown,
showConversation: ShowConversationType
): void {
): void => {
const conversation = this.getConversationAndMessageAtIndex(0);
if (!conversation) {
return;
}
showConversation(conversation);
clearSearchQuery();
}
};
}
function getRowCountForLoadedSearchResults(

View file

@ -38,21 +38,14 @@ export type LeftPaneSetGroupMetadataPropsType = {
};
export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGroupMetadataPropsType> {
private readonly groupAvatar: undefined | Uint8Array;
private readonly groupName: string;
private readonly groupExpireTimer: DurationInSeconds;
private readonly hasError: boolean;
private readonly isCreating: boolean;
private readonly isEditingAvatar: boolean;
private readonly selectedContacts: ReadonlyArray<ContactListItemConversationType>;
private readonly userAvatarData: ReadonlyArray<AvatarDataType>;
readonly #groupAvatar: undefined | Uint8Array;
readonly #groupName: string;
readonly #groupExpireTimer: DurationInSeconds;
readonly #hasError: boolean;
readonly #isCreating: boolean;
readonly #isEditingAvatar: boolean;
readonly #selectedContacts: ReadonlyArray<ContactListItemConversationType>;
readonly #userAvatarData: ReadonlyArray<AvatarDataType>;
constructor({
groupAvatar,
@ -66,14 +59,14 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
}: Readonly<LeftPaneSetGroupMetadataPropsType>) {
super();
this.groupAvatar = groupAvatar;
this.groupName = groupName;
this.groupExpireTimer = groupExpireTimer;
this.hasError = hasError;
this.isCreating = isCreating;
this.isEditingAvatar = isEditingAvatar;
this.selectedContacts = selectedContacts;
this.userAvatarData = userAvatarData;
this.#groupAvatar = groupAvatar;
this.#groupName = groupName;
this.#groupExpireTimer = groupExpireTimer;
this.#hasError = hasError;
this.#isCreating = isCreating;
this.#isEditingAvatar = isEditingAvatar;
this.#selectedContacts = selectedContacts;
this.#userAvatarData = userAvatarData;
}
override getHeaderContents({
@ -90,7 +83,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
<button
aria-label={backButtonLabel}
className="module-left-pane__header__contents__back-button"
disabled={this.isCreating}
disabled={this.#isCreating}
onClick={this.getBackAction({ showChooseGroupMembers })}
title={backButtonLabel}
type="button"
@ -107,7 +100,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
}: {
showChooseGroupMembers: () => void;
}): undefined | (() => void) {
return this.isCreating ? undefined : showChooseGroupMembers;
return this.#isCreating ? undefined : showChooseGroupMembers;
}
override getPreRowsNode({
@ -134,7 +127,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
toggleComposeEditingAvatar: () => unknown;
}>): ReactChild {
const [avatarColor] = AvatarColors;
const disabled = this.isCreating;
const disabled = this.#isCreating;
return (
<form
@ -143,14 +136,14 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
event.preventDefault();
event.stopPropagation();
if (!this.canCreateGroup()) {
if (!this.#canCreateGroup()) {
return;
}
createGroup();
}}
>
{this.isEditingAvatar && (
{this.#isEditingAvatar && (
<Modal
modalName="LeftPaneSetGroupMetadataHelper.AvatarEditor"
hasXButton
@ -162,7 +155,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
>
<AvatarEditor
avatarColor={avatarColor}
avatarValue={this.groupAvatar}
avatarValue={this.#groupAvatar}
deleteAvatarFromDisk={composeDeleteAvatarFromDisk}
i18n={i18n}
isGroup
@ -171,7 +164,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
setComposeGroupAvatar(newAvatar);
toggleComposeEditingAvatar();
}}
userAvatarData={this.userAvatarData}
userAvatarData={this.#userAvatarData}
replaceAvatar={composeReplaceAvatar}
saveAvatarToDisk={composeSaveAvatarToDisk}
/>
@ -179,7 +172,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
)}
<AvatarPreview
avatarColor={avatarColor}
avatarValue={this.groupAvatar}
avatarValue={this.#groupAvatar}
i18n={i18n}
isEditable
isGroup
@ -196,7 +189,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
i18n={i18n}
onChangeValue={setComposeGroupName}
ref={focusRef}
value={this.groupName}
value={this.#groupName}
/>
</div>
@ -206,12 +199,12 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
</div>
<DisappearingTimerSelect
i18n={i18n}
value={this.groupExpireTimer}
value={this.#groupExpireTimer}
onChange={setComposeGroupExpireTimer}
/>
</section>
{this.hasError && (
{this.#hasError && (
<Alert
body={i18n('icu:setGroupMetadata__error-message')}
i18n={i18n}
@ -231,12 +224,12 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
}>): ReactChild {
return (
<Button
disabled={!this.canCreateGroup()}
disabled={!this.#canCreateGroup()}
onClick={() => {
createGroup();
}}
>
{this.isCreating ? (
{this.#isCreating ? (
<span aria-label={i18n('icu:loading')} role="status">
<Spinner size="20px" svgSize="small" direction="on-avatar" />
</span>
@ -248,14 +241,14 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
}
getRowCount(): number {
if (!this.selectedContacts.length) {
if (!this.#selectedContacts.length) {
return 0;
}
return this.selectedContacts.length + 2;
return this.#selectedContacts.length + 2;
}
getRow(rowIndex: number): undefined | Row {
if (!this.selectedContacts.length) {
if (!this.#selectedContacts.length) {
return undefined;
}
@ -267,11 +260,11 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
}
// This puts a blank row for the footer.
if (rowIndex === this.selectedContacts.length + 1) {
if (rowIndex === this.#selectedContacts.length + 1) {
return { type: RowType.Blank };
}
const contact = this.selectedContacts[rowIndex - 1];
const contact = this.#selectedContacts[rowIndex - 1];
return contact
? {
type: RowType.Contact,
@ -299,8 +292,8 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
return false;
}
private canCreateGroup(): boolean {
return !this.isCreating && Boolean(this.groupName.trim());
#canCreateGroup(): boolean {
return !this.#isCreating && Boolean(this.#groupName.trim());
}
}

View file

@ -9,35 +9,34 @@ export type Timeout = {
};
export class Timers {
private counter = 0;
private readonly timers = new Map<number, NodeJS.Timeout>();
#counter = 0;
readonly #timers = new Map<number, NodeJS.Timeout>();
public setTimeout(callback: () => void, delay: number): Timeout {
let id: number;
do {
id = this.counter;
id = this.#counter;
// eslint-disable-next-line no-bitwise
this.counter = (this.counter + 1) >>> 0;
} while (this.timers.has(id));
this.#counter = (this.#counter + 1) >>> 0;
} while (this.#timers.has(id));
const timer = setTimeout(() => {
this.timers.delete(id);
this.#timers.delete(id);
callback();
}, delay);
this.timers.set(id, timer);
this.#timers.set(id, timer);
return { id } as unknown as Timeout;
}
public clearTimeout({ id }: Timeout): ReturnType<typeof clearTimeout> {
const timer = this.timers.get(id);
const timer = this.#timers.get(id);
if (timer === undefined) {
return;
}
this.timers.delete(id);
this.#timers.delete(id);
return clearTimeout(timer);
}
}

View file

@ -119,8 +119,9 @@ function getJobIdForLogging(job: CoreAttachmentDownloadJobType): string {
}
export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownloadJobType> {
private visibleTimelineMessages: Set<string> = new Set();
private saveJobsBatcher = createBatcher<AttachmentDownloadJobType>({
#visibleTimelineMessages: Set<string> = new Set();
#saveJobsBatcher = createBatcher<AttachmentDownloadJobType>({
name: 'saveAttachmentDownloadJobs',
wait: 150,
maxSize: 1000,
@ -129,6 +130,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
drop(this.maybeStartJobs());
},
});
private static _instance: AttachmentDownloadManager | undefined;
override logPrefix = 'AttachmentDownloadManager';
@ -136,7 +138,9 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
markAllJobsInactive: DataWriter.resetAttachmentDownloadActive,
saveJob: async (job, options) => {
if (options?.allowBatching) {
AttachmentDownloadManager._instance?.saveJobsBatcher.add(job);
if (AttachmentDownloadManager._instance != null) {
AttachmentDownloadManager._instance.#saveJobsBatcher.add(job);
}
} else {
await DataWriter.saveAttachmentDownloadJob(job);
}
@ -166,7 +170,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
getNextJobs: ({ limit }) => {
return params.getNextJobs({
limit,
prioritizeMessageIds: [...this.visibleTimelineMessages],
prioritizeMessageIds: [...this.#visibleTimelineMessages],
sources: window.storage.get('backupMediaDownloadPaused')
? [AttachmentDownloadSource.STANDARD]
: undefined,
@ -180,7 +184,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
isLastAttempt,
}: { abortSignal: AbortSignal; isLastAttempt: boolean }
) => {
const isForCurrentlyVisibleMessage = this.visibleTimelineMessages.has(
const isForCurrentlyVisibleMessage = this.#visibleTimelineMessages.has(
job.messageId
);
return params.runDownloadAttachmentJob({
@ -250,7 +254,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
}
updateVisibleTimelineMessages(messageIds: Array<string>): void {
this.visibleTimelineMessages = new Set(messageIds);
this.#visibleTimelineMessages = new Set(messageIds);
}
static get instance(): AttachmentDownloadManager {
@ -268,7 +272,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
}
static async saveBatchedJobs(): Promise<void> {
await AttachmentDownloadManager.instance.saveJobsBatcher.flushAndWait();
await AttachmentDownloadManager.instance.#saveJobsBatcher.flushAndWait();
}
static async stop(): Promise<void> {

View file

@ -5,9 +5,8 @@ import type { LoggerType } from '../types/Logging';
import type { ParsedJob } from './types';
export class JobLogger implements LoggerType {
private id: string;
private queueType: string;
#id: string;
#queueType: string;
public attempt = -1;
@ -15,35 +14,35 @@ export class JobLogger implements LoggerType {
job: Readonly<Pick<ParsedJob<unknown>, 'id' | 'queueType'>>,
private logger: LoggerType
) {
this.id = job.id;
this.queueType = job.queueType;
this.#id = job.id;
this.#queueType = job.queueType;
}
fatal(...args: ReadonlyArray<unknown>): void {
this.logger.fatal(this.prefix(), ...args);
this.logger.fatal(this.#prefix(), ...args);
}
error(...args: ReadonlyArray<unknown>): void {
this.logger.error(this.prefix(), ...args);
this.logger.error(this.#prefix(), ...args);
}
warn(...args: ReadonlyArray<unknown>): void {
this.logger.warn(this.prefix(), ...args);
this.logger.warn(this.#prefix(), ...args);
}
info(...args: ReadonlyArray<unknown>): void {
this.logger.info(this.prefix(), ...args);
this.logger.info(this.#prefix(), ...args);
}
debug(...args: ReadonlyArray<unknown>): void {
this.logger.debug(this.prefix(), ...args);
this.logger.debug(this.#prefix(), ...args);
}
trace(...args: ReadonlyArray<unknown>): void {
this.logger.trace(this.prefix(), ...args);
this.logger.trace(this.#prefix(), ...args);
}
private prefix(): string {
return `${this.queueType} job queue, job ID ${this.id}, attempt ${this.attempt}:`;
#prefix(): string {
return `${this.#queueType} job queue, job ID ${this.#id}, attempt ${this.attempt}:`;
}
}

View file

@ -77,14 +77,12 @@ export type ActiveJobData<CoreJobType> = {
};
export abstract class JobManager<CoreJobType> {
private enabled: boolean = false;
private activeJobs: Map<string, ActiveJobData<CoreJobType>> = new Map();
private jobStartPromises: Map<string, ExplodePromiseResultType<void>> =
new Map();
private jobCompletePromises: Map<string, ExplodePromiseResultType<void>> =
new Map();
private tickTimeout: NodeJS.Timeout | null = null;
private idleCallbacks = new Array<() => void>();
#enabled: boolean = false;
#activeJobs: Map<string, ActiveJobData<CoreJobType>> = new Map();
#jobStartPromises: Map<string, ExplodePromiseResultType<void>> = new Map();
#jobCompletePromises: Map<string, ExplodePromiseResultType<void>> = new Map();
#tickTimeout: NodeJS.Timeout | null = null;
#idleCallbacks = new Array<() => void>();
protected logPrefix = 'JobManager';
public tickInterval = DEFAULT_TICK_INTERVAL;
@ -92,25 +90,25 @@ export abstract class JobManager<CoreJobType> {
async start(): Promise<void> {
log.info(`${this.logPrefix}: starting`);
if (!this.enabled) {
this.enabled = true;
if (!this.#enabled) {
this.#enabled = true;
await this.params.markAllJobsInactive();
}
await this.maybeStartJobs();
this.tick();
this.#tick();
}
async stop(): Promise<void> {
const activeJobs = [...this.activeJobs.values()];
const activeJobs = [...this.#activeJobs.values()];
log.info(
`${this.logPrefix}: stopping. There are ` +
`${activeJobs.length} active job(s)`
);
this.enabled = false;
clearTimeoutIfNecessary(this.tickTimeout);
this.tickTimeout = null;
this.#enabled = false;
clearTimeoutIfNecessary(this.#tickTimeout);
this.#tickTimeout = null;
await Promise.all(
activeJobs.map(async ({ abortController, completionPromise }) => {
abortController.abort();
@ -120,26 +118,26 @@ export abstract class JobManager<CoreJobType> {
}
async waitForIdle(): Promise<void> {
if (this.activeJobs.size === 0) {
if (this.#activeJobs.size === 0) {
return;
}
await new Promise<void>(resolve => this.idleCallbacks.push(resolve));
await new Promise<void>(resolve => this.#idleCallbacks.push(resolve));
}
private tick(): void {
clearTimeoutIfNecessary(this.tickTimeout);
this.tickTimeout = null;
#tick(): void {
clearTimeoutIfNecessary(this.#tickTimeout);
this.#tickTimeout = null;
drop(this.maybeStartJobs());
this.tickTimeout = setTimeout(() => this.tick(), this.tickInterval);
this.#tickTimeout = setTimeout(() => this.#tick(), this.tickInterval);
}
private pauseForDuration(durationMs: number): void {
this.enabled = false;
clearTimeoutIfNecessary(this.tickTimeout);
this.tickTimeout = setTimeout(() => {
this.enabled = true;
this.tick();
#pauseForDuration(durationMs: number): void {
this.#enabled = false;
clearTimeoutIfNecessary(this.#tickTimeout);
this.#tickTimeout = setTimeout(() => {
this.#enabled = true;
this.#tick();
}, durationMs);
}
@ -147,26 +145,26 @@ export abstract class JobManager<CoreJobType> {
waitForJobToBeStarted(
job: CoreJobType & Pick<JobManagerJobType, 'attempts'>
): Promise<void> {
const id = this.getJobIdIncludingAttempts(job);
const existingPromise = this.jobStartPromises.get(id)?.promise;
const id = this.#getJobIdIncludingAttempts(job);
const existingPromise = this.#jobStartPromises.get(id)?.promise;
if (existingPromise) {
return existingPromise;
}
const { promise, resolve, reject } = explodePromise<void>();
this.jobStartPromises.set(id, { promise, resolve, reject });
this.#jobStartPromises.set(id, { promise, resolve, reject });
return promise;
}
waitForJobToBeCompleted(
job: CoreJobType & Pick<JobManagerJobType, 'attempts'>
): Promise<void> {
const id = this.getJobIdIncludingAttempts(job);
const existingPromise = this.jobCompletePromises.get(id)?.promise;
const id = this.#getJobIdIncludingAttempts(job);
const existingPromise = this.#jobCompletePromises.get(id)?.promise;
if (existingPromise) {
return existingPromise;
}
const { promise, resolve, reject } = explodePromise<void>();
this.jobCompletePromises.set(id, { promise, resolve, reject });
this.#jobCompletePromises.set(id, { promise, resolve, reject });
return promise;
}
@ -188,7 +186,7 @@ export abstract class JobManager<CoreJobType> {
};
const logId = this.params.getJobIdForLogging(job);
try {
const runningJob = this.getRunningJob(job);
const runningJob = this.#getRunningJob(job);
if (runningJob) {
log.info(`${logId}: already running; resetting attempts`);
runningJob.attempts = 0;
@ -205,7 +203,7 @@ export abstract class JobManager<CoreJobType> {
await this.params.saveJob(job, { allowBatching: !options?.forceStart });
if (options?.forceStart) {
if (!this.enabled) {
if (!this.#enabled) {
log.warn(
`${logId}: added but jobManager not enabled, can't start immediately`
);
@ -213,7 +211,7 @@ export abstract class JobManager<CoreJobType> {
log.info(`${logId}: starting job immediately`);
drop(this.startJob(job));
}
} else if (this.enabled) {
} else if (this.#enabled) {
drop(this.maybeStartJobs());
}
@ -230,20 +228,21 @@ export abstract class JobManager<CoreJobType> {
// 3. after a job finishes (via startJob)
// preventing re-entrancy allow us to simplify some logic and ensure we don't try to
// start too many jobs
private _inMaybeStartJobs = false;
#_inMaybeStartJobs = false;
protected async maybeStartJobs(): Promise<void> {
if (this._inMaybeStartJobs) {
if (this.#_inMaybeStartJobs) {
return;
}
try {
this._inMaybeStartJobs = true;
if (!this.enabled) {
this.#_inMaybeStartJobs = true;
if (!this.#enabled) {
log.info(`${this.logPrefix}/_maybeStartJobs: not enabled, returning`);
return;
}
const numJobsToStart = this.getMaximumNumberOfJobsToStart();
const numJobsToStart = this.#getMaximumNumberOfJobsToStart();
if (numJobsToStart <= 0) {
return;
@ -254,10 +253,10 @@ export abstract class JobManager<CoreJobType> {
timestamp: Date.now(),
});
if (nextJobs.length === 0 && this.activeJobs.size === 0) {
if (this.idleCallbacks.length > 0) {
const callbacks = this.idleCallbacks;
this.idleCallbacks = [];
if (nextJobs.length === 0 && this.#activeJobs.size === 0) {
if (this.#idleCallbacks.length > 0) {
const callbacks = this.#idleCallbacks;
this.#idleCallbacks = [];
for (const callback of callbacks) {
callback();
}
@ -276,7 +275,7 @@ export abstract class JobManager<CoreJobType> {
drop(this.startJob(job));
}
} finally {
this._inMaybeStartJobs = false;
this.#_inMaybeStartJobs = false;
}
}
@ -286,7 +285,7 @@ export abstract class JobManager<CoreJobType> {
const logId = `${this.logPrefix}/startJob(${this.params.getJobIdForLogging(
job
)})`;
if (this.isJobRunning(job)) {
if (this.#isJobRunning(job)) {
log.info(`${logId}: job is already running`);
return;
}
@ -298,13 +297,13 @@ export abstract class JobManager<CoreJobType> {
let jobRunResult: JobManagerJobResultType<CoreJobType> | undefined;
try {
log.info(`${logId}: starting job`);
const { abortController } = this.addRunningJob(job);
const { abortController } = this.#addRunningJob(job);
await this.params.saveJob({ ...job, active: true });
const runJobPromise = this.params.runJob(job, {
abortSignal: abortController.signal,
isLastAttempt,
});
this.handleJobStartPromises(job);
this.#handleJobStartPromises(job);
jobRunResult = await runJobPromise;
const { status } = jobRunResult;
log.info(`${logId}: job completed with status: ${status}`);
@ -317,14 +316,14 @@ export abstract class JobManager<CoreJobType> {
if (isLastAttempt) {
throw new Error('Cannot retry on last attempt');
}
await this.retryJobLater(job);
await this.#retryJobLater(job);
return;
case 'rate-limited':
log.info(
`${logId}: rate-limited; retrying in ${jobRunResult.pauseDurationMs}`
);
this.pauseForDuration(jobRunResult.pauseDurationMs);
await this.retryJobLater(job);
this.#pauseForDuration(jobRunResult.pauseDurationMs);
await this.#retryJobLater(job);
return;
default:
throw missingCaseError(status);
@ -334,10 +333,10 @@ export abstract class JobManager<CoreJobType> {
if (isLastAttempt) {
await this.params.removeJob(job);
} else {
await this.retryJobLater(job);
await this.#retryJobLater(job);
}
} finally {
this.removeRunningJob(job);
this.#removeRunningJob(job);
if (jobRunResult?.status === 'finished') {
if (jobRunResult.newJob) {
log.info(
@ -350,7 +349,7 @@ export abstract class JobManager<CoreJobType> {
}
}
private async retryJobLater(job: CoreJobType & JobManagerJobType) {
async #retryJobLater(job: CoreJobType & JobManagerJobType) {
const now = Date.now();
await this.params.saveJob({
...job,
@ -366,43 +365,43 @@ export abstract class JobManager<CoreJobType> {
});
}
private getActiveJobCount(): number {
return this.activeJobs.size;
#getActiveJobCount(): number {
return this.#activeJobs.size;
}
private getMaximumNumberOfJobsToStart(): number {
#getMaximumNumberOfJobsToStart(): number {
return Math.max(
0,
this.params.maxConcurrentJobs - this.getActiveJobCount()
this.params.maxConcurrentJobs - this.#getActiveJobCount()
);
}
private getRunningJob(
#getRunningJob(
job: CoreJobType & JobManagerJobType
): (CoreJobType & JobManagerJobType) | undefined {
const id = this.params.getJobId(job);
return this.activeJobs.get(id)?.job;
return this.#activeJobs.get(id)?.job;
}
private isJobRunning(job: CoreJobType & JobManagerJobType): boolean {
return Boolean(this.getRunningJob(job));
#isJobRunning(job: CoreJobType & JobManagerJobType): boolean {
return Boolean(this.#getRunningJob(job));
}
private removeRunningJob(job: CoreJobType & JobManagerJobType) {
const idWithAttempts = this.getJobIdIncludingAttempts(job);
this.jobCompletePromises.get(idWithAttempts)?.resolve();
this.jobCompletePromises.delete(idWithAttempts);
#removeRunningJob(job: CoreJobType & JobManagerJobType) {
const idWithAttempts = this.#getJobIdIncludingAttempts(job);
this.#jobCompletePromises.get(idWithAttempts)?.resolve();
this.#jobCompletePromises.delete(idWithAttempts);
const id = this.params.getJobId(job);
this.activeJobs.get(id)?.completionPromise.resolve();
this.activeJobs.delete(id);
this.#activeJobs.get(id)?.completionPromise.resolve();
this.#activeJobs.delete(id);
}
public async cancelJobs(
predicate: (job: CoreJobType & JobManagerJobType) => boolean
): Promise<void> {
const logId = `${this.logPrefix}/cancelJobs`;
const jobs = Array.from(this.activeJobs.values()).filter(data =>
const jobs = Array.from(this.#activeJobs.values()).filter(data =>
predicate(data.job)
);
@ -419,15 +418,15 @@ export abstract class JobManager<CoreJobType> {
// First tell those waiting for the job that it's not happening
const rejectionError = new Error('Cancelled at JobManager.cancelJobs');
const idWithAttempts = this.getJobIdIncludingAttempts(job);
this.jobCompletePromises.get(idWithAttempts)?.reject(rejectionError);
this.jobCompletePromises.delete(idWithAttempts);
const idWithAttempts = this.#getJobIdIncludingAttempts(job);
this.#jobCompletePromises.get(idWithAttempts)?.reject(rejectionError);
this.#jobCompletePromises.delete(idWithAttempts);
// Give the job 1 second to cancel itself
await Promise.race([completionPromise.promise, sleep(SECOND)]);
const jobId = this.params.getJobId(job);
const hasCompleted = Boolean(this.activeJobs.get(jobId));
const hasCompleted = Boolean(this.#activeJobs.get(jobId));
if (!hasCompleted) {
const jobIdForLogging = this.params.getJobIdForLogging(job);
@ -435,7 +434,7 @@ export abstract class JobManager<CoreJobType> {
`${logId}: job ${jobIdForLogging} didn't complete; rejecting promises`
);
completionPromise.reject(rejectionError);
this.activeJobs.delete(jobId);
this.#activeJobs.delete(jobId);
}
await this.params.removeJob(job);
@ -445,10 +444,10 @@ export abstract class JobManager<CoreJobType> {
log.warn(`${logId}: Successfully cancelled ${jobs.length} jobs`);
}
private addRunningJob(
#addRunningJob(
job: CoreJobType & JobManagerJobType
): ActiveJobData<CoreJobType> {
if (this.isJobRunning(job)) {
if (this.#isJobRunning(job)) {
const jobIdForLogging = this.params.getJobIdForLogging(job);
log.warn(
`${this.logPrefix}/addRunningJob: job ${jobIdForLogging} is already running`
@ -460,18 +459,18 @@ export abstract class JobManager<CoreJobType> {
abortController: new AbortController(),
job,
};
this.activeJobs.set(this.params.getJobId(job), activeJob);
this.#activeJobs.set(this.params.getJobId(job), activeJob);
return activeJob;
}
private handleJobStartPromises(job: CoreJobType & JobManagerJobType) {
const id = this.getJobIdIncludingAttempts(job);
this.jobStartPromises.get(id)?.resolve();
this.jobStartPromises.delete(id);
#handleJobStartPromises(job: CoreJobType & JobManagerJobType) {
const id = this.#getJobIdIncludingAttempts(job);
this.#jobStartPromises.get(id)?.resolve();
this.#jobStartPromises.delete(id);
}
private getJobIdIncludingAttempts(
#getJobIdIncludingAttempts(
job: CoreJobType & Pick<JobManagerJobType, 'attempts'>
) {
return `${this.params.getJobId(job)}.${job.attempts}`;

View file

@ -53,20 +53,15 @@ export enum JOB_STATUS {
}
export abstract class JobQueue<T> {
private readonly maxAttempts: number;
readonly #maxAttempts: number;
readonly #queueType: string;
readonly #store: JobQueueStore;
readonly #logger: LoggerType;
readonly #logPrefix: string;
#shuttingDown = false;
#paused = false;
private readonly queueType: string;
private readonly store: JobQueueStore;
private readonly logger: LoggerType;
private readonly logPrefix: string;
private shuttingDown = false;
private paused = false;
private readonly onCompleteCallbacks = new Map<
readonly #onCompleteCallbacks = new Map<
string,
{
resolve: () => void;
@ -74,15 +69,15 @@ export abstract class JobQueue<T> {
}
>();
private readonly defaultInMemoryQueue = new PQueue({ concurrency: 1 });
private started = false;
readonly #defaultInMemoryQueue = new PQueue({ concurrency: 1 });
#started = false;
get isShuttingDown(): boolean {
return this.shuttingDown;
return this.#shuttingDown;
}
get isPaused(): boolean {
return this.paused;
return this.#paused;
}
constructor(options: Readonly<JobQueueOptions>) {
@ -99,12 +94,12 @@ export abstract class JobQueue<T> {
'queueType should be a non-blank string'
);
this.maxAttempts = options.maxAttempts;
this.queueType = options.queueType;
this.store = options.store;
this.logger = options.logger ?? log;
this.#maxAttempts = options.maxAttempts;
this.#queueType = options.queueType;
this.#store = options.store;
this.#logger = options.logger ?? log;
this.logPrefix = `${this.queueType} job queue:`;
this.#logPrefix = `${this.#queueType} job queue:`;
}
/**
@ -135,35 +130,37 @@ export abstract class JobQueue<T> {
): Promise<JOB_STATUS.NEEDS_RETRY | undefined>;
protected getQueues(): ReadonlySet<PQueue> {
return new Set([this.defaultInMemoryQueue]);
return new Set([this.#defaultInMemoryQueue]);
}
/**
* Start streaming jobs from the store.
*/
async streamJobs(): Promise<void> {
if (this.started) {
if (this.#started) {
throw new Error(
`${this.logPrefix} should not start streaming more than once`
`${this.#logPrefix} should not start streaming more than once`
);
}
this.started = true;
this.#started = true;
log.info(`${this.logPrefix} starting to stream jobs`);
log.info(`${this.#logPrefix} starting to stream jobs`);
const stream = this.store.stream(this.queueType);
const stream = this.#store.stream(this.#queueType);
for await (const storedJob of stream) {
if (this.shuttingDown) {
log.info(`${this.logPrefix} is shutting down. Can't accept more work.`);
if (this.#shuttingDown) {
log.info(
`${this.#logPrefix} is shutting down. Can't accept more work.`
);
break;
}
if (this.paused) {
log.info(`${this.logPrefix} is paused. Waiting until resume.`);
while (this.paused) {
if (this.#paused) {
log.info(`${this.#logPrefix} is paused. Waiting until resume.`);
while (this.#paused) {
// eslint-disable-next-line no-await-in-loop
await sleep(SECOND);
}
log.info(`${this.logPrefix} has been resumed. Queuing job.`);
log.info(`${this.#logPrefix} has been resumed. Queuing job.`);
}
drop(this.enqueueStoredJob(storedJob));
}
@ -183,18 +180,18 @@ export abstract class JobQueue<T> {
): Promise<Job<T>> {
const job = this.createJob(data);
if (!this.started) {
if (!this.#started) {
log.warn(
`${this.logPrefix} This queue has not started streaming, adding job ${job.id} to database only.`
`${this.#logPrefix} This queue has not started streaming, adding job ${job.id} to database only.`
);
}
if (insert) {
await insert(job);
}
await this.store.insert(job, { shouldPersist: !insert });
await this.#store.insert(job, { shouldPersist: !insert });
log.info(`${this.logPrefix} added new job ${job.id}`);
log.info(`${this.#logPrefix} added new job ${job.id}`);
return job;
}
@ -203,7 +200,7 @@ export abstract class JobQueue<T> {
const timestamp = Date.now();
const completionPromise = new Promise<void>((resolve, reject) => {
this.onCompleteCallbacks.set(id, { resolve, reject });
this.#onCompleteCallbacks.set(id, { resolve, reject });
});
const completion = (async () => {
try {
@ -211,41 +208,41 @@ export abstract class JobQueue<T> {
} catch (err: unknown) {
throw new JobError(err);
} finally {
this.onCompleteCallbacks.delete(id);
this.#onCompleteCallbacks.delete(id);
}
})();
return new Job(id, timestamp, this.queueType, data, completion);
return new Job(id, timestamp, this.#queueType, data, completion);
}
protected getInMemoryQueue(_parsedJob: ParsedJob<T>): PQueue {
return this.defaultInMemoryQueue;
return this.#defaultInMemoryQueue;
}
protected async enqueueStoredJob(
storedJob: Readonly<StoredJob>
): Promise<void> {
assertDev(
storedJob.queueType === this.queueType,
storedJob.queueType === this.#queueType,
'Received a mis-matched queue type'
);
log.info(`${this.logPrefix} enqueuing job ${storedJob.id}`);
log.info(`${this.#logPrefix} enqueuing job ${storedJob.id}`);
// It's okay if we don't have a callback; that likely means the job was created before
// the process was started (e.g., from a previous run).
const { resolve, reject } =
this.onCompleteCallbacks.get(storedJob.id) || noopOnCompleteCallbacks;
this.#onCompleteCallbacks.get(storedJob.id) || noopOnCompleteCallbacks;
let parsedData: T;
try {
parsedData = this.parseData(storedJob.data);
} catch (err) {
log.error(
`${this.logPrefix} failed to parse data for job ${storedJob.id}, created ${storedJob.timestamp}. Deleting job. Parse error:`,
`${this.#logPrefix} failed to parse data for job ${storedJob.id}, created ${storedJob.timestamp}. Deleting job. Parse error:`,
Errors.toLogFormat(err)
);
await this.store.delete(storedJob.id);
await this.#store.delete(storedJob.id);
reject(
new Error(
'Failed to parse job data. Was unexpected data loaded from the database?'
@ -261,7 +258,7 @@ export abstract class JobQueue<T> {
const queue: PQueue = this.getInMemoryQueue(parsedJob);
const logger = new JobLogger(parsedJob, this.logger);
const logger = new JobLogger(parsedJob, this.#logger);
const result:
| undefined
@ -269,18 +266,18 @@ export abstract class JobQueue<T> {
| { status: JOB_STATUS.NEEDS_RETRY }
| { status: JOB_STATUS.ERROR; err: unknown } = await queue.add(
async () => {
for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
const isFinalAttempt = attempt === this.maxAttempts;
for (let attempt = 1; attempt <= this.#maxAttempts; attempt += 1) {
const isFinalAttempt = attempt === this.#maxAttempts;
logger.attempt = attempt;
log.info(
`${this.logPrefix} running job ${storedJob.id}, attempt ${attempt} of ${this.maxAttempts}`
`${this.#logPrefix} running job ${storedJob.id}, attempt ${attempt} of ${this.#maxAttempts}`
);
if (this.isShuttingDown) {
log.warn(
`${this.logPrefix} returning early for job ${storedJob.id}; shutting down`
`${this.#logPrefix} returning early for job ${storedJob.id}; shutting down`
);
return {
status: JOB_STATUS.ERROR,
@ -298,17 +295,17 @@ export abstract class JobQueue<T> {
});
if (!jobStatus) {
log.info(
`${this.logPrefix} job ${storedJob.id} succeeded on attempt ${attempt}`
`${this.#logPrefix} job ${storedJob.id} succeeded on attempt ${attempt}`
);
return { status: JOB_STATUS.SUCCESS };
}
log.info(
`${this.logPrefix} job ${storedJob.id} returned status ${jobStatus} on attempt ${attempt}`
`${this.#logPrefix} job ${storedJob.id} returned status ${jobStatus} on attempt ${attempt}`
);
return { status: jobStatus };
} catch (err: unknown) {
log.error(
`${this.logPrefix} job ${
`${this.#logPrefix} job ${
storedJob.id
} failed on attempt ${attempt}. ${Errors.toLogFormat(err)}`
);
@ -330,14 +327,14 @@ export abstract class JobQueue<T> {
logger,
});
if (!addJobSuccess) {
await this.store.delete(storedJob.id);
await this.#store.delete(storedJob.id);
}
}
if (
result?.status === JOB_STATUS.SUCCESS ||
(result?.status === JOB_STATUS.ERROR && !this.isShuttingDown)
) {
await this.store.delete(storedJob.id);
await this.#store.delete(storedJob.id);
}
assertDev(
@ -359,7 +356,7 @@ export abstract class JobQueue<T> {
logger: LoggerType;
}): Promise<boolean> {
logger.error(
`retryJobOnQueueIdle: not implemented for queue ${this.queueType}; dropping job`
`retryJobOnQueueIdle: not implemented for queue ${this.#queueType}; dropping job`
);
return false;
}
@ -367,18 +364,20 @@ export abstract class JobQueue<T> {
async shutdown(): Promise<void> {
const queues = this.getQueues();
log.info(
`${this.logPrefix} shutdown: stop accepting new work and drain ${queues.size} promise queues`
`${this.#logPrefix} shutdown: stop accepting new work and drain ${queues.size} promise queues`
);
this.shuttingDown = true;
this.#shuttingDown = true;
await Promise.all([...queues].map(q => q.onIdle()));
log.info(`${this.logPrefix} shutdown: complete`);
log.info(`${this.#logPrefix} shutdown: complete`);
}
pause(): void {
log.info(`${this.logPrefix}: pausing queue`);
this.paused = true;
log.info(`${this.#logPrefix}: pausing queue`);
this.#paused = true;
}
resume(): void {
log.info(`${this.logPrefix}: resuming queue`);
this.paused = false;
log.info(`${this.#logPrefix}: resuming queue`);
this.#paused = false;
}
}

View file

@ -16,11 +16,9 @@ type Database = {
};
export class JobQueueDatabaseStore implements JobQueueStore {
private activeQueueTypes = new Set<string>();
private queues = new Map<string, AsyncQueue<StoredJob>>();
private initialFetchPromises = new Map<string, Promise<void>>();
#activeQueueTypes = new Set<string>();
#queues = new Map<string, AsyncQueue<StoredJob>>();
#initialFetchPromises = new Map<string, Promise<void>>();
constructor(private readonly db: Database) {}
@ -34,7 +32,7 @@ export class JobQueueDatabaseStore implements JobQueueStore {
)}`
);
const initialFetchPromise = this.initialFetchPromises.get(job.queueType);
const initialFetchPromise = this.#initialFetchPromises.get(job.queueType);
if (initialFetchPromise) {
await initialFetchPromise;
} else {
@ -48,7 +46,7 @@ export class JobQueueDatabaseStore implements JobQueueStore {
}
if (initialFetchPromise) {
this.getQueue(job.queueType).add(job);
this.#getQueue(job.queueType).add(job);
}
}
@ -57,31 +55,31 @@ export class JobQueueDatabaseStore implements JobQueueStore {
}
stream(queueType: string): AsyncIterable<StoredJob> {
if (this.activeQueueTypes.has(queueType)) {
if (this.#activeQueueTypes.has(queueType)) {
throw new Error(
`Cannot stream queue type ${JSON.stringify(queueType)} more than once`
);
}
this.activeQueueTypes.add(queueType);
this.#activeQueueTypes.add(queueType);
return concat([
wrapPromise(this.fetchJobsAtStart(queueType)),
this.getQueue(queueType),
wrapPromise(this.#fetchJobsAtStart(queueType)),
this.#getQueue(queueType),
]);
}
private getQueue(queueType: string): AsyncQueue<StoredJob> {
const existingQueue = this.queues.get(queueType);
#getQueue(queueType: string): AsyncQueue<StoredJob> {
const existingQueue = this.#queues.get(queueType);
if (existingQueue) {
return existingQueue;
}
const result = new AsyncQueue<StoredJob>();
this.queues.set(queueType, result);
this.#queues.set(queueType, result);
return result;
}
private async fetchJobsAtStart(queueType: string): Promise<Array<StoredJob>> {
async #fetchJobsAtStart(queueType: string): Promise<Array<StoredJob>> {
log.info(
`JobQueueDatabaseStore fetching existing jobs for queue ${JSON.stringify(
queueType
@ -94,7 +92,7 @@ export class JobQueueDatabaseStore implements JobQueueStore {
const initialFetchPromise = new Promise<void>(resolve => {
onFinished = resolve;
});
this.initialFetchPromises.set(queueType, initialFetchPromise);
this.#initialFetchPromises.set(queueType, initialFetchPromise);
const result = await this.db.getJobsInQueue(queueType);
log.info(

View file

@ -45,18 +45,17 @@ export type CallLinkRefreshJobData = z.infer<
>;
export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
private parallelQueue = new PQueue({ concurrency: MAX_PARALLEL_JOBS });
private readonly pendingCallLinks = new Map<string, PendingCallLinkType>();
#parallelQueue = new PQueue({ concurrency: MAX_PARALLEL_JOBS });
readonly #pendingCallLinks = new Map<string, PendingCallLinkType>();
protected override getQueues(): ReadonlySet<PQueue> {
return new Set([this.parallelQueue]);
return new Set([this.#parallelQueue]);
}
protected override getInMemoryQueue(
_parsedJob: ParsedJob<CallLinkRefreshJobData>
): PQueue {
return this.parallelQueue;
return this.#parallelQueue;
}
protected parseData(data: unknown): CallLinkRefreshJobData {
@ -81,7 +80,7 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
adminKey,
} = parsedData ?? {};
if (storageID && storageVersion && rootKey) {
this.pendingCallLinks.set(rootKey, {
this.#pendingCallLinks.set(rootKey, {
rootKey,
adminKey: adminKey ?? null,
storageID: storageID ?? undefined,
@ -94,7 +93,7 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
await super.enqueueStoredJob(storedJob);
if (rootKey) {
this.pendingCallLinks.delete(rootKey);
this.#pendingCallLinks.delete(rootKey);
}
}
@ -102,13 +101,13 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
// depending on the refresh result, we will create either CallLinks or DefunctCallLinks,
// and we'll save storageID and version onto those records.
public getPendingAdminCallLinks(): ReadonlyArray<PendingCallLinkType> {
return Array.from(this.pendingCallLinks.values()).filter(
return Array.from(this.#pendingCallLinks.values()).filter(
callLink => callLink.adminKey != null
);
}
public hasPendingCallLink(rootKey: string): boolean {
return this.pendingCallLinks.has(rootKey);
return this.#pendingCallLinks.has(rootKey);
}
// If a new version of storage is uploaded before we get a chance to refresh the
@ -118,7 +117,7 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
rootKey: string,
storageFields: StorageServiceFieldsType
): void {
const existingStorageFields = this.pendingCallLinks.get(rootKey);
const existingStorageFields = this.#pendingCallLinks.get(rootKey);
if (!existingStorageFields) {
globalLogger.warn(
'callLinkRefreshJobQueue.updatePendingCallLinkStorageFields: unknown rootKey'
@ -126,7 +125,7 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
return;
}
this.pendingCallLinks.set(rootKey, {
this.#pendingCallLinks.set(rootKey, {
...existingStorageFields,
...storageFields,
});
@ -136,7 +135,7 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
storageID: string,
jobData: CallLinkRefreshJobData
): StorageServiceFieldsType | undefined {
const storageFields = this.pendingCallLinks.get(storageID);
const storageFields = this.#pendingCallLinks.get(storageID);
if (storageFields) {
return {
storageID: storageFields.storageID,

View file

@ -384,12 +384,14 @@ type ConversationData = Readonly<
>;
export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
private readonly perConversationData = new Map<
readonly #perConversationData = new Map<
string,
ConversationData | undefined
>();
private readonly inMemoryQueues = new InMemoryQueues();
private readonly verificationWaitMap = new Map<
readonly #inMemoryQueues = new InMemoryQueues();
readonly #verificationWaitMap = new Map<
string,
{
resolve: (value: unknown) => unknown;
@ -397,10 +399,11 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
promise: Promise<unknown>;
}
>();
private callbackCount = 0;
#callbackCount = 0;
override getQueues(): ReadonlySet<PQueue> {
return this.inMemoryQueues.allQueues;
return this.#inMemoryQueues.allQueues;
}
public override async add(
@ -430,11 +433,11 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
protected override getInMemoryQueue({
data,
}: Readonly<{ data: ConversationQueueJobData }>): PQueue {
return this.inMemoryQueues.get(data.conversationId);
return this.#inMemoryQueues.get(data.conversationId);
}
private startVerificationWaiter(conversationId: string): Promise<unknown> {
const existing = this.verificationWaitMap.get(conversationId);
#startVerificationWaiter(conversationId: string): Promise<unknown> {
const existing = this.#verificationWaitMap.get(conversationId);
if (existing) {
globalLogger.info(
`startVerificationWaiter: Found existing waiter for conversation ${conversationId}. Returning it.`
@ -446,7 +449,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
`startVerificationWaiter: Starting new waiter for conversation ${conversationId}.`
);
const { resolve, reject, promise } = explodePromise();
this.verificationWaitMap.set(conversationId, {
this.#verificationWaitMap.set(conversationId, {
resolve,
reject,
promise,
@ -456,25 +459,25 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
}
public resolveVerificationWaiter(conversationId: string): void {
const existing = this.verificationWaitMap.get(conversationId);
const existing = this.#verificationWaitMap.get(conversationId);
if (existing) {
globalLogger.info(
`resolveVerificationWaiter: Found waiter for conversation ${conversationId}. Resolving.`
);
existing.resolve('resolveVerificationWaiter: success');
this.verificationWaitMap.delete(conversationId);
this.#verificationWaitMap.delete(conversationId);
} else {
globalLogger.warn(
`resolveVerificationWaiter: Missing waiter for conversation ${conversationId}.`
);
this.unblockConversationRetries(conversationId);
this.#unblockConversationRetries(conversationId);
}
}
private unblockConversationRetries(conversationId: string) {
#unblockConversationRetries(conversationId: string) {
const logId = `unblockConversationRetries/${conversationId}`;
const perConversationData = this.perConversationData.get(conversationId);
const perConversationData = this.#perConversationData.get(conversationId);
if (!perConversationData) {
return;
}
@ -484,7 +487,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
globalLogger.info(
`${logId}: Previously BLOCKED, moving to RUNNING state`
);
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
status: RETRY_STATUS.RUNNING,
attempts,
callback: undefined,
@ -496,7 +499,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
globalLogger.info(
`${logId}: Moving previous BLOCKED state to UNBLOCKED, calling callback directly`
);
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
...perConversationData,
status: RETRY_STATUS.UNBLOCKED,
retryAt: undefined,
@ -516,10 +519,10 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
}
}
private recordSuccessfulSend(conversationId: string) {
#recordSuccessfulSend(conversationId: string) {
const logId = `recordSuccessfulSend/${conversationId}`;
const perConversationData = this.perConversationData.get(conversationId);
const perConversationData = this.#perConversationData.get(conversationId);
if (!perConversationData) {
return;
}
@ -527,7 +530,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
const { status } = perConversationData;
if (status === RETRY_STATUS.RUNNING || status === RETRY_STATUS.BLOCKED) {
globalLogger.info(`${logId}: Previously ${status}; clearing state`);
this.perConversationData.delete(conversationId);
this.#perConversationData.delete(conversationId);
} else if (
status === RETRY_STATUS.BLOCKED_WITH_JOBS ||
status === RETRY_STATUS.UNBLOCKED
@ -536,23 +539,23 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
`${logId}: We're still in ${status} state; calling unblockConversationRetries`
);
// We have to do this because in these states there are jobs that need to be retried
this.unblockConversationRetries(conversationId);
this.#unblockConversationRetries(conversationId);
} else {
throw missingCaseError(status);
}
}
private getRetryWithBackoff(attempts: number) {
#getRetryWithBackoff(attempts: number) {
return (
Date.now() +
MINUTE * (FIBONACCI[attempts] ?? FIBONACCI[FIBONACCI.length - 1])
);
}
private captureRetryAt(conversationId: string, retryAt: number | undefined) {
#captureRetryAt(conversationId: string, retryAt: number | undefined) {
const logId = `captureRetryAt/${conversationId}`;
const perConversationData = this.perConversationData.get(conversationId);
const perConversationData = this.#perConversationData.get(conversationId);
if (!perConversationData) {
const newRetryAt = retryAt || Date.now() + MINUTE;
if (!retryAt) {
@ -560,7 +563,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
`${logId}: No existing data, using retryAt of ${newRetryAt}`
);
}
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
status: RETRY_STATUS.BLOCKED,
attempts: 1,
retryAt: newRetryAt,
@ -573,7 +576,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
const { status, retryAt: existingRetryAt } = perConversationData;
const attempts = perConversationData.attempts + 1;
const retryWithBackoff = this.getRetryWithBackoff(attempts);
const retryWithBackoff = this.#getRetryWithBackoff(attempts);
if (existingRetryAt && existingRetryAt >= retryWithBackoff) {
globalLogger.warn(
@ -589,7 +592,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
globalLogger.info(
`${logId}: Updating to new retryAt ${retryWithBackoff} (attempts ${attempts}) from existing retryAt ${existingRetryAt}, status ${status}`
);
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
...perConversationData,
retryAt: retryWithBackoff,
});
@ -597,7 +600,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
globalLogger.info(
`${logId}: Updating to new retryAt ${retryWithBackoff} (attempts ${attempts}) from previous UNBLOCKED status`
);
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
...perConversationData,
status: RETRY_STATUS.BLOCKED_WITH_JOBS,
retryAt: retryWithBackoff,
@ -606,7 +609,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
globalLogger.info(
`${logId}: Updating to new retryAt ${retryWithBackoff} (attempts ${attempts}) from previous RUNNING status`
);
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
status: RETRY_STATUS.BLOCKED,
attempts,
retryAt: retryWithBackoff,
@ -629,7 +632,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
}): Promise<boolean> {
const { conversationId } = job.data;
const logId = `retryJobOnQueueIdle/${conversationId}/${job.id}`;
const perConversationData = this.perConversationData.get(conversationId);
const perConversationData = this.#perConversationData.get(conversationId);
if (!perConversationData) {
logger.warn(`${logId}: no data for conversation; using default retryAt`);
@ -653,13 +656,13 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
);
const newCallback =
callback || this.createRetryCallback(conversationId, job.id);
callback || this.#createRetryCallback(conversationId, job.id);
if (
status === RETRY_STATUS.BLOCKED ||
status === RETRY_STATUS.BLOCKED_WITH_JOBS
) {
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
status: RETRY_STATUS.BLOCKED_WITH_JOBS,
attempts,
retryAt,
@ -668,11 +671,11 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
});
} else if (status === RETRY_STATUS.RUNNING) {
const newAttempts = attempts + 1;
const newRetryAt = this.getRetryWithBackoff(newAttempts);
const newRetryAt = this.#getRetryWithBackoff(newAttempts);
logger.warn(
`${logId}: Moving from state RUNNING to BLOCKED_WITH_JOBS, with retryAt ${newRetryAt}, (attempts ${newAttempts})`
);
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
status: RETRY_STATUS.BLOCKED_WITH_JOBS,
attempts: newAttempts,
retryAt: newRetryAt,
@ -680,7 +683,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
callback: newCallback,
});
} else {
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
status: RETRY_STATUS.UNBLOCKED,
attempts,
retryAt,
@ -703,9 +706,9 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
return true;
}
private createRetryCallback(conversationId: string, jobId: string) {
this.callbackCount += 1;
const id = this.callbackCount;
#createRetryCallback(conversationId: string, jobId: string) {
this.#callbackCount += 1;
const id = this.#callbackCount;
globalLogger.info(
`createRetryCallback/${conversationId}/${id}: callback created for job ${jobId}`
@ -714,7 +717,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
return () => {
const logId = `retryCallback/${conversationId}/${id}`;
const perConversationData = this.perConversationData.get(conversationId);
const perConversationData = this.#perConversationData.get(conversationId);
if (!perConversationData) {
globalLogger.warn(`${logId}: no perConversationData, returning early.`);
return;
@ -741,7 +744,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
// We're starting to retry jobs; remove the challenge handler
drop(window.Signal.challengeHandler?.unregister(conversationId, logId));
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
status: RETRY_STATUS.RUNNING,
attempts,
callback: undefined,
@ -761,7 +764,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
`${logId}: retryAt ${retryAt} is in the future, scheduling timeout for ${timeLeft}ms`
);
this.perConversationData.set(conversationId, {
this.#perConversationData.set(conversationId, {
...perConversationData,
retryAtTimeout: setTimeout(() => {
globalLogger.info(`${logId}: Running callback due to timeout`);
@ -780,7 +783,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
const { type, conversationId } = data;
const isFinalAttempt = attempt >= MAX_ATTEMPTS;
const perConversationData = this.perConversationData.get(conversationId);
const perConversationData = this.#perConversationData.get(conversationId);
await window.ConversationController.load();
@ -818,7 +821,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
const isChallengeRegistered =
window.Signal.challengeHandler?.isRegistered(conversationId);
if (!isChallengeRegistered) {
this.unblockConversationRetries(conversationId);
this.#unblockConversationRetries(conversationId);
}
if (isChallengeRegistered && shouldSendShowCaptcha(type)) {
@ -838,7 +841,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
);
// eslint-disable-next-line no-await-in-loop
await Promise.race([
this.startVerificationWaiter(conversation.id),
this.#startVerificationWaiter(conversation.id),
// don't resolve on shutdown, otherwise we end up in an infinite loop
sleeper.sleep(
5 * MINUTE,
@ -877,7 +880,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
);
// eslint-disable-next-line no-await-in-loop
await Promise.race([
this.startVerificationWaiter(conversation.id),
this.#startVerificationWaiter(conversation.id),
// don't resolve on shutdown, otherwise we end up in an infinite loop
sleeper.sleep(
5 * MINUTE,
@ -986,7 +989,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
}
if (shouldContinue && !this.isShuttingDown) {
this.recordSuccessfulSend(conversationId);
this.#recordSuccessfulSend(conversationId);
}
return undefined;
@ -1030,7 +1033,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
);
if (silent) {
this.captureRetryAt(conversationId, toProcess.retryAt);
this.#captureRetryAt(conversationId, toProcess.retryAt);
return JOB_STATUS.NEEDS_RETRY;
}
}

View file

@ -4,24 +4,24 @@
import PQueue from 'p-queue';
export class InMemoryQueues {
private readonly queues = new Map<string, PQueue>();
readonly #queues = new Map<string, PQueue>();
get(key: string): PQueue {
const existingQueue = this.queues.get(key);
const existingQueue = this.#queues.get(key);
if (existingQueue) {
return existingQueue;
}
const newQueue = new PQueue({ concurrency: 1 });
newQueue.once('idle', () => {
this.queues.delete(key);
this.#queues.delete(key);
});
this.queues.set(key, newQueue);
this.#queues.set(key, newQueue);
return newQueue;
}
get allQueues(): ReadonlySet<PQueue> {
return new Set(this.queues.values());
return new Set(this.#queues.values());
}
}

View file

@ -38,10 +38,10 @@ const reportSpamJobDataSchema = z.object({
export type ReportSpamJobData = z.infer<typeof reportSpamJobDataSchema>;
export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
private server?: WebAPIType;
#server?: WebAPIType;
public initialize({ server }: { server: WebAPIType }): void {
this.server = server;
this.#server = server;
}
protected parseData(data: unknown): ReportSpamJobData {
@ -65,7 +65,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
await waitForOnline();
const { server } = this;
const server = this.#server;
strictAssert(server !== undefined, 'ReportSpamJobQueue not initialized');
try {

View file

@ -31,16 +31,16 @@ const MAX_PARALLEL_JOBS = 5;
const MAX_ATTEMPTS = exponentialBackoffMaxAttempts(MAX_RETRY_TIME);
export class SingleProtoJobQueue extends JobQueue<SingleProtoJobData> {
private parallelQueue = new PQueue({ concurrency: MAX_PARALLEL_JOBS });
#parallelQueue = new PQueue({ concurrency: MAX_PARALLEL_JOBS });
protected override getQueues(): ReadonlySet<PQueue> {
return new Set([this.parallelQueue]);
return new Set([this.#parallelQueue]);
}
protected override getInMemoryQueue(
_parsedJob: ParsedJob<SingleProtoJobData>
): PQueue {
return this.parallelQueue;
return this.#parallelQueue;
}
protected parseData(data: unknown): SingleProtoJobData {

View file

@ -13,11 +13,11 @@ function getState(): NativeThemeState {
}
export class NativeThemeNotifier {
private readonly listeners = new Set<BrowserWindow>();
readonly #listeners = new Set<BrowserWindow>();
public initialize(): void {
nativeTheme.on('updated', () => {
this.notifyListeners();
this.#notifyListeners();
});
ipc.on('native-theme:init', event => {
@ -27,19 +27,19 @@ export class NativeThemeNotifier {
}
public addWindow(window: BrowserWindow): void {
if (this.listeners.has(window)) {
if (this.#listeners.has(window)) {
return;
}
this.listeners.add(window);
this.#listeners.add(window);
window.once('closed', () => {
this.listeners.delete(window);
this.#listeners.delete(window);
});
}
private notifyListeners(): void {
for (const window of this.listeners) {
#notifyListeners(): void {
for (const window of this.#listeners) {
window.webContents.send('native-theme:changed', getState());
}
}

View file

@ -8,17 +8,17 @@ import * as log from '../logging/log';
import type { IPCRequest, IPCResponse, ChallengeResponse } from '../challenge';
export class ChallengeMainHandler {
private handlers: Array<(response: ChallengeResponse) => void> = [];
#handlers: Array<(response: ChallengeResponse) => void> = [];
constructor() {
this.initialize();
this.#initialize();
}
public handleCaptcha(captcha: string): void {
const response: ChallengeResponse = { captcha };
const { handlers } = this;
this.handlers = [];
const handlers = this.#handlers;
this.#handlers = [];
log.info(
'challengeMain.handleCaptcha: sending captcha response to ' +
@ -29,17 +29,14 @@ export class ChallengeMainHandler {
}
}
private async onRequest(
event: IpcMainEvent,
request: IPCRequest
): Promise<void> {
async #onRequest(event: IpcMainEvent, request: IPCRequest): Promise<void> {
const logId = `challengeMain.onRequest(${request.reason})`;
log.info(`${logId}: received challenge request, waiting for response`);
const start = Date.now();
const data = await new Promise<ChallengeResponse>(resolve => {
this.handlers.push(resolve);
this.#handlers.push(resolve);
});
const duration = Date.now() - start;
@ -52,9 +49,9 @@ export class ChallengeMainHandler {
event.sender.send('challenge:response', ipcResponse);
}
private initialize(): void {
#initialize(): void {
ipc.on('challenge:request', (event, request) => {
void this.onRequest(event, request);
void this.#onRequest(event, request);
});
}
}

View file

@ -32,85 +32,83 @@ type SettingChangeEventType<Key extends keyof SettingsValuesType> =
`change:${Key}`;
export class SettingsChannel extends EventEmitter {
private mainWindow?: BrowserWindow;
private readonly responseQueue = new Map<number, ResponseQueueEntry>();
private responseSeq = 0;
#mainWindow?: BrowserWindow;
readonly #responseQueue = new Map<number, ResponseQueueEntry>();
#responseSeq = 0;
public setMainWindow(mainWindow: BrowserWindow | undefined): void {
this.mainWindow = mainWindow;
this.#mainWindow = mainWindow;
}
public getMainWindow(): BrowserWindow | undefined {
return this.mainWindow;
return this.#mainWindow;
}
public install(): void {
this.installSetting('deviceName', { setter: false });
this.installSetting('phoneNumber', { setter: false });
this.#installSetting('deviceName', { setter: false });
this.#installSetting('phoneNumber', { setter: false });
// ChatColorPicker redux hookups
this.installCallback('getCustomColors');
this.installCallback('getConversationsWithCustomColor');
this.installCallback('resetAllChatColors');
this.installCallback('resetDefaultChatColor');
this.installCallback('addCustomColor');
this.installCallback('editCustomColor');
this.installCallback('removeCustomColor');
this.installCallback('removeCustomColorOnConversations');
this.installCallback('setGlobalDefaultConversationColor');
this.installCallback('getDefaultConversationColor');
this.#installCallback('getCustomColors');
this.#installCallback('getConversationsWithCustomColor');
this.#installCallback('resetAllChatColors');
this.#installCallback('resetDefaultChatColor');
this.#installCallback('addCustomColor');
this.#installCallback('editCustomColor');
this.#installCallback('removeCustomColor');
this.#installCallback('removeCustomColorOnConversations');
this.#installCallback('setGlobalDefaultConversationColor');
this.#installCallback('getDefaultConversationColor');
// Various callbacks
this.installCallback('deleteAllMyStories');
this.installCallback('getAvailableIODevices');
this.installCallback('isPrimary');
this.installCallback('syncRequest');
this.#installCallback('deleteAllMyStories');
this.#installCallback('getAvailableIODevices');
this.#installCallback('isPrimary');
this.#installCallback('syncRequest');
// Getters only. These are set by the primary device
this.installSetting('blockedCount', { setter: false });
this.installSetting('linkPreviewSetting', { setter: false });
this.installSetting('readReceiptSetting', { setter: false });
this.installSetting('typingIndicatorSetting', { setter: false });
this.#installSetting('blockedCount', { setter: false });
this.#installSetting('linkPreviewSetting', { setter: false });
this.#installSetting('readReceiptSetting', { setter: false });
this.#installSetting('typingIndicatorSetting', { setter: false });
this.installSetting('hideMenuBar');
this.installSetting('notificationSetting');
this.installSetting('notificationDrawAttention');
this.installSetting('audioMessage');
this.installSetting('audioNotification');
this.installSetting('countMutedConversations');
this.#installSetting('hideMenuBar');
this.#installSetting('notificationSetting');
this.#installSetting('notificationDrawAttention');
this.#installSetting('audioMessage');
this.#installSetting('audioNotification');
this.#installSetting('countMutedConversations');
this.installSetting('sentMediaQualitySetting');
this.installSetting('textFormatting');
this.#installSetting('sentMediaQualitySetting');
this.#installSetting('textFormatting');
this.installSetting('autoConvertEmoji');
this.installSetting('autoDownloadUpdate');
this.installSetting('autoLaunch');
this.#installSetting('autoConvertEmoji');
this.#installSetting('autoDownloadUpdate');
this.#installSetting('autoLaunch');
this.installSetting('alwaysRelayCalls');
this.installSetting('callRingtoneNotification');
this.installSetting('callSystemNotification');
this.installSetting('incomingCallNotification');
this.#installSetting('alwaysRelayCalls');
this.#installSetting('callRingtoneNotification');
this.#installSetting('callSystemNotification');
this.#installSetting('incomingCallNotification');
// Media settings
this.installSetting('preferredAudioInputDevice');
this.installSetting('preferredAudioOutputDevice');
this.installSetting('preferredVideoInputDevice');
this.#installSetting('preferredAudioInputDevice');
this.#installSetting('preferredAudioOutputDevice');
this.#installSetting('preferredVideoInputDevice');
this.installSetting('lastSyncTime');
this.installSetting('universalExpireTimer');
this.#installSetting('lastSyncTime');
this.#installSetting('universalExpireTimer');
this.installSetting('hasStoriesDisabled');
this.installSetting('zoomFactor');
this.#installSetting('hasStoriesDisabled');
this.#installSetting('zoomFactor');
this.installSetting('phoneNumberDiscoverabilitySetting');
this.installSetting('phoneNumberSharingSetting');
this.#installSetting('phoneNumberDiscoverabilitySetting');
this.#installSetting('phoneNumberSharingSetting');
this.installEphemeralSetting('themeSetting');
this.installEphemeralSetting('systemTraySetting');
this.installEphemeralSetting('localeOverride');
this.installEphemeralSetting('spellCheck');
this.#installEphemeralSetting('themeSetting');
this.#installEphemeralSetting('systemTraySetting');
this.#installEphemeralSetting('localeOverride');
this.#installEphemeralSetting('spellCheck');
installPermissionsHandler({ session: session.defaultSession, userConfig });
@ -142,8 +140,8 @@ export class SettingsChannel extends EventEmitter {
});
ipc.on('settings:response', (_event, seq, error, value) => {
const entry = this.responseQueue.get(seq);
this.responseQueue.delete(seq);
const entry = this.#responseQueue.get(seq);
this.#responseQueue.delete(seq);
if (!entry) {
return;
}
@ -157,15 +155,15 @@ export class SettingsChannel extends EventEmitter {
});
}
private waitForResponse<Value>(): { promise: Promise<Value>; seq: number } {
const seq = this.responseSeq;
#waitForResponse<Value>(): { promise: Promise<Value>; seq: number } {
const seq = this.#responseSeq;
// eslint-disable-next-line no-bitwise
this.responseSeq = (this.responseSeq + 1) & 0x7fffffff;
this.#responseSeq = (this.#responseSeq + 1) & 0x7fffffff;
const { promise, resolve, reject } = explodePromise<Value>();
this.responseQueue.set(seq, { resolve, reject });
this.#responseQueue.set(seq, { resolve, reject });
return { seq, promise };
}
@ -173,12 +171,12 @@ export class SettingsChannel extends EventEmitter {
public getSettingFromMainWindow<Name extends keyof IPCEventsValuesType>(
name: Name
): Promise<IPCEventsValuesType[Name]> {
const { mainWindow } = this;
const mainWindow = this.#mainWindow;
if (!mainWindow || !mainWindow.webContents) {
throw new Error('No main window');
}
const { seq, promise } = this.waitForResponse<IPCEventsValuesType[Name]>();
const { seq, promise } = this.#waitForResponse<IPCEventsValuesType[Name]>();
mainWindow.webContents.send(`settings:get:${name}`, { seq });
@ -189,12 +187,12 @@ export class SettingsChannel extends EventEmitter {
name: Name,
value: IPCEventsValuesType[Name]
): Promise<void> {
const { mainWindow } = this;
const mainWindow = this.#mainWindow;
if (!mainWindow || !mainWindow.webContents) {
throw new Error('No main window');
}
const { seq, promise } = this.waitForResponse<void>();
const { seq, promise } = this.#waitForResponse<void>();
mainWindow.webContents.send(`settings:set:${name}`, { seq, value });
@ -205,19 +203,19 @@ export class SettingsChannel extends EventEmitter {
name: Name,
args: ReadonlyArray<unknown>
): Promise<unknown> {
const { mainWindow } = this;
const mainWindow = this.#mainWindow;
if (!mainWindow || !mainWindow.webContents) {
throw new Error('Main window not found');
}
const { seq, promise } = this.waitForResponse<unknown>();
const { seq, promise } = this.#waitForResponse<unknown>();
mainWindow.webContents.send(`settings:call:${name}`, { seq, args });
return promise;
}
private installCallback<Name extends keyof IPCEventsCallbacksType>(
#installCallback<Name extends keyof IPCEventsCallbacksType>(
name: Name
): void {
ipc.handle(`settings:call:${name}`, async (_event, args) => {
@ -225,7 +223,7 @@ export class SettingsChannel extends EventEmitter {
});
}
private installSetting<Name extends keyof IPCEventsValuesType>(
#installSetting<Name extends keyof IPCEventsValuesType>(
name: Name,
{
getter = true,
@ -249,7 +247,7 @@ export class SettingsChannel extends EventEmitter {
});
}
private installEphemeralSetting<Name extends keyof EphemeralSettings>(
#installEphemeralSetting<Name extends keyof EphemeralSettings>(
name: Name
): void {
ipc.handle(`settings:get:${name}`, async () => {
@ -276,7 +274,7 @@ export class SettingsChannel extends EventEmitter {
// to main the event 'preferences-changed'.
this.emit('ephemeral-setting-changed');
const { mainWindow } = this;
const mainWindow = this.#mainWindow;
if (!mainWindow || !mainWindow.webContents) {
return;
}

View file

@ -15,11 +15,11 @@ export class MediaEditorFabricCropRect extends fabric.Rect {
...(options || {}),
});
this.on('scaling', this.containBounds);
this.on('moving', this.containBounds);
this.on('scaling', this.#containBounds);
this.on('moving', this.#containBounds);
}
private containBounds = () => {
#containBounds = () => {
if (!this.canvas) {
return;
}

View file

@ -295,21 +295,16 @@ export class ConversationModel extends window.Backbone
throttledUpdateSharedGroups?: () => Promise<void>;
private cachedIdenticon?: CachedIdenticon;
#cachedIdenticon?: CachedIdenticon;
public isFetchingUUID?: boolean;
private lastIsTyping?: boolean;
private muteTimer?: NodeJS.Timeout;
private isInReduxBatch = false;
private privVerifiedEnum?: typeof window.textsecure.storage.protocol.VerifiedStatus;
private isShuttingDown = false;
private savePromises = new Set<Promise<void>>();
#lastIsTyping?: boolean;
#muteTimer?: NodeJS.Timeout;
#isInReduxBatch = false;
#privVerifiedEnum?: typeof window.textsecure.storage.protocol.VerifiedStatus;
#isShuttingDown = false;
#savePromises = new Set<Promise<void>>();
override defaults(): Partial<ConversationAttributesType> {
return {
@ -364,7 +359,7 @@ export class ConversationModel extends window.Backbone
this.storeName = 'conversations';
this.privVerifiedEnum = window.textsecure.storage.protocol.VerifiedStatus;
this.#privVerifiedEnum = window.textsecure.storage.protocol.VerifiedStatus;
// This may be overridden by window.ConversationController.getOrCreate, and signify
// our first save to the database. Or first fetch from the database.
@ -409,7 +404,7 @@ export class ConversationModel extends window.Backbone
this.unset('tokens');
this.on('change:members change:membersV2', this.fetchContacts);
this.on('change:active_at', this.onActiveAtChange);
this.on('change:active_at', this.#onActiveAtChange);
this.typingRefreshTimer = null;
this.typingPauseTimer = null;
@ -436,7 +431,7 @@ export class ConversationModel extends window.Backbone
this.oldCachedProps = this.cachedProps;
}
this.cachedProps = null;
this.trigger('props-change', this, this.isInReduxBatch);
this.trigger('props-change', this, this.#isInReduxBatch);
}
);
@ -477,13 +472,13 @@ export class ConversationModel extends window.Backbone
}
addSavePromise(promise: Promise<void>): void {
this.savePromises.add(promise);
this.#savePromises.add(promise);
}
removeSavePromise(promise: Promise<void>): void {
this.savePromises.delete(promise);
this.#savePromises.delete(promise);
}
getSavePromises(): Array<Promise<void>> {
return Array.from(this.savePromises);
return Array.from(this.#savePromises);
}
toSenderKeyTarget(): SenderKeyTargetType {
@ -503,12 +498,12 @@ export class ConversationModel extends window.Backbone
};
}
private get verifiedEnum(): typeof window.textsecure.storage.protocol.VerifiedStatus {
strictAssert(this.privVerifiedEnum, 'ConversationModel not initialize');
return this.privVerifiedEnum;
get #verifiedEnum(): typeof window.textsecure.storage.protocol.VerifiedStatus {
strictAssert(this.#privVerifiedEnum, 'ConversationModel not initialize');
return this.#privVerifiedEnum;
}
private isMemberRequestingToJoin(serviceId: ServiceIdString): boolean {
#isMemberRequestingToJoin(serviceId: ServiceIdString): boolean {
return isMemberRequestingToJoin(this.attributes, serviceId);
}
@ -544,7 +539,7 @@ export class ConversationModel extends window.Backbone
});
}
private async promotePendingMember(
async #promotePendingMember(
serviceIdKind: ServiceIdKind
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
@ -594,7 +589,7 @@ export class ConversationModel extends window.Backbone
});
}
private async denyPendingApprovalRequest(
async #denyPendingApprovalRequest(
aci: AciString
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
@ -602,7 +597,7 @@ export class ConversationModel extends window.Backbone
// This user's pending state may have changed in the time between the user's
// button press and when we get here. It's especially important to check here
// in conflict/retry cases.
if (!this.isMemberRequestingToJoin(aci)) {
if (!this.#isMemberRequestingToJoin(aci)) {
log.warn(
`denyPendingApprovalRequest/${idLog}: ${aci} is not requesting ` +
'to join the group. Returning early.'
@ -718,13 +713,13 @@ export class ConversationModel extends window.Backbone
});
}
private async removePendingMember(
async #removePendingMember(
serviceIds: ReadonlyArray<ServiceIdString>
): Promise<Proto.GroupChange.Actions | undefined> {
return removePendingMember(this.attributes, serviceIds);
}
private async removeMember(
async #removeMember(
serviceId: ServiceIdString
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
@ -748,7 +743,7 @@ export class ConversationModel extends window.Backbone
});
}
private async toggleAdminChange(
async #toggleAdminChange(
serviceId: ServiceIdString
): Promise<Proto.GroupChange.Actions | undefined> {
if (!isGroupV2(this.attributes)) {
@ -1355,7 +1350,7 @@ export class ConversationModel extends window.Backbone
// `sendTypingMessage`. The first 'sendTypingMessage' job to run will
// pick it and reset it back to `undefined` so that later jobs will
// in effect be ignored.
this.lastIsTyping = isTyping;
this.#lastIsTyping = isTyping;
// If captchas are active, then we should drop typing messages because
// they're less important and could overwhelm the queue.
@ -1377,7 +1372,7 @@ export class ConversationModel extends window.Backbone
return;
}
if (this.lastIsTyping === undefined) {
if (this.#lastIsTyping === undefined) {
log.info(`sendTypingMessage(${this.idForLogging()}): ignoring`);
return;
}
@ -1392,10 +1387,10 @@ export class ConversationModel extends window.Backbone
recipientId,
groupId,
groupMembers,
isTyping: this.lastIsTyping,
isTyping: this.#lastIsTyping,
timestamp,
};
this.lastIsTyping = undefined;
this.#lastIsTyping = undefined;
log.info(
`sendTypingMessage(${this.idForLogging()}): sending ${content.isTyping}`
@ -1491,14 +1486,12 @@ export class ConversationModel extends window.Backbone
message: MessageAttributesType,
{ isJustSent }: { isJustSent: boolean } = { isJustSent: false }
): Promise<void> {
await this.beforeAddSingleMessage(message);
this.doAddSingleMessage(message, { isJustSent });
await this.#beforeAddSingleMessage(message);
this.#doAddSingleMessage(message, { isJustSent });
this.debouncedUpdateLastMessage();
}
private async beforeAddSingleMessage(
message: MessageAttributesType
): Promise<void> {
async #beforeAddSingleMessage(message: MessageAttributesType): Promise<void> {
await hydrateStoryContext(message.id, undefined, { shouldSave: true });
if (!this.newMessageQueue) {
@ -1514,7 +1507,7 @@ export class ConversationModel extends window.Backbone
});
}
private doAddSingleMessage(
#doAddSingleMessage(
message: MessageAttributesType,
{ isJustSent }: { isJustSent: boolean }
): void {
@ -1551,7 +1544,7 @@ export class ConversationModel extends window.Backbone
}
}
private async setInProgressFetch(): Promise<() => void> {
async #setInProgressFetch(): Promise<() => void> {
const logId = `setInProgressFetch(${this.idForLogging()})`;
while (this.inProgressFetch != null) {
log.warn(`${logId}: blocked, waiting`);
@ -1598,7 +1591,7 @@ export class ConversationModel extends window.Backbone
return;
}
const finish = await this.setInProgressFetch();
const finish = await this.#setInProgressFetch();
log.info(`${logId}: starting`);
try {
let metrics = await getMessageMetricsForConversation({
@ -1676,7 +1669,7 @@ export class ConversationModel extends window.Backbone
conversationId,
TimelineMessageLoadingState.DoingInitialLoad
);
let finish: undefined | (() => void) = await this.setInProgressFetch();
let finish: undefined | (() => void) = await this.#setInProgressFetch();
const preloadedId = getPreloadedConversationId(
window.reduxStore.getState()
@ -1797,7 +1790,7 @@ export class ConversationModel extends window.Backbone
conversationId,
TimelineMessageLoadingState.LoadingOlderMessages
);
const finish = await this.setInProgressFetch();
const finish = await this.#setInProgressFetch();
try {
const message = await getMessageById(oldestMessageId);
@ -1854,7 +1847,7 @@ export class ConversationModel extends window.Backbone
conversationId,
TimelineMessageLoadingState.LoadingNewerMessages
);
const finish = await this.setInProgressFetch();
const finish = await this.#setInProgressFetch();
try {
const message = await getMessageById(newestMessageId);
@ -1911,7 +1904,7 @@ export class ConversationModel extends window.Backbone
);
let { onFinish: finish } = options;
if (!finish) {
finish = await this.setInProgressFetch();
finish = await this.#setInProgressFetch();
}
try {
@ -2528,7 +2521,7 @@ export class ConversationModel extends window.Backbone
name: 'promotePendingMember',
usingCredentialsFrom: [ourConversation],
createGroupChange: () =>
this.promotePendingMember(ServiceIdKind.ACI),
this.#promotePendingMember(ServiceIdKind.ACI),
});
} else if (
ourPni &&
@ -2539,7 +2532,7 @@ export class ConversationModel extends window.Backbone
name: 'promotePendingMember',
usingCredentialsFrom: [ourConversation],
createGroupChange: () =>
this.promotePendingMember(ServiceIdKind.PNI),
this.#promotePendingMember(ServiceIdKind.PNI),
});
} else if (isGroupV2(this.attributes) && this.isMember(ourAci)) {
log.info(
@ -2662,7 +2655,7 @@ export class ConversationModel extends window.Backbone
name: 'cancelJoinRequest',
usingCredentialsFrom: [],
inviteLinkPassword,
createGroupChange: () => this.denyPendingApprovalRequest(ourAci),
createGroupChange: () => this.#denyPendingApprovalRequest(ourAci),
});
}
@ -2680,20 +2673,20 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'delete',
usingCredentialsFrom: [],
createGroupChange: () => this.removePendingMember([ourAci]),
createGroupChange: () => this.#removePendingMember([ourAci]),
});
} else if (this.isMember(ourAci)) {
await this.modifyGroupV2({
name: 'delete',
usingCredentialsFrom: [ourConversation],
createGroupChange: () => this.removeMember(ourAci),
createGroupChange: () => this.#removeMember(ourAci),
});
// Keep PNI in pending if ACI was a member.
} else if (ourPni && this.isMemberPending(ourPni)) {
await this.modifyGroupV2({
name: 'delete',
usingCredentialsFrom: [],
createGroupChange: () => this.removePendingMember([ourPni]),
createGroupChange: () => this.#removePendingMember([ourPni]),
syncMessageOnly: true,
});
} else {
@ -2765,7 +2758,7 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'toggleAdmin',
usingCredentialsFrom: [member],
createGroupChange: () => this.toggleAdminChange(serviceId),
createGroupChange: () => this.#toggleAdminChange(serviceId),
});
}
@ -2786,26 +2779,26 @@ export class ConversationModel extends window.Backbone
`removeFromGroupV2/${logId}`
);
if (this.isMemberRequestingToJoin(serviceId)) {
if (this.#isMemberRequestingToJoin(serviceId)) {
strictAssert(isAciString(serviceId), 'Requesting member is not ACI');
await this.modifyGroupV2({
name: 'denyPendingApprovalRequest',
usingCredentialsFrom: [],
createGroupChange: () => this.denyPendingApprovalRequest(serviceId),
createGroupChange: () => this.#denyPendingApprovalRequest(serviceId),
extraConversationsForSend: [conversationId],
});
} else if (this.isMemberPending(serviceId)) {
await this.modifyGroupV2({
name: 'removePendingMember',
usingCredentialsFrom: [],
createGroupChange: () => this.removePendingMember([serviceId]),
createGroupChange: () => this.#removePendingMember([serviceId]),
extraConversationsForSend: [conversationId],
});
} else if (this.isMember(serviceId)) {
await this.modifyGroupV2({
name: 'removeFromGroup',
usingCredentialsFrom: [pendingMember],
createGroupChange: () => this.removeMember(serviceId),
createGroupChange: () => this.#removeMember(serviceId),
extraConversationsForSend: [conversationId],
});
} else {
@ -2818,13 +2811,13 @@ export class ConversationModel extends window.Backbone
async safeGetVerified(): Promise<number> {
const serviceId = this.getServiceId();
if (!serviceId) {
return this.verifiedEnum.DEFAULT;
return this.#verifiedEnum.DEFAULT;
}
try {
return await window.textsecure.storage.protocol.getVerified(serviceId);
} catch {
return this.verifiedEnum.DEFAULT;
return this.#verifiedEnum.DEFAULT;
}
}
@ -2856,24 +2849,24 @@ export class ConversationModel extends window.Backbone
}
setVerifiedDefault(): Promise<boolean> {
const { DEFAULT } = this.verifiedEnum;
const { DEFAULT } = this.#verifiedEnum;
return this.queueJob('setVerifiedDefault', () =>
this._setVerified(DEFAULT)
this.#_setVerified(DEFAULT)
);
}
setVerified(): Promise<boolean> {
const { VERIFIED } = this.verifiedEnum;
return this.queueJob('setVerified', () => this._setVerified(VERIFIED));
const { VERIFIED } = this.#verifiedEnum;
return this.queueJob('setVerified', () => this.#_setVerified(VERIFIED));
}
setUnverified(): Promise<boolean> {
const { UNVERIFIED } = this.verifiedEnum;
return this.queueJob('setUnverified', () => this._setVerified(UNVERIFIED));
const { UNVERIFIED } = this.#verifiedEnum;
return this.queueJob('setUnverified', () => this.#_setVerified(UNVERIFIED));
}
private async _setVerified(verified: number): Promise<boolean> {
const { VERIFIED, DEFAULT } = this.verifiedEnum;
async #_setVerified(verified: number): Promise<boolean> {
const { VERIFIED, DEFAULT } = this.#verifiedEnum;
if (!isDirectConversation(this.attributes)) {
throw new Error(
@ -2886,7 +2879,7 @@ export class ConversationModel extends window.Backbone
const beginningVerified = this.get('verified') ?? DEFAULT;
const keyChange = false;
if (aci) {
if (verified === this.verifiedEnum.DEFAULT) {
if (verified === this.#verifiedEnum.DEFAULT) {
await window.textsecure.storage.protocol.setVerified(aci, verified);
} else {
await window.textsecure.storage.protocol.setVerified(aci, verified, {
@ -2963,7 +2956,7 @@ export class ConversationModel extends window.Backbone
isVerified(): boolean {
if (isDirectConversation(this.attributes)) {
return this.get('verified') === this.verifiedEnum.VERIFIED;
return this.get('verified') === this.#verifiedEnum.VERIFIED;
}
const contacts = this.contactCollection;
@ -2988,8 +2981,8 @@ export class ConversationModel extends window.Backbone
if (isDirectConversation(this.attributes)) {
const verified = this.get('verified');
return (
verified !== this.verifiedEnum.VERIFIED &&
verified !== this.verifiedEnum.DEFAULT
verified !== this.#verifiedEnum.VERIFIED &&
verified !== this.#verifiedEnum.DEFAULT
);
}
@ -3659,7 +3652,7 @@ export class ConversationModel extends window.Backbone
): Promise<T> {
const logId = `conversation.queueJob(${this.idForLogging()}, ${name})`;
if (this.isShuttingDown) {
if (this.#isShuttingDown) {
log.warn(`${logId}: shutting down, can't accept more work`);
throw new Error(`${logId}: shutting down, can't accept more work`);
}
@ -3898,13 +3891,13 @@ export class ConversationModel extends window.Backbone
}
batchReduxChanges(callback: () => void): void {
strictAssert(!this.isInReduxBatch, 'Nested redux batching is not allowed');
this.isInReduxBatch = true;
strictAssert(!this.#isInReduxBatch, 'Nested redux batching is not allowed');
this.#isInReduxBatch = true;
batchDispatch(() => {
try {
callback();
} finally {
this.isInReduxBatch = false;
this.#isInReduxBatch = false;
}
});
}
@ -3935,7 +3928,7 @@ export class ConversationModel extends window.Backbone
);
if (!dontAddMessage) {
this.doAddSingleMessage(message, { isJustSent: true });
this.#doAddSingleMessage(message, { isJustSent: true });
}
const notificationData = getNotificationDataForMessage(message);
const draftProperties = dontClearDraft
@ -4167,7 +4160,7 @@ export class ConversationModel extends window.Backbone
const renderStart = Date.now();
// Perform asynchronous tasks before entering the batching mode
await this.beforeAddSingleMessage(model.attributes);
await this.#beforeAddSingleMessage(model.attributes);
if (sticker) {
await addStickerPackReference(model.id, sticker.packId);
@ -4425,7 +4418,7 @@ export class ConversationModel extends window.Backbone
}
}
private async onActiveAtChange(): Promise<void> {
async #onActiveAtChange(): Promise<void> {
if (this.get('active_at') && this.get('messagesDeleted')) {
this.set('messagesDeleted', false);
await DataWriter.updateConversation(this.attributes);
@ -5382,8 +5375,8 @@ export class ConversationModel extends window.Backbone
}
startMuteTimer({ viaStorageServiceSync = false } = {}): void {
clearTimeoutIfNecessary(this.muteTimer);
this.muteTimer = undefined;
clearTimeoutIfNecessary(this.#muteTimer);
this.#muteTimer = undefined;
const muteExpiresAt = this.get('muteExpiresAt');
if (isNumber(muteExpiresAt) && muteExpiresAt < Number.MAX_SAFE_INTEGER) {
@ -5393,7 +5386,7 @@ export class ConversationModel extends window.Backbone
return;
}
this.muteTimer = setTimeout(() => this.setMuteExpiration(0), delay);
this.#muteTimer = setTimeout(() => this.setMuteExpiration(0), delay);
}
}
@ -5520,12 +5513,12 @@ export class ConversationModel extends window.Backbone
return {
url: avatarUrl,
absolutePath: saveToDisk
? await this.getTemporaryAvatarPath()
? await this.#getTemporaryAvatarPath()
: undefined,
};
}
const { url, path } = await this.getIdenticon({
const { url, path } = await this.#getIdenticon({
saveToDisk,
});
return {
@ -5534,7 +5527,7 @@ export class ConversationModel extends window.Backbone
};
}
private async getTemporaryAvatarPath(): Promise<string | undefined> {
async #getTemporaryAvatarPath(): Promise<string | undefined> {
const avatar = getAvatar(this.attributes);
if (avatar?.path == null) {
return undefined;
@ -5574,9 +5567,7 @@ export class ConversationModel extends window.Backbone
}
}
private async getIdenticon({
saveToDisk,
}: { saveToDisk?: boolean } = {}): Promise<{
async #getIdenticon({ saveToDisk }: { saveToDisk?: boolean } = {}): Promise<{
url: string;
path?: string;
}> {
@ -5587,7 +5578,7 @@ export class ConversationModel extends window.Backbone
if (isContact) {
const text = (title && getInitials(title)) || '#';
const cached = this.cachedIdenticon;
const cached = this.#cachedIdenticon;
if (cached && cached.text === text && cached.color === color) {
return { ...cached };
}
@ -5603,11 +5594,11 @@ export class ConversationModel extends window.Backbone
}
);
this.cachedIdenticon = { text, color, url, path };
this.#cachedIdenticon = { text, color, url, path };
return { url, path };
}
const cached = this.cachedIdenticon;
const cached = this.#cachedIdenticon;
if (cached && cached.color === color) {
return { ...cached };
}
@ -5620,7 +5611,7 @@ export class ConversationModel extends window.Backbone
}
);
this.cachedIdenticon = { color, url, path };
this.#cachedIdenticon = { color, url, path };
return { url, path };
}
@ -5820,10 +5811,10 @@ export class ConversationModel extends window.Backbone
return undefined;
}
return this.getGroupStorySendMode();
return this.#getGroupStorySendMode();
}
private getGroupStorySendMode(): StorySendMode {
#getGroupStorySendMode(): StorySendMode {
strictAssert(
!isDirectConversation(this.attributes),
'Must be a group to have send story mode'
@ -5846,11 +5837,11 @@ export class ConversationModel extends window.Backbone
log.warn(
`conversation ${this.idForLogging()} jobQueue stop accepting new work`
);
this.isShuttingDown = true;
this.#isShuttingDown = true;
}, 10 * SECOND);
await this.jobQueue.onIdle();
this.isShuttingDown = true;
this.#isShuttingDown = true;
clearTimeout(to);
log.info(`conversation ${this.idForLogging()} jobQueue shutdown complete`);

View file

@ -9,7 +9,7 @@ type StringKey<T> = keyof T & string;
export class MessageModel {
public get id(): string {
return this._attributes.id;
return this.#_attributes.id;
}
public get<keyName extends StringKey<MessageAttributesType>>(
@ -21,7 +21,7 @@ export class MessageModel {
attributes: Partial<MessageAttributesType>,
{ noTrigger }: { noTrigger?: boolean } = {}
): void {
this._attributes = {
this.#_attributes = {
...this.attributes,
...attributes,
};
@ -34,12 +34,12 @@ export class MessageModel {
}
public get attributes(): Readonly<MessageAttributesType> {
return this._attributes;
return this.#_attributes;
}
private _attributes: MessageAttributesType;
#_attributes: MessageAttributesType;
constructor(attributes: MessageAttributesType) {
this._attributes = attributes;
this.#_attributes = attributes;
this.set(
window.Signal.Types.Message.initializeSchemaVersion({

View file

@ -68,47 +68,46 @@ const FUSE_OPTIONS = {
};
export class MemberRepository {
private members: ReadonlyArray<MemberType>;
private isFuseReady = false;
private fuse = new Fuse<MemberType>([], FUSE_OPTIONS);
#members: ReadonlyArray<MemberType>;
#isFuseReady = false;
#fuse = new Fuse<MemberType>([], FUSE_OPTIONS);
constructor(conversations: ReadonlyArray<ConversationType> = []) {
this.members = _toMembers(conversations);
this.#members = _toMembers(conversations);
}
updateMembers(conversations: ReadonlyArray<ConversationType>): void {
this.members = _toMembers(conversations);
this.isFuseReady = false;
this.#members = _toMembers(conversations);
this.#isFuseReady = false;
}
getMembers(omitId?: string): ReadonlyArray<MemberType> {
if (omitId) {
return this.members.filter(({ id }) => id !== omitId);
return this.#members.filter(({ id }) => id !== omitId);
}
return this.members;
return this.#members;
}
getMemberById(id?: string): MemberType | undefined {
return id
? this.members.find(({ id: memberId }) => memberId === id)
? this.#members.find(({ id: memberId }) => memberId === id)
: undefined;
}
getMemberByAci(aci?: AciString): MemberType | undefined {
return aci
? this.members.find(({ aci: memberAci }) => memberAci === aci)
? this.#members.find(({ aci: memberAci }) => memberAci === aci)
: undefined;
}
search(pattern: string, omitId?: string): ReadonlyArray<MemberType> {
if (!this.isFuseReady) {
this.fuse.setCollection(this.members);
this.isFuseReady = true;
if (!this.#isFuseReady) {
this.#fuse.setCollection(this.#members);
this.#isFuseReady = true;
}
const results = this.fuse
const results = this.#fuse
.search(removeDiacritics(pattern))
.map(result => result.item);

View file

@ -26,8 +26,8 @@ const MIN_REFRESH_DELAY = MINUTE;
let idCounter = 1;
export class RoutineProfileRefresher {
private started = false;
private id: number;
#started = false;
#id: number;
constructor(
private readonly options: {
@ -39,20 +39,20 @@ export class RoutineProfileRefresher {
// We keep track of how many of these classes we create, because we suspect that
// there might be too many...
idCounter += 1;
this.id = idCounter;
this.#id = idCounter;
log.info(
`Creating new RoutineProfileRefresher instance with id ${this.id}`
`Creating new RoutineProfileRefresher instance with id ${this.#id}`
);
}
public async start(): Promise<void> {
const logId = `RoutineProfileRefresher.start/${this.id}`;
const logId = `RoutineProfileRefresher.start/${this.#id}`;
if (this.started) {
if (this.#started) {
log.warn(`${logId}: already started!`);
return;
}
this.started = true;
this.#started = true;
const { storage, getAllConversations, getOurConversationId } = this.options;
@ -81,7 +81,7 @@ export class RoutineProfileRefresher {
allConversations: getAllConversations(),
ourConversationId,
storage,
id: this.id,
id: this.#id,
});
} catch (error) {
log.error(`${logId}: failure`, Errors.toLogFormat(error));

View file

@ -19,95 +19,91 @@ const ACTIVE_EVENTS = [
class ActiveWindowService {
// This starting value might be wrong but we should get an update from the main process
// soon. We'd rather report that the window is inactive so we can show notifications.
private isInitialized = false;
#isInitialized = false;
private isFocused = false;
private activeCallbacks: Array<() => void> = [];
private changeCallbacks: Array<(isActive: boolean) => void> = [];
private lastActiveEventAt = -Infinity;
private callActiveCallbacks: () => void;
#isFocused = false;
#activeCallbacks: Array<() => void> = [];
#changeCallbacks: Array<(isActive: boolean) => void> = [];
#lastActiveEventAt = -Infinity;
#callActiveCallbacks: () => void;
constructor() {
this.callActiveCallbacks = throttle(() => {
this.activeCallbacks.forEach(callback => callback());
this.#callActiveCallbacks = throttle(() => {
this.#activeCallbacks.forEach(callback => callback());
}, LISTENER_THROTTLE_TIME);
}
// These types aren't perfectly accurate, but they make this class easier to test.
initialize(document: EventTarget, ipc: NodeJS.EventEmitter): void {
if (this.isInitialized) {
if (this.#isInitialized) {
throw new Error(
'Active window service should not be initialized multiple times'
);
}
this.isInitialized = true;
this.#isInitialized = true;
this.lastActiveEventAt = Date.now();
this.#lastActiveEventAt = Date.now();
const onActiveEvent = this.onActiveEvent.bind(this);
const onActiveEvent = this.#onActiveEvent.bind(this);
ACTIVE_EVENTS.forEach((eventName: string) => {
document.addEventListener(eventName, onActiveEvent, true);
});
// We don't know for sure that we'll get the right data over IPC so we use `unknown`.
ipc.on('set-window-focus', (_event: unknown, isFocused: unknown) => {
this.setWindowFocus(Boolean(isFocused));
this.#setWindowFocus(Boolean(isFocused));
});
}
isActive(): boolean {
return (
this.isFocused && Date.now() < this.lastActiveEventAt + ACTIVE_TIMEOUT
this.#isFocused && Date.now() < this.#lastActiveEventAt + ACTIVE_TIMEOUT
);
}
registerForActive(callback: () => void): void {
this.activeCallbacks.push(callback);
this.#activeCallbacks.push(callback);
}
unregisterForActive(callback: () => void): void {
this.activeCallbacks = this.activeCallbacks.filter(
this.#activeCallbacks = this.#activeCallbacks.filter(
item => item !== callback
);
}
registerForChange(callback: (isActive: boolean) => void): void {
this.changeCallbacks.push(callback);
this.#changeCallbacks.push(callback);
}
unregisterForChange(callback: (isActive: boolean) => void): void {
this.changeCallbacks = this.changeCallbacks.filter(
this.#changeCallbacks = this.#changeCallbacks.filter(
item => item !== callback
);
}
private onActiveEvent(): void {
this.updateState(() => {
this.lastActiveEventAt = Date.now();
#onActiveEvent(): void {
this.#updateState(() => {
this.#lastActiveEventAt = Date.now();
});
}
private setWindowFocus(isFocused: boolean): void {
this.updateState(() => {
this.isFocused = isFocused;
#setWindowFocus(isFocused: boolean): void {
this.#updateState(() => {
this.#isFocused = isFocused;
});
}
private updateState(fn: () => void): void {
#updateState(fn: () => void): void {
const wasActiveBefore = this.isActive();
fn();
const isActiveNow = this.isActive();
if (!wasActiveBefore && isActiveNow) {
this.callActiveCallbacks();
this.#callActiveCallbacks();
}
if (wasActiveBefore !== isActiveNow) {
for (const callback of this.changeCallbacks) {
for (const callback of this.#changeCallbacks) {
callback(isActiveNow);
}
}

View file

@ -26,7 +26,7 @@ export class MessageCache {
return instance;
}
private state = {
#state = {
messages: new Map<string, MessageModel>(),
messageIdsBySender: new Map<string, string>(),
messageIdsBySentAt: new Map<number, Array<string>>(),
@ -60,14 +60,14 @@ export class MessageCache {
return existing;
}
this.addMessageToCache(message);
this.#addMessageToCache(message);
return message;
}
// Finds a message in the cache by sender identifier
public findBySender(senderIdentifier: string): MessageModel | undefined {
const id = this.state.messageIdsBySender.get(senderIdentifier);
const id = this.#state.messageIdsBySender.get(senderIdentifier);
if (!id) {
return undefined;
}
@ -77,12 +77,12 @@ export class MessageCache {
// Finds a message in the cache by Id
public getById(id: string): MessageModel | undefined {
const message = this.state.messages.get(id);
const message = this.#state.messages.get(id);
if (!message) {
return undefined;
}
this.state.lastAccessedAt.set(id, Date.now());
this.#state.lastAccessedAt.set(id, Date.now());
return message;
}
@ -92,7 +92,7 @@ export class MessageCache {
sentAt: number,
predicate: (model: MessageModel) => boolean
): Promise<MessageModel | undefined> {
const items = this.state.messageIdsBySentAt.get(sentAt) ?? [];
const items = this.#state.messageIdsBySentAt.get(sentAt) ?? [];
const inMemory = items
.map(id => this.getById(id))
.filter(isNotNil)
@ -113,12 +113,12 @@ export class MessageCache {
// Deletes the message from our cache
public unregister(id: string): void {
const message = this.state.messages.get(id);
const message = this.#state.messages.get(id);
if (!message) {
return;
}
this.removeMessage(id);
this.#removeMessage(id);
}
// Evicts messages from the message cache if they have not been accessed past
@ -126,8 +126,8 @@ export class MessageCache {
public deleteExpiredMessages(expiryTime: number): void {
const now = Date.now();
for (const [messageId, message] of this.state.messages) {
const timeLastAccessed = this.state.lastAccessedAt.get(messageId) ?? 0;
for (const [messageId, message] of this.#state.messages) {
const timeLastAccessed = this.#state.lastAccessedAt.get(messageId) ?? 0;
const conversation = getMessageConversation(message.attributes);
const state = window.reduxStore.getState();
@ -177,7 +177,7 @@ export class MessageCache {
};
};
for (const [, message] of this.state.messages) {
for (const [, message] of this.#state.messages) {
if (message.get('conversationId') !== obsoleteId) {
continue;
}
@ -213,12 +213,12 @@ export class MessageCache {
return;
}
this.state.messageIdsBySender.delete(
this.#state.messageIdsBySender.delete(
getSenderIdentifier(message.attributes)
);
const { id, sent_at: sentAt } = message.attributes;
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
const previousIdsBySentAt = this.#state.messageIdsBySentAt.get(sentAt);
let nextIdsBySentAtSet: Set<string>;
if (previousIdsBySentAt) {
@ -228,29 +228,29 @@ export class MessageCache {
nextIdsBySentAtSet = new Set([id]);
}
this.state.lastAccessedAt.set(id, Date.now());
this.state.messageIdsBySender.set(
this.#state.lastAccessedAt.set(id, Date.now());
this.#state.messageIdsBySender.set(
getSenderIdentifier(message.attributes),
id
);
this.throttledUpdateRedux(message.attributes);
this.#throttledUpdateRedux(message.attributes);
}
// Helpers
private addMessageToCache(message: MessageModel): void {
#addMessageToCache(message: MessageModel): void {
if (!message.id) {
return;
}
if (this.state.messages.has(message.id)) {
this.state.lastAccessedAt.set(message.id, Date.now());
if (this.#state.messages.has(message.id)) {
this.#state.lastAccessedAt.set(message.id, Date.now());
return;
}
const { id, sent_at: sentAt } = message.attributes;
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
const previousIdsBySentAt = this.#state.messageIdsBySentAt.get(sentAt);
let nextIdsBySentAtSet: Set<string>;
if (previousIdsBySentAt) {
@ -260,41 +260,44 @@ export class MessageCache {
nextIdsBySentAtSet = new Set([id]);
}
this.state.messages.set(message.id, message);
this.state.lastAccessedAt.set(message.id, Date.now());
this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
this.state.messageIdsBySender.set(
this.#state.messages.set(message.id, message);
this.#state.lastAccessedAt.set(message.id, Date.now());
this.#state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
this.#state.messageIdsBySender.set(
getSenderIdentifier(message.attributes),
id
);
}
private removeMessage(messageId: string): void {
const message = this.state.messages.get(messageId);
#removeMessage(messageId: string): void {
const message = this.#state.messages.get(messageId);
if (!message) {
return;
}
const { id, sent_at: sentAt } = message.attributes;
const nextIdsBySentAtSet =
new Set(this.state.messageIdsBySentAt.get(sentAt)) || new Set();
new Set(this.#state.messageIdsBySentAt.get(sentAt)) || new Set();
nextIdsBySentAtSet.delete(id);
if (nextIdsBySentAtSet.size) {
this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
this.#state.messageIdsBySentAt.set(
sentAt,
Array.from(nextIdsBySentAtSet)
);
} else {
this.state.messageIdsBySentAt.delete(sentAt);
this.#state.messageIdsBySentAt.delete(sentAt);
}
this.state.messages.delete(messageId);
this.state.lastAccessedAt.delete(messageId);
this.state.messageIdsBySender.delete(
this.#state.messages.delete(messageId);
this.#state.lastAccessedAt.delete(messageId);
this.#state.messageIdsBySender.delete(
getSenderIdentifier(message.attributes)
);
}
private updateRedux(attributes: MessageAttributesType) {
#updateRedux(attributes: MessageAttributesType) {
if (!window.reduxActions) {
return;
}
@ -320,21 +323,21 @@ export class MessageCache {
);
}
private throttledReduxUpdaters = new LRUCache<
#throttledReduxUpdaters = new LRUCache<
string,
typeof this.updateRedux
(attributes: MessageAttributesType) => void
>({
max: MAX_THROTTLED_REDUX_UPDATERS,
});
private throttledUpdateRedux(attributes: MessageAttributesType) {
let updater = this.throttledReduxUpdaters.get(attributes.id);
#throttledUpdateRedux(attributes: MessageAttributesType) {
let updater = this.#throttledReduxUpdaters.get(attributes.id);
if (!updater) {
updater = throttle(this.updateRedux.bind(this), 200, {
updater = throttle(this.#updateRedux.bind(this), 200, {
leading: true,
trailing: true,
});
this.throttledReduxUpdaters.set(attributes.id, updater);
this.#throttledReduxUpdaters.set(attributes.id, updater);
}
updater(attributes);

View file

@ -8,13 +8,13 @@ import { waitForOnline } from '../util/waitForOnline';
// This is only exported for testing.
export class AreWeASubscriberService {
private readonly queue = new LatestQueue();
readonly #queue = new LatestQueue();
update(
storage: Pick<StorageInterface, 'get' | 'put' | 'onready'>,
server: Pick<WebAPIType, 'getHasSubscription' | 'isOnline'>
): void {
this.queue.add(async () => {
this.#queue.add(async () => {
await new Promise<void>(resolve => storage.onready(resolve));
const subscriberId = storage.get('subscriberId');

View file

@ -7,40 +7,40 @@ import { requestMicrophonePermissions } from '../util/requestMicrophonePermissio
import { WebAudioRecorder } from '../WebAudioRecorder';
export class RecorderClass {
private context?: AudioContext;
private input?: GainNode;
private recorder?: WebAudioRecorder;
private source?: MediaStreamAudioSourceNode;
private stream?: MediaStream;
private blob?: Blob;
private resolve?: (blob: Blob) => void;
#context?: AudioContext;
#input?: GainNode;
#recorder?: WebAudioRecorder;
#source?: MediaStreamAudioSourceNode;
#stream?: MediaStream;
#blob?: Blob;
#resolve?: (blob: Blob) => void;
clear(): void {
this.blob = undefined;
this.resolve = undefined;
this.#blob = undefined;
this.#resolve = undefined;
if (this.source) {
this.source.disconnect();
this.source = undefined;
if (this.#source) {
this.#source.disconnect();
this.#source = undefined;
}
if (this.recorder) {
if (this.recorder.isRecording()) {
this.recorder.cancelRecording();
if (this.#recorder) {
if (this.#recorder.isRecording()) {
this.#recorder.cancelRecording();
}
// Reach in and terminate the web worker used by WebAudioRecorder, otherwise
// it gets leaked due to a reference cycle with its onmessage listener
this.recorder.worker?.terminate();
this.recorder = undefined;
this.#recorder.worker?.terminate();
this.#recorder = undefined;
}
this.input = undefined;
this.stream = undefined;
this.#input = undefined;
this.#stream = undefined;
if (this.context) {
void this.context.close();
this.context = undefined;
if (this.#context) {
void this.#context.close();
this.#context = undefined;
}
}
@ -55,11 +55,11 @@ export class RecorderClass {
this.clear();
this.context = new AudioContext();
this.input = this.context.createGain();
this.#context = new AudioContext();
this.#input = this.#context.createGain();
this.recorder = new WebAudioRecorder(
this.input,
this.#recorder = new WebAudioRecorder(
this.#input,
{
timeLimit: 60 + 3600, // one minute more than our UI-imposed limit
},
@ -76,24 +76,24 @@ export class RecorderClass {
audio: { mandatory: { googAutoGainControl: false } } as any,
});
if (!this.context || !this.input) {
if (!this.#context || !this.#input) {
const err = new Error(
'Recorder/getUserMedia/stream: Missing context or input!'
);
this.onError(this.recorder, String(err));
this.onError(this.#recorder, String(err));
throw err;
}
this.source = this.context.createMediaStreamSource(stream);
this.source.connect(this.input);
this.stream = stream;
this.#source = this.#context.createMediaStreamSource(stream);
this.#source.connect(this.#input);
this.#stream = stream;
} catch (err) {
log.error('Recorder.onGetUserMediaError:', Errors.toLogFormat(err));
this.clear();
throw err;
}
if (this.recorder) {
this.recorder.startRecording();
if (this.#recorder) {
this.#recorder.startRecording();
return true;
}
@ -101,34 +101,34 @@ export class RecorderClass {
}
async stop(): Promise<Blob | undefined> {
if (!this.recorder) {
if (!this.#recorder) {
return;
}
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
if (this.#stream) {
this.#stream.getTracks().forEach(track => track.stop());
}
if (this.blob) {
return this.blob;
if (this.#blob) {
return this.#blob;
}
const promise = new Promise<Blob>(resolve => {
this.resolve = resolve;
this.#resolve = resolve;
});
this.recorder.finishRecording();
this.#recorder.finishRecording();
return promise;
}
onComplete(_recorder: WebAudioRecorder, blob: Blob): void {
this.blob = blob;
this.resolve?.(blob);
this.#blob = blob;
this.#resolve?.(blob);
}
onError(_recorder: WebAudioRecorder, error: string): void {
if (!this.recorder) {
if (!this.#recorder) {
log.warn('Recorder/onError: Called with no recorder');
return;
}
@ -139,11 +139,11 @@ export class RecorderClass {
}
getBlob(): Blob {
if (!this.blob) {
if (!this.#blob) {
throw new Error('no blob found');
}
return this.blob;
return this.#blob;
}
}

View file

@ -25,7 +25,7 @@ export type DownloadOptionsType = Readonly<{
}>;
export class BackupAPI {
private cachedBackupInfo = new Map<
#cachedBackupInfo = new Map<
BackupCredentialType,
GetBackupInfoResponseType
>();
@ -38,23 +38,23 @@ export class BackupAPI {
this.credentials.getHeadersForToday(type)
)
);
await Promise.all(headers.map(h => this.server.refreshBackup(h)));
await Promise.all(headers.map(h => this.#server.refreshBackup(h)));
}
public async getInfo(
credentialType: BackupCredentialType
): Promise<GetBackupInfoResponseType> {
const backupInfo = await this.server.getBackupInfo(
const backupInfo = await this.#server.getBackupInfo(
await this.credentials.getHeadersForToday(credentialType)
);
this.cachedBackupInfo.set(credentialType, backupInfo);
this.#cachedBackupInfo.set(credentialType, backupInfo);
return backupInfo;
}
private async getCachedInfo(
async #getCachedInfo(
credentialType: BackupCredentialType
): Promise<GetBackupInfoResponseType> {
const cached = this.cachedBackupInfo.get(credentialType);
const cached = this.#cachedBackupInfo.get(credentialType);
if (cached) {
return cached;
}
@ -63,15 +63,15 @@ export class BackupAPI {
}
public async getMediaDir(): Promise<string> {
return (await this.getCachedInfo(BackupCredentialType.Media)).mediaDir;
return (await this.#getCachedInfo(BackupCredentialType.Media)).mediaDir;
}
public async getBackupDir(): Promise<string> {
return (await this.getCachedInfo(BackupCredentialType.Media))?.backupDir;
return (await this.#getCachedInfo(BackupCredentialType.Media))?.backupDir;
}
public async upload(filePath: string, fileSize: number): Promise<void> {
const form = await this.server.getBackupUploadForm(
const form = await this.#server.getBackupUploadForm(
await this.credentials.getHeadersForToday(BackupCredentialType.Messages)
);
@ -95,7 +95,7 @@ export class BackupAPI {
BackupCredentialType.Messages
);
return this.server.getBackupStream({
return this.#server.getBackupStream({
cdn,
backupDir,
backupName,
@ -111,7 +111,7 @@ export class BackupAPI {
onProgress,
abortSignal,
}: DownloadOptionsType): Promise<Readable> {
const response = await this.server.getTransferArchive({
const response = await this.#server.getTransferArchive({
abortSignal,
});
@ -128,7 +128,7 @@ export class BackupAPI {
const { cdn, key } = response;
return this.server.getEphemeralBackupStream({
return this.#server.getEphemeralBackupStream({
cdn,
key,
downloadOffset,
@ -138,7 +138,7 @@ export class BackupAPI {
}
public async getMediaUploadForm(): Promise<AttachmentUploadFormResponseType> {
return this.server.getBackupMediaUploadForm(
return this.#server.getBackupMediaUploadForm(
await this.credentials.getHeadersForToday(BackupCredentialType.Media)
);
}
@ -146,7 +146,7 @@ export class BackupAPI {
public async backupMediaBatch(
items: ReadonlyArray<BackupMediaItemType>
): Promise<BackupMediaBatchResponseType> {
return this.server.backupMediaBatch({
return this.#server.backupMediaBatch({
headers: await this.credentials.getHeadersForToday(
BackupCredentialType.Media
),
@ -161,7 +161,7 @@ export class BackupAPI {
cursor?: string;
limit: number;
}): Promise<BackupListMediaResponseType> {
return this.server.backupListMedia({
return this.#server.backupListMedia({
headers: await this.credentials.getHeadersForToday(
BackupCredentialType.Media
),
@ -171,10 +171,10 @@ export class BackupAPI {
}
public clearCache(): void {
this.cachedBackupInfo.clear();
this.#cachedBackupInfo.clear();
}
private get server(): WebAPIType {
get #server(): WebAPIType {
const { server } = window.textsecure;
strictAssert(server, 'server not available');

View file

@ -44,15 +44,14 @@ const FETCH_INTERVAL = 3 * DAY;
const BACKUP_CDN_READ_CREDENTIALS_VALID_DURATION = 12 * HOUR;
export class BackupCredentials {
private activeFetch: ReturnType<typeof this.fetch> | undefined;
private cachedCdnReadCredentials: Record<
number,
BackupCdnReadCredentialType
> = {};
private readonly fetchBackoff = new BackOff(FIBONACCI_TIMEOUTS);
#activeFetch: Promise<ReadonlyArray<BackupCredentialWrapperType>> | undefined;
#cachedCdnReadCredentials: Record<number, BackupCdnReadCredentialType> = {};
readonly #fetchBackoff = new BackOff(FIBONACCI_TIMEOUTS);
public start(): void {
this.scheduleFetch();
this.#scheduleFetch();
}
public async getForToday(
@ -73,7 +72,7 @@ export class BackupCredentials {
}
// Start with cache
let credentials = this.getFromCache();
let credentials = this.#getFromCache();
let result = credentials.find(({ type, redemptionTimeMs }) => {
return type === credentialType && redemptionTimeMs === now;
@ -81,7 +80,7 @@ export class BackupCredentials {
if (result === undefined) {
log.info(`BackupCredentials: cache miss for ${now}`);
credentials = await this.fetch();
credentials = await this.#fetch();
result = credentials.find(({ type, redemptionTimeMs }) => {
return type === credentialType && redemptionTimeMs === now;
});
@ -143,7 +142,7 @@ export class BackupCredentials {
// Backup CDN read credentials are short-lived; we'll just cache them in memory so
// that they get invalidated for any reason, we'll fetch new ones on app restart
const cachedCredentialsForThisCdn = this.cachedCdnReadCredentials[cdn];
const cachedCredentialsForThisCdn = this.#cachedCdnReadCredentials[cdn];
if (
cachedCredentialsForThisCdn &&
@ -163,7 +162,7 @@ export class BackupCredentials {
cdn,
});
this.cachedCdnReadCredentials[cdn] = {
this.#cachedCdnReadCredentials[cdn] = {
credentials: newCredentials,
cdnNumber: cdn,
retrievedAtMs,
@ -172,7 +171,7 @@ export class BackupCredentials {
return newCredentials;
}
private scheduleFetch(): void {
#scheduleFetch(): void {
const lastFetchAt = window.storage.get(
'backupCombinedCredentialsLastRequestTime',
0
@ -181,45 +180,45 @@ export class BackupCredentials {
const delay = Math.max(0, nextFetchAt - Date.now());
log.info(`BackupCredentials: scheduling fetch in ${delay}ms`);
setTimeout(() => drop(this.runPeriodicFetch()), delay);
setTimeout(() => drop(this.#runPeriodicFetch()), delay);
}
private async runPeriodicFetch(): Promise<void> {
async #runPeriodicFetch(): Promise<void> {
try {
log.info('BackupCredentials: run periodic fetch');
await this.fetch();
await this.#fetch();
const now = Date.now();
await window.storage.put('backupCombinedCredentialsLastRequestTime', now);
this.fetchBackoff.reset();
this.scheduleFetch();
this.#fetchBackoff.reset();
this.#scheduleFetch();
} catch (error) {
const delay = this.fetchBackoff.getAndIncrement();
const delay = this.#fetchBackoff.getAndIncrement();
log.error(
'BackupCredentials: periodic fetch failed with ' +
`error: ${toLogFormat(error)}, retrying in ${delay}ms`
);
setTimeout(() => this.scheduleFetch(), delay);
setTimeout(() => this.#scheduleFetch(), delay);
}
}
private async fetch(): Promise<ReadonlyArray<BackupCredentialWrapperType>> {
if (this.activeFetch) {
return this.activeFetch;
async #fetch(): Promise<ReadonlyArray<BackupCredentialWrapperType>> {
if (this.#activeFetch) {
return this.#activeFetch;
}
const promise = this.doFetch();
this.activeFetch = promise;
const promise = this.#doFetch();
this.#activeFetch = promise;
try {
return await promise;
} finally {
this.activeFetch = undefined;
this.#activeFetch = undefined;
}
}
private async doFetch(): Promise<ReadonlyArray<BackupCredentialWrapperType>> {
async #doFetch(): Promise<ReadonlyArray<BackupCredentialWrapperType>> {
log.info('BackupCredentials: fetching');
const now = Date.now();
@ -227,8 +226,8 @@ export class BackupCredentials {
const endDayInMs = toDayMillis(now + 6 * DAY);
// And fetch missing credentials
const messagesCtx = this.getAuthContext(BackupCredentialType.Messages);
const mediaCtx = this.getAuthContext(BackupCredentialType.Media);
const messagesCtx = this.#getAuthContext(BackupCredentialType.Messages);
const mediaCtx = this.#getAuthContext(BackupCredentialType.Media);
const { server } = window.textsecure;
strictAssert(server, 'server not available');
@ -333,7 +332,7 @@ export class BackupCredentials {
// Add cached credentials that are still in the date range, and not in
// the response.
for (const cached of this.getFromCache()) {
for (const cached of this.#getFromCache()) {
const { type, redemptionTimeMs } = cached;
if (
!(startDayInMs <= redemptionTimeMs && redemptionTimeMs <= endDayInMs)
@ -348,7 +347,7 @@ export class BackupCredentials {
}
result.sort((a, b) => a.redemptionTimeMs - b.redemptionTimeMs);
await this.updateCache(result);
await this.#updateCache(result);
const startMs = result[0].redemptionTimeMs;
const endMs = result[result.length - 1].redemptionTimeMs;
@ -359,7 +358,7 @@ export class BackupCredentials {
return result;
}
private getAuthContext(
#getAuthContext(
credentialType: BackupCredentialType
): BackupAuthCredentialRequestContext {
let key: BackupKey;
@ -376,11 +375,11 @@ export class BackupCredentials {
);
}
private getFromCache(): ReadonlyArray<BackupCredentialWrapperType> {
#getFromCache(): ReadonlyArray<BackupCredentialWrapperType> {
return window.storage.get('backupCombinedCredentials', []);
}
private async updateCache(
async #updateCache(
values: ReadonlyArray<BackupCredentialWrapperType>
): Promise<void> {
await window.storage.put('backupCombinedCredentials', values);
@ -394,7 +393,7 @@ export class BackupCredentials {
// Called when backup tier changes or when userChanged event
public async clearCache(): Promise<void> {
this.cachedCdnReadCredentials = {};
await this.updateCache([]);
this.#cachedCdnReadCredentials = {};
await this.#updateCache([]);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -91,10 +91,11 @@ export type ImportOptionsType = Readonly<{
}>;
export class BackupsService {
private isStarted = false;
private isRunning: 'import' | 'export' | false = false;
private downloadController: AbortController | undefined;
private downloadRetryPromise:
#isStarted = false;
#isRunning: 'import' | 'export' | false = false;
#downloadController: AbortController | undefined;
#downloadRetryPromise:
| ExplodePromiseResultType<RetryBackupImportValue>
| undefined;
@ -102,19 +103,19 @@ export class BackupsService {
public readonly api = new BackupAPI(this.credentials);
public start(): void {
if (this.isStarted) {
if (this.#isStarted) {
log.warn('BackupsService: already started');
return;
}
this.isStarted = true;
this.#isStarted = true;
log.info('BackupsService: starting...');
setInterval(() => {
drop(this.runPeriodicRefresh());
drop(this.#runPeriodicRefresh());
}, BACKUP_REFRESH_INTERVAL);
drop(this.runPeriodicRefresh());
drop(this.#runPeriodicRefresh());
this.credentials.start();
window.Whisper.events.on('userChanged', () => {
@ -142,13 +143,13 @@ export class BackupsService {
while (true) {
try {
// eslint-disable-next-line no-await-in-loop
hasBackup = await this.doDownloadAndImport({
hasBackup = await this.#doDownloadAndImport({
downloadPath: absoluteDownloadPath,
onProgress: options.onProgress,
ephemeralKey,
});
} catch (error) {
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
this.#downloadRetryPromise = explodePromise<RetryBackupImportValue>();
let installerError: InstallScreenBackupError;
if (error instanceof RelinkRequestedError) {
@ -158,7 +159,7 @@ export class BackupsService {
Errors.toLogFormat(error)
);
// eslint-disable-next-line no-await-in-loop
await this.unlinkAndDeleteAllData();
await this.#unlinkAndDeleteAllData();
} else if (error instanceof UnsupportedBackupVersion) {
installerError = InstallScreenBackupError.UnsupportedVersion;
log.error(
@ -178,7 +179,7 @@ export class BackupsService {
Errors.toLogFormat(error)
);
// eslint-disable-next-line no-await-in-loop
await this.unlinkAndDeleteAllData();
await this.#unlinkAndDeleteAllData();
} else {
log.error(
'backups.downloadAndImport: unknown error, prompting user to retry'
@ -191,7 +192,7 @@ export class BackupsService {
});
// eslint-disable-next-line no-await-in-loop
const nextStep = await this.downloadRetryPromise.promise;
const nextStep = await this.#downloadRetryPromise.promise;
if (nextStep === 'retry') {
continue;
}
@ -218,11 +219,11 @@ export class BackupsService {
}
public retryDownload(): void {
if (!this.downloadRetryPromise) {
if (!this.#downloadRetryPromise) {
return;
}
this.downloadRetryPromise.resolve('retry');
this.#downloadRetryPromise.resolve('retry');
}
public async upload(): Promise<void> {
@ -274,7 +275,7 @@ export class BackupsService {
const chunks = new Array<Uint8Array>();
sink.on('data', chunk => chunks.push(chunk));
await this.exportBackup(sink, backupLevel, backupType);
await this.#exportBackup(sink, backupLevel, backupType);
return Bytes.concatenate(chunks);
}
@ -285,7 +286,7 @@ export class BackupsService {
backupLevel: BackupLevel = BackupLevel.Free,
backupType = BackupType.Ciphertext
): Promise<number> {
const size = await this.exportBackup(
const size = await this.#exportBackup(
createWriteStream(path),
backupLevel,
backupType
@ -318,12 +319,12 @@ export class BackupsService {
}
public cancelDownload(): void {
if (this.downloadController) {
if (this.#downloadController) {
log.warn('importBackup: canceling download');
this.downloadController.abort();
this.downloadController = undefined;
if (this.downloadRetryPromise) {
this.downloadRetryPromise.resolve('cancel');
this.#downloadController.abort();
this.#downloadController = undefined;
if (this.#downloadRetryPromise) {
this.#downloadRetryPromise.resolve('cancel');
}
} else {
log.error('importBackup: not canceling download, not running');
@ -338,12 +339,12 @@ export class BackupsService {
onProgress,
}: ImportOptionsType = {}
): Promise<void> {
strictAssert(!this.isRunning, 'BackupService is already running');
strictAssert(!this.#isRunning, 'BackupService is already running');
window.IPC.startTrackingQueryStats();
log.info(`importBackup: starting ${backupType}...`);
this.isRunning = 'import';
this.#isRunning = 'import';
const importStart = Date.now();
await DataWriter.disableMessageInsertTriggers();
@ -439,7 +440,7 @@ export class BackupsService {
throw error;
} finally {
this.isRunning = false;
this.#isRunning = false;
await DataWriter.enableMessageInsertTriggersAndBackfill();
window.IPC.stopTrackingQueryStats({ epochName: 'Backup Import' });
@ -494,7 +495,7 @@ export class BackupsService {
return { isInBackupTier: true, cdnNumber: storedInfo.cdnNumber };
}
private async doDownloadAndImport({
async #doDownloadAndImport({
downloadPath,
ephemeralKey,
onProgress,
@ -502,8 +503,8 @@ export class BackupsService {
const controller = new AbortController();
// Abort previous download
this.downloadController?.abort();
this.downloadController = controller;
this.#downloadController?.abort();
this.#downloadController = controller;
let downloadOffset = 0;
try {
@ -591,7 +592,7 @@ export class BackupsService {
return false;
}
this.downloadController = undefined;
this.#downloadController = undefined;
try {
// Too late to cancel now, make sure we are unlinked if the process
@ -633,15 +634,15 @@ export class BackupsService {
return true;
}
private async exportBackup(
async #exportBackup(
sink: Writable,
backupLevel: BackupLevel = BackupLevel.Free,
backupType = BackupType.Ciphertext
): Promise<number> {
strictAssert(!this.isRunning, 'BackupService is already running');
strictAssert(!this.#isRunning, 'BackupService is already running');
log.info('exportBackup: starting...');
this.isRunning = 'export';
this.#isRunning = 'export';
try {
// TODO (DESKTOP-7168): Update mock-server to support this endpoint
@ -694,11 +695,11 @@ export class BackupsService {
return totalBytes;
} finally {
log.info('exportBackup: finished...');
this.isRunning = false;
this.#isRunning = false;
}
}
private async runPeriodicRefresh(): Promise<void> {
async #runPeriodicRefresh(): Promise<void> {
try {
await this.api.refresh();
log.info('Backup: refreshed');
@ -707,7 +708,7 @@ export class BackupsService {
}
}
private async unlinkAndDeleteAllData() {
async #unlinkAndDeleteAllData() {
try {
await window.textsecure.server?.unlink();
} catch (e) {
@ -730,10 +731,10 @@ export class BackupsService {
}
public isImportRunning(): boolean {
return this.isRunning === 'import';
return this.#isRunning === 'import';
}
public isExportRunning(): boolean {
return this.isRunning === 'export';
return this.#isRunning === 'export';
}
}

View file

@ -6,49 +6,49 @@ import { Buffer } from 'node:buffer';
import { InputStream } from '@signalapp/libsignal-client/dist/io';
export class FileStream extends InputStream {
private file: FileHandle | undefined;
private position = 0;
private buffer = Buffer.alloc(16 * 1024);
private initPromise: Promise<unknown> | undefined;
#file: FileHandle | undefined;
#position = 0;
#buffer = Buffer.alloc(16 * 1024);
#initPromise: Promise<unknown> | undefined;
constructor(private readonly filePath: string) {
super();
}
public async close(): Promise<void> {
await this.initPromise;
await this.file?.close();
await this.#initPromise;
await this.#file?.close();
}
async read(amount: number): Promise<Buffer> {
const file = await this.lazyOpen();
if (this.buffer.length < amount) {
this.buffer = Buffer.alloc(amount);
const file = await this.#lazyOpen();
if (this.#buffer.length < amount) {
this.#buffer = Buffer.alloc(amount);
}
const { bytesRead } = await file.read(
this.buffer,
this.#buffer,
0,
amount,
this.position
this.#position
);
this.position += bytesRead;
return this.buffer.slice(0, bytesRead);
this.#position += bytesRead;
return this.#buffer.slice(0, bytesRead);
}
async skip(amount: number): Promise<void> {
this.position += amount;
this.#position += amount;
}
private async lazyOpen(): Promise<FileHandle> {
await this.initPromise;
async #lazyOpen(): Promise<FileHandle> {
await this.#initPromise;
if (this.file) {
return this.file;
if (this.#file) {
return this.#file;
}
const filePromise = open(this.filePath);
this.initPromise = filePromise;
this.file = await filePromise;
return this.file;
this.#initPromise = filePromise;
this.#file = await filePromise;
return this.#file;
}
}

File diff suppressed because it is too large Load diff

View file

@ -14,15 +14,15 @@ import { MessageModel } from '../models/messages';
import { cleanupMessages } from '../util/cleanup';
class ExpiringMessagesDeletionService {
public update: typeof this.checkExpiringMessages;
public update: () => void;
private timeout?: ReturnType<typeof setTimeout>;
#timeout?: ReturnType<typeof setTimeout>;
constructor() {
this.update = debounce(this.checkExpiringMessages, 1000);
this.update = debounce(this.#checkExpiringMessages, 1000);
}
private async destroyExpiredMessages() {
async #destroyExpiredMessages() {
try {
window.SignalContext.log.info(
'destroyExpiredMessages: Loading messages...'
@ -74,7 +74,7 @@ class ExpiringMessagesDeletionService {
void this.update();
}
private async checkExpiringMessages() {
async #checkExpiringMessages() {
window.SignalContext.log.info(
'checkExpiringMessages: checking for expiring messages'
);
@ -105,8 +105,8 @@ class ExpiringMessagesDeletionService {
).toISOString()}; waiting ${wait} ms before clearing`
);
clearTimeoutIfNecessary(this.timeout);
this.timeout = setTimeout(this.destroyExpiredMessages.bind(this), wait);
clearTimeoutIfNecessary(this.#timeout);
this.#timeout = setTimeout(this.#destroyExpiredMessages.bind(this), wait);
}
}

View file

@ -79,27 +79,25 @@ export const FALLBACK_NOTIFICATION_TITLE = 'Signal';
// [0]: https://github.com/electron/electron/issues/15364
// [1]: https://github.com/electron/electron/issues/21646
class NotificationService extends EventEmitter {
private i18n?: LocalizerType;
private storage?: StorageInterface;
#i18n?: LocalizerType;
#storage?: StorageInterface;
public isEnabled = false;
private lastNotification: null | Notification = null;
private notificationData: null | NotificationDataType = null;
#lastNotification: null | Notification = null;
#notificationData: null | NotificationDataType = null;
// Testing indicated that trying to create/destroy notifications too quickly
// resulted in notifications that stuck around forever, requiring the user
// to manually close them. This introduces a minimum amount of time between calls,
// and batches up the quick successive update() calls we get from an incoming
// read sync, which might have a number of messages referenced inside of it.
private update: () => unknown;
#update: () => unknown;
constructor() {
super();
this.update = debounce(this.fastUpdate.bind(this), 1000);
this.#update = debounce(this.#fastUpdate.bind(this), 1000);
}
public initialize({
@ -107,13 +105,13 @@ class NotificationService extends EventEmitter {
storage,
}: Readonly<{ i18n: LocalizerType; storage: StorageInterface }>): void {
log.info('NotificationService initialized');
this.i18n = i18n;
this.storage = storage;
this.#i18n = i18n;
this.#storage = storage;
}
private getStorage(): StorageInterface {
if (this.storage) {
return this.storage;
#getStorage(): StorageInterface {
if (this.#storage) {
return this.#storage;
}
log.error(
@ -122,9 +120,9 @@ class NotificationService extends EventEmitter {
return window.storage;
}
private getI18n(): LocalizerType {
if (this.i18n) {
return this.i18n;
#getI18n(): LocalizerType {
if (this.#i18n) {
return this.#i18n;
}
log.error(
@ -141,8 +139,8 @@ class NotificationService extends EventEmitter {
log.info(
'NotificationService: adding a notification and requesting an update'
);
this.notificationData = notificationData;
this.update();
this.#notificationData = notificationData;
this.#update();
}
/**
@ -190,7 +188,7 @@ class NotificationService extends EventEmitter {
})
);
} else {
this.lastNotification?.close();
this.#lastNotification?.close();
const notification = new window.Notification(title, {
body: OS.isLinux() ? filterNotificationText(message) : message,
@ -226,7 +224,7 @@ class NotificationService extends EventEmitter {
}
};
this.lastNotification = notification;
this.#lastNotification = notification;
}
if (!silent) {
@ -254,7 +252,7 @@ class NotificationService extends EventEmitter {
targetAuthorAci?: string;
targetTimestamp?: number;
}>): void {
if (!this.notificationData) {
if (!this.#notificationData) {
log.info('NotificationService#removeBy: no notification data');
return;
}
@ -262,12 +260,12 @@ class NotificationService extends EventEmitter {
let shouldClear = false;
if (
conversationId &&
this.notificationData.conversationId === conversationId
this.#notificationData.conversationId === conversationId
) {
log.info('NotificationService#removeBy: conversation ID matches');
shouldClear = true;
}
if (messageId && this.notificationData.messageId === messageId) {
if (messageId && this.#notificationData.messageId === messageId) {
log.info('NotificationService#removeBy: message ID matches');
shouldClear = true;
}
@ -276,7 +274,7 @@ class NotificationService extends EventEmitter {
return;
}
const { reaction } = this.notificationData;
const { reaction } = this.#notificationData;
if (
reaction &&
emoji &&
@ -290,13 +288,13 @@ class NotificationService extends EventEmitter {
}
this.clear();
this.update();
this.#update();
}
private fastUpdate(): void {
const storage = this.getStorage();
const i18n = this.getI18n();
const { notificationData } = this;
#fastUpdate(): void {
const storage = this.#getStorage();
const i18n = this.#getI18n();
const notificationData = this.#notificationData;
const isAppFocused = window.SignalContext.activeWindowService.isActive();
const userSetting = this.getNotificationSetting();
@ -308,9 +306,9 @@ class NotificationService extends EventEmitter {
if (!notificationData) {
drop(window.IPC.clearAllWindowsNotifications());
}
} else if (this.lastNotification) {
this.lastNotification.close();
this.lastNotification = null;
} else if (this.#lastNotification) {
this.#lastNotification.close();
this.#lastNotification = null;
}
// This isn't a boolean because TypeScript isn't smart enough to know that, if
@ -326,7 +324,7 @@ class NotificationService extends EventEmitter {
}notification data`
);
if (isAppFocused) {
this.notificationData = null;
this.#notificationData = null;
}
return;
}
@ -422,7 +420,7 @@ class NotificationService extends EventEmitter {
log.info('NotificationService: requesting a notification to be shown');
this.notificationData = {
this.#notificationData = {
...notificationData,
wasShown: true,
};
@ -444,7 +442,7 @@ class NotificationService extends EventEmitter {
public getNotificationSetting(): NotificationSetting {
return parseNotificationSetting(
this.getStorage().get('notification-setting')
this.#getStorage().get('notification-setting')
);
}
@ -452,8 +450,8 @@ class NotificationService extends EventEmitter {
log.info(
'NotificationService: clearing notification and requesting an update'
);
this.notificationData = null;
this.update();
this.#notificationData = null;
this.#update();
}
// We don't usually call this, but when the process is shutting down, we should at
@ -461,8 +459,8 @@ class NotificationService extends EventEmitter {
// normal debounce.
public fastClear(): void {
log.info('NotificationService: clearing notification and updating');
this.notificationData = null;
this.fastUpdate();
this.#notificationData = null;
this.#fastUpdate();
}
public enable(): void {
@ -470,7 +468,7 @@ class NotificationService extends EventEmitter {
const needUpdate = !this.isEnabled;
this.isEnabled = true;
if (needUpdate) {
this.update();
this.#update();
}
}

View file

@ -9,9 +9,8 @@ import type { StorageInterface } from '../types/Storage.d';
export class OurProfileKeyService {
private getPromise: undefined | Promise<undefined | Uint8Array>;
private promisesBlockingGet: Array<Promise<unknown>> = [];
private storage?: StorageInterface;
#promisesBlockingGet: Array<Promise<unknown>> = [];
#storage?: StorageInterface;
initialize(storage: StorageInterface): void {
log.info('Our profile key service: initializing');
@ -22,9 +21,9 @@ export class OurProfileKeyService {
});
});
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.promisesBlockingGet = [storageReadyPromise];
this.#promisesBlockingGet = [storageReadyPromise];
this.storage = storage;
this.#storage = storage;
}
get(): Promise<undefined | Uint8Array> {
@ -34,44 +33,44 @@ export class OurProfileKeyService {
);
} else {
log.info('Our profile key service: kicking off a new fetch');
this.getPromise = this.doGet();
this.getPromise = this.#doGet();
}
return this.getPromise;
}
async set(newValue: undefined | Uint8Array): Promise<void> {
assertDev(this.storage, 'OurProfileKeyService was not initialized');
assertDev(this.#storage, 'OurProfileKeyService was not initialized');
if (newValue != null) {
strictAssert(
newValue.byteLength > 0,
'Our profile key service: Profile key cannot be empty'
);
log.info('Our profile key service: updating profile key');
await this.storage.put('profileKey', newValue);
await this.#storage.put('profileKey', newValue);
} else {
log.info('Our profile key service: removing profile key');
await this.storage.remove('profileKey');
await this.#storage.remove('profileKey');
}
}
blockGetWithPromise(promise: Promise<unknown>): void {
this.promisesBlockingGet.push(promise);
this.#promisesBlockingGet.push(promise);
}
private async doGet(): Promise<undefined | Uint8Array> {
async #doGet(): Promise<undefined | Uint8Array> {
log.info(
`Our profile key service: waiting for ${this.promisesBlockingGet.length} promises before fetching`
`Our profile key service: waiting for ${this.#promisesBlockingGet.length} promises before fetching`
);
await Promise.allSettled(this.promisesBlockingGet);
this.promisesBlockingGet = [];
await Promise.allSettled(this.#promisesBlockingGet);
this.#promisesBlockingGet = [];
delete this.getPromise;
assertDev(this.storage, 'OurProfileKeyService was not initialized');
assertDev(this.#storage, 'OurProfileKeyService was not initialized');
log.info('Our profile key service: fetching profile key from storage');
const result = this.storage.get('profileKey');
const result = this.#storage.get('profileKey');
if (result === undefined || result instanceof Uint8Array) {
return result;
}

View file

@ -73,15 +73,13 @@ const OBSERVED_CAPABILITY_KEYS = Object.keys({
} satisfies CapabilitiesType) as ReadonlyArray<keyof CapabilitiesType>;
export class ProfileService {
private jobQueue: PQueue;
private jobsByConversationId: Map<string, JobType> = new Map();
private isPaused = false;
#jobQueue: PQueue;
#jobsByConversationId: Map<string, JobType> = new Map();
#isPaused = false;
constructor(private fetchProfile = doGetProfile) {
this.jobQueue = new PQueue({ concurrency: 3, timeout: MINUTE * 2 });
this.jobsByConversationId = new Map();
this.#jobQueue = new PQueue({ concurrency: 3, timeout: MINUTE * 2 });
this.#jobsByConversationId = new Map();
log.info('Profile Service initialized');
}
@ -102,13 +100,13 @@ export class ProfileService {
return;
}
if (this.isPaused) {
if (this.#isPaused) {
throw new Error(
`ProfileService.get: Cannot add job to paused queue for conversation ${preCheckConversation.idForLogging()}`
);
}
const existing = this.jobsByConversationId.get(conversationId);
const existing = this.#jobsByConversationId.get(conversationId);
if (existing) {
return existing.promise;
}
@ -135,7 +133,7 @@ export class ProfileService {
} catch (error) {
reject(error);
if (this.isPaused) {
if (this.#isPaused) {
return;
}
@ -149,7 +147,7 @@ export class ProfileService {
}
}
} finally {
this.jobsByConversationId.delete(conversationId);
this.#jobsByConversationId.delete(conversationId);
const now = Date.now();
const delta = now - jobData.startTime;
@ -158,7 +156,7 @@ export class ProfileService {
`ProfileServices.get: Job for ${conversation.idForLogging()} finished ${delta}ms after queue`
);
}
const remainingItems = this.jobQueue.size;
const remainingItems = this.#jobQueue.size;
if (remainingItems && remainingItems % 10 === 0) {
log.info(
`ProfileServices.get: ${remainingItems} jobs remaining in the queue`
@ -167,14 +165,14 @@ export class ProfileService {
}
};
this.jobsByConversationId.set(conversationId, jobData);
drop(this.jobQueue.add(job));
this.#jobsByConversationId.set(conversationId, jobData);
drop(this.#jobQueue.add(job));
return promise;
}
public clearAll(reason: string): void {
if (this.isPaused) {
if (this.#isPaused) {
log.warn(
`ProfileService.clearAll: Already paused; not clearing; reason: '${reason}'`
);
@ -184,10 +182,10 @@ export class ProfileService {
log.info(`ProfileService.clearAll: Clearing; reason: '${reason}'`);
try {
this.isPaused = true;
this.jobQueue.pause();
this.#isPaused = true;
this.#jobQueue.pause();
this.jobsByConversationId.forEach(job => {
this.#jobsByConversationId.forEach(job => {
job.reject(
new Error(
`ProfileService.clearAll: job cancelled because '${reason}'`
@ -195,33 +193,33 @@ export class ProfileService {
);
});
this.jobsByConversationId.clear();
this.jobQueue.clear();
this.#jobsByConversationId.clear();
this.#jobQueue.clear();
this.jobQueue.start();
this.#jobQueue.start();
} finally {
this.isPaused = false;
this.#isPaused = false;
log.info('ProfileService.clearAll: Done clearing');
}
}
public async pause(timeInMS: number): Promise<void> {
if (this.isPaused) {
if (this.#isPaused) {
log.warn('ProfileService.pause: Already paused, not pausing again.');
return;
}
log.info(`ProfileService.pause: Pausing queue for ${timeInMS}ms`);
this.isPaused = true;
this.jobQueue.pause();
this.#isPaused = true;
this.#jobQueue.pause();
try {
await sleep(timeInMS);
} finally {
log.info('ProfileService.pause: Restarting queue');
this.jobQueue.start();
this.isPaused = false;
this.#jobQueue.start();
this.#isPaused = false;
}
}
}

View file

@ -49,8 +49,8 @@ export type ReleaseNoteType = ReleaseNoteResponseType &
let initComplete = false;
export class ReleaseNotesFetcher {
private timeout: NodeJS.Timeout | undefined;
private isRunning = false;
#timeout: NodeJS.Timeout | undefined;
#isRunning = false;
protected async scheduleUpdateForNow(): Promise<void> {
const now = Date.now();
@ -74,11 +74,11 @@ export class ReleaseNotesFetcher {
waitTime = 0;
}
clearTimeoutIfNecessary(this.timeout);
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
clearTimeoutIfNecessary(this.#timeout);
this.#timeout = setTimeout(() => this.#runWhenOnline(), waitTime);
}
private getOrInitializeVersionWatermark(): string {
#getOrInitializeVersionWatermark(): string {
const versionWatermark = window.textsecure.storage.get(
VERSION_WATERMARK_STORAGE_KEY
);
@ -99,7 +99,7 @@ export class ReleaseNotesFetcher {
return currentVersion;
}
private async getReleaseNote(
async #getReleaseNote(
note: ManifestReleaseNoteType
): Promise<ReleaseNoteType | undefined> {
if (!window.textsecure.server) {
@ -154,7 +154,7 @@ export class ReleaseNotesFetcher {
);
}
private async processReleaseNotes(
async #processReleaseNotes(
notes: ReadonlyArray<ManifestReleaseNoteType>
): Promise<void> {
const sortedNotes = [...notes].sort(
@ -164,7 +164,7 @@ export class ReleaseNotesFetcher {
const hydratedNotes = [];
for (const note of sortedNotes) {
// eslint-disable-next-line no-await-in-loop
hydratedNotes.push(await this.getReleaseNote(note));
hydratedNotes.push(await this.#getReleaseNote(note));
}
if (!hydratedNotes.length) {
log.warn('ReleaseNotesFetcher: No hydrated notes available, stopping');
@ -232,22 +232,22 @@ export class ReleaseNotesFetcher {
);
}
private async scheduleForNextRun(): Promise<void> {
async #scheduleForNextRun(): Promise<void> {
const now = Date.now();
const nextTime = now + FETCH_INTERVAL;
await window.textsecure.storage.put(NEXT_FETCH_TIME_STORAGE_KEY, nextTime);
}
private async run(): Promise<void> {
if (this.isRunning) {
async #run(): Promise<void> {
if (this.#isRunning) {
log.warn('ReleaseNotesFetcher: Already running, preventing reentrancy');
return;
}
this.isRunning = true;
this.#isRunning = true;
log.info('ReleaseNotesFetcher: Starting');
try {
const versionWatermark = this.getOrInitializeVersionWatermark();
const versionWatermark = this.#getOrInitializeVersionWatermark();
log.info(`ReleaseNotesFetcher: Version watermark is ${versionWatermark}`);
if (!window.textsecure.server) {
@ -276,7 +276,7 @@ export class ReleaseNotesFetcher {
log.info(
`ReleaseNotesFetcher: Processing ${validNotes.length} new release notes`
);
drop(this.processReleaseNotes(validNotes));
drop(this.#processReleaseNotes(validNotes));
} else {
log.info('ReleaseNotesFetcher: No new release notes');
}
@ -291,7 +291,7 @@ export class ReleaseNotesFetcher {
log.info('ReleaseNotesFetcher: Manifest hash unchanged');
}
await this.scheduleForNextRun();
await this.#scheduleForNextRun();
this.setTimeoutForNextRun();
} catch (error) {
const errorString =
@ -303,13 +303,13 @@ export class ReleaseNotesFetcher {
);
setTimeout(() => this.setTimeoutForNextRun(), ERROR_RETRY_DELAY);
} finally {
this.isRunning = false;
this.#isRunning = false;
}
}
private runWhenOnline() {
#runWhenOnline() {
if (window.textsecure.server?.isOnline()) {
drop(this.run());
drop(this.#run());
} else {
log.info(
'ReleaseNotesFetcher: We are offline; will fetch when we are next online'

View file

@ -28,16 +28,15 @@ const CLOCK_SKEW_THRESHOLD = 15 * 60 * 1000;
// This is exported for testing.
export class SenderCertificateService {
private server?: WebAPIType;
#server?: WebAPIType;
private fetchPromises: Map<
#fetchPromises: Map<
SenderCertificateMode,
Promise<undefined | SerializedCertificateType>
> = new Map();
private events?: Pick<typeof window.Whisper.events, 'on' | 'off'>;
private storage?: StorageInterface;
#events?: Pick<typeof window.Whisper.events, 'on' | 'off'>;
#storage?: StorageInterface;
initialize({
server,
@ -50,15 +49,15 @@ export class SenderCertificateService {
}): void {
log.info('Sender certificate service initialized');
this.server = server;
this.events = events;
this.storage = storage;
this.#server = server;
this.#events = events;
this.#storage = storage;
}
async get(
mode: SenderCertificateMode
): Promise<undefined | SerializedCertificateType> {
const storedCertificate = this.getStoredCertificate(mode);
const storedCertificate = this.#getStoredCertificate(mode);
if (storedCertificate) {
log.info(
`Sender certificate service found a valid ${modeToLogString(
@ -68,7 +67,7 @@ export class SenderCertificateService {
return storedCertificate;
}
return this.fetchCertificate(mode);
return this.#fetchCertificate(mode);
}
// This is intended to be called when our credentials have been deleted, so any fetches
@ -78,9 +77,9 @@ export class SenderCertificateService {
'Sender certificate service: Clearing in-progress fetches and ' +
'deleting cached certificates'
);
await Promise.all(this.fetchPromises.values());
await Promise.all(this.#fetchPromises.values());
const { storage } = this;
const storage = this.#storage;
assertDev(
storage,
'Sender certificate service method was called before it was initialized'
@ -89,10 +88,10 @@ export class SenderCertificateService {
await storage.remove('senderCertificateNoE164');
}
private getStoredCertificate(
#getStoredCertificate(
mode: SenderCertificateMode
): undefined | SerializedCertificateType {
const { storage } = this;
const storage = this.#storage;
assertDev(
storage,
'Sender certificate service method was called before it was initialized'
@ -109,11 +108,11 @@ export class SenderCertificateService {
return undefined;
}
private fetchCertificate(
#fetchCertificate(
mode: SenderCertificateMode
): Promise<undefined | SerializedCertificateType> {
// This prevents multiple concurrent fetches.
const existingPromise = this.fetchPromises.get(mode);
const existingPromise = this.#fetchPromises.get(mode);
if (existingPromise) {
log.info(
`Sender certificate service was already fetching a ${modeToLogString(
@ -125,28 +124,30 @@ export class SenderCertificateService {
let promise: Promise<undefined | SerializedCertificateType>;
const doFetch = async () => {
const result = await this.fetchAndSaveCertificate(mode);
const result = await this.#fetchAndSaveCertificate(mode);
assertDev(
this.fetchPromises.get(mode) === promise,
this.#fetchPromises.get(mode) === promise,
'Sender certificate service was deleting a different promise than expected'
);
this.fetchPromises.delete(mode);
this.#fetchPromises.delete(mode);
return result;
};
promise = doFetch();
assertDev(
!this.fetchPromises.has(mode),
!this.#fetchPromises.has(mode),
'Sender certificate service somehow already had a promise for this mode'
);
this.fetchPromises.set(mode, promise);
this.#fetchPromises.set(mode, promise);
return promise;
}
private async fetchAndSaveCertificate(
async #fetchAndSaveCertificate(
mode: SenderCertificateMode
): Promise<undefined | SerializedCertificateType> {
const { storage, server, events } = this;
const storage = this.#storage;
const events = this.#events;
const server = this.#server;
assertDev(
storage && server && events,
'Sender certificate service method was called before it was initialized'
@ -162,7 +163,7 @@ export class SenderCertificateService {
let certificateString: string;
try {
certificateString = await this.requestSenderCertificate(mode);
certificateString = await this.#requestSenderCertificate(mode);
} catch (err) {
log.warn(
`Sender certificate service could not fetch a ${modeToLogString(
@ -198,10 +199,10 @@ export class SenderCertificateService {
return serializedCertificate;
}
private async requestSenderCertificate(
async #requestSenderCertificate(
mode: SenderCertificateMode
): Promise<string> {
const { server } = this;
const server = this.#server;
assertDev(
server,
'Sender certificate service method was called before it was initialized'

View file

@ -53,15 +53,15 @@ async function eraseTapToViewMessages() {
}
class TapToViewMessagesDeletionService {
public update: typeof this.checkTapToViewMessages;
public update: () => Promise<void>;
private timeout?: ReturnType<typeof setTimeout>;
#timeout?: ReturnType<typeof setTimeout>;
constructor() {
this.update = debounce(this.checkTapToViewMessages, 1000);
this.update = debounce(this.#checkTapToViewMessages, 1000);
}
private async checkTapToViewMessages() {
async #checkTapToViewMessages() {
const receivedAtMsForOldestTapToViewMessage =
await DataReader.getNextTapToViewMessageTimestampToAgeOut();
if (!receivedAtMsForOldestTapToViewMessage) {
@ -87,8 +87,8 @@ class TapToViewMessagesDeletionService {
wait = 2147483647;
}
clearTimeoutIfNecessary(this.timeout);
this.timeout = setTimeout(async () => {
clearTimeoutIfNecessary(this.#timeout);
this.#timeout = setTimeout(async () => {
await eraseTapToViewMessages();
void this.update();
}, wait);

View file

@ -25,20 +25,20 @@ const CHECK_INTERVAL = DAY;
const STORAGE_SERVICE_TIMEOUT = 30 * MINUTE;
class UsernameIntegrityService {
private isStarted = false;
private readonly backOff = new BackOff(FIBONACCI_TIMEOUTS);
#isStarted = false;
readonly #backOff = new BackOff(FIBONACCI_TIMEOUTS);
async start(): Promise<void> {
if (this.isStarted) {
if (this.#isStarted) {
return;
}
this.isStarted = true;
this.#isStarted = true;
this.scheduleCheck();
this.#scheduleCheck();
}
private scheduleCheck(): void {
#scheduleCheck(): void {
const lastCheckTimestamp = window.storage.get(
'usernameLastIntegrityCheck',
0
@ -46,40 +46,40 @@ class UsernameIntegrityService {
const delay = Math.max(0, lastCheckTimestamp + CHECK_INTERVAL - Date.now());
if (delay === 0) {
log.info('usernameIntegrity: running the check immediately');
drop(this.safeCheck());
drop(this.#safeCheck());
} else {
log.info(`usernameIntegrity: running the check in ${delay}ms`);
setTimeout(() => drop(this.safeCheck()), delay);
setTimeout(() => drop(this.#safeCheck()), delay);
}
}
private async safeCheck(): Promise<void> {
async #safeCheck(): Promise<void> {
try {
await storageJobQueue(() => this.check());
this.backOff.reset();
await storageJobQueue(() => this.#check());
this.#backOff.reset();
await window.storage.put('usernameLastIntegrityCheck', Date.now());
this.scheduleCheck();
this.#scheduleCheck();
} catch (error) {
const delay = this.backOff.getAndIncrement();
const delay = this.#backOff.getAndIncrement();
log.error(
'usernameIntegrity: check failed with ' +
`error: ${Errors.toLogFormat(error)} retrying in ${delay}ms`
);
setTimeout(() => drop(this.safeCheck()), delay);
setTimeout(() => drop(this.#safeCheck()), delay);
}
}
private async check(): Promise<void> {
async #check(): Promise<void> {
if (!isRegistrationDone()) {
return;
}
await this.checkUsername();
await this.checkPhoneNumberSharing();
await this.#checkUsername();
await this.#checkPhoneNumberSharing();
}
private async checkUsername(): Promise<void> {
async #checkUsername(): Promise<void> {
const me = window.ConversationController.getOurConversationOrThrow();
const username = me.get('username');
if (!username) {
@ -124,7 +124,7 @@ class UsernameIntegrityService {
}
}
private async checkPhoneNumberSharing(): Promise<void> {
async #checkPhoneNumberSharing(): Promise<void> {
const me = window.ConversationController.getOurConversationOrThrow();
await getProfile({
@ -150,10 +150,10 @@ class UsernameIntegrityService {
// Since we already run on storage service job queue - don't await the
// promise below (otherwise deadlock will happen).
drop(this.fixProfile());
drop(this.#fixProfile());
}
private async fixProfile(): Promise<void> {
async #fixProfile(): Promise<void> {
const { promise: once, resolve } = explodePromise<void>();
window.Whisper.events.once('storageService:syncComplete', () => resolve());

View file

@ -116,31 +116,25 @@ export type QueryStatsOptions = {
};
export class MainSQL {
private readonly pool = new Array<PoolEntry>();
private pauseWaiters: Array<() => void> | undefined;
private isReady = false;
private onReady: Promise<void> | undefined;
private readonly onExit: Promise<unknown>;
readonly #pool = new Array<PoolEntry>();
#pauseWaiters: Array<() => void> | undefined;
#isReady = false;
#onReady: Promise<void> | undefined;
readonly #onExit: Promise<unknown>;
// Promise resolve callbacks for corruption and readonly errors.
private errorResolvers = new Array<KnownErrorResolverType>();
#errorResolvers = new Array<KnownErrorResolverType>();
private seq = 0;
private logger?: LoggerType;
#seq = 0;
#logger?: LoggerType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private onResponse = new Map<number, PromisePair<any>>();
#onResponse = new Map<number, PromisePair<any>>();
private shouldTimeQueries = false;
#shouldTimeQueries = false;
#shouldTrackQueryStats = false;
private shouldTrackQueryStats = false;
private queryStats?: {
#queryStats?: {
start: number;
statsByQuery: Map<string, QueryStatsType>;
};
@ -148,12 +142,12 @@ export class MainSQL {
constructor() {
const exitPromises = new Array<Promise<void>>();
for (let i = 0; i < WORKER_COUNT; i += 1) {
const { worker, onExit } = this.createWorker();
this.pool.push({ worker, load: 0 });
const { worker, onExit } = this.#createWorker();
this.#pool.push({ worker, load: 0 });
exitPromises.push(onExit);
}
this.onExit = Promise.all(exitPromises);
this.#onExit = Promise.all(exitPromises);
}
public async initialize({
@ -162,19 +156,19 @@ export class MainSQL {
key,
logger,
}: InitializeOptions): Promise<void> {
if (this.isReady || this.onReady) {
if (this.#isReady || this.#onReady) {
throw new Error('Already initialized');
}
this.shouldTimeQueries = Boolean(process.env.TIME_QUERIES);
this.#shouldTimeQueries = Boolean(process.env.TIME_QUERIES);
this.logger = logger;
this.#logger = logger;
this.onReady = (async () => {
const primary = this.pool[0];
const rest = this.pool.slice(1);
this.#onReady = (async () => {
const primary = this.#pool[0];
const rest = this.#pool.slice(1);
await this.send(primary, {
await this.#send(primary, {
type: 'init',
options: { appVersion, configDir, key },
isPrimary: true,
@ -182,7 +176,7 @@ export class MainSQL {
await Promise.all(
rest.map(worker =>
this.send(worker, {
this.#send(worker, {
type: 'init',
options: { appVersion, configDir, key },
isPrimary: false,
@ -191,22 +185,22 @@ export class MainSQL {
);
})();
await this.onReady;
await this.#onReady;
this.onReady = undefined;
this.isReady = true;
this.#onReady = undefined;
this.#isReady = true;
}
public pauseWriteAccess(): void {
strictAssert(this.pauseWaiters == null, 'Already paused');
strictAssert(this.#pauseWaiters == null, 'Already paused');
this.pauseWaiters = [];
this.#pauseWaiters = [];
}
public resumeWriteAccess(): void {
const { pauseWaiters } = this;
const pauseWaiters = this.#pauseWaiters;
strictAssert(pauseWaiters != null, 'Not paused');
this.pauseWaiters = undefined;
this.#pauseWaiters = undefined;
for (const waiter of pauseWaiters) {
waiter();
@ -215,37 +209,39 @@ export class MainSQL {
public whenCorrupted(): Promise<Error> {
const { promise, resolve } = explodePromise<Error>();
this.errorResolvers.push({ kind: SqliteErrorKind.Corrupted, resolve });
this.#errorResolvers.push({ kind: SqliteErrorKind.Corrupted, resolve });
return promise;
}
public whenReadonly(): Promise<Error> {
const { promise, resolve } = explodePromise<Error>();
this.errorResolvers.push({ kind: SqliteErrorKind.Readonly, resolve });
this.#errorResolvers.push({ kind: SqliteErrorKind.Readonly, resolve });
return promise;
}
public async close(): Promise<void> {
if (this.onReady) {
if (this.#onReady) {
try {
await this.onReady;
await this.#onReady;
} catch (err) {
this.logger?.error(`MainSQL close, failed: ${Errors.toLogFormat(err)}`);
this.#logger?.error(
`MainSQL close, failed: ${Errors.toLogFormat(err)}`
);
// Init failed
return;
}
}
if (!this.isReady) {
if (!this.#isReady) {
throw new Error('Not initialized');
}
await this.terminate({ type: 'close' });
await this.onExit;
await this.#terminate({ type: 'close' });
await this.#onExit;
}
public async removeDB(): Promise<void> {
await this.terminate({ type: 'removeDB' });
await this.#terminate({ type: 'removeDB' });
}
public async sqlRead<Method extends keyof ServerReadableDirectInterface>(
@ -261,16 +257,16 @@ export class MainSQL {
// the same temporary table.
const isPaging = PAGING_QUERIES.has(method);
const entry = isPaging ? this.pool.at(-1) : this.getWorker();
const entry = isPaging ? this.#pool.at(-1) : this.#getWorker();
strictAssert(entry != null, 'Must have a pool entry');
const { result, duration } = await this.send<SqlCallResult>(entry, {
const { result, duration } = await this.#send<SqlCallResult>(entry, {
type: 'sqlCall:read',
method,
args,
});
this.traceDuration(method, duration);
this.#traceDuration(method, duration);
return result;
}
@ -285,63 +281,63 @@ export class MainSQL {
duration: number;
}>;
while (this.pauseWaiters != null) {
while (this.#pauseWaiters != null) {
const { promise, resolve } = explodePromise<void>();
this.pauseWaiters.push(resolve);
this.#pauseWaiters.push(resolve);
// eslint-disable-next-line no-await-in-loop
await promise;
}
const primary = this.pool[0];
const primary = this.#pool[0];
const { result, duration } = await this.send<SqlCallResult>(primary, {
const { result, duration } = await this.#send<SqlCallResult>(primary, {
type: 'sqlCall:write',
method,
args,
});
this.traceDuration(method, duration);
this.#traceDuration(method, duration);
return result;
}
public startTrackingQueryStats(): void {
if (this.shouldTrackQueryStats) {
this.logQueryStats({});
this.logger?.info('Resetting query stats');
if (this.#shouldTrackQueryStats) {
this.#logQueryStats({});
this.#logger?.info('Resetting query stats');
}
this.resetQueryStats();
this.shouldTrackQueryStats = true;
this.#resetQueryStats();
this.#shouldTrackQueryStats = true;
}
public stopTrackingQueryStats(options: QueryStatsOptions): void {
if (this.shouldTrackQueryStats) {
this.logQueryStats(options);
if (this.#shouldTrackQueryStats) {
this.#logQueryStats(options);
}
this.queryStats = undefined;
this.shouldTrackQueryStats = false;
this.#queryStats = undefined;
this.#shouldTrackQueryStats = false;
}
private async send<Response>(
async #send<Response>(
entry: PoolEntry,
request: WorkerRequest
): Promise<Response> {
if (request.type === 'sqlCall:read' || request.type === 'sqlCall:write') {
if (this.onReady) {
await this.onReady;
if (this.#onReady) {
await this.#onReady;
}
if (!this.isReady) {
if (!this.#isReady) {
throw new Error('Not initialized');
}
}
const { seq } = this;
const seq = this.#seq;
// eslint-disable-next-line no-bitwise
this.seq = (this.seq + 1) >>> 0;
this.#seq = (this.#seq + 1) >>> 0;
const { promise: result, resolve, reject } = explodePromise<Response>();
this.onResponse.set(seq, { resolve, reject });
this.#onResponse.set(seq, { resolve, reject });
const wrappedRequest: WrappedWorkerRequest = {
seq,
@ -359,24 +355,24 @@ export class MainSQL {
}
}
private async terminate(request: WorkerRequest): Promise<void> {
const primary = this.pool[0];
const rest = this.pool.slice(1);
async #terminate(request: WorkerRequest): Promise<void> {
const primary = this.#pool[0];
const rest = this.#pool.slice(1);
// Terminate non-primary workers first
await Promise.all(rest.map(worker => this.send(worker, request)));
await Promise.all(rest.map(worker => this.#send(worker, request)));
// Primary last
await this.send(primary, request);
await this.#send(primary, request);
}
private onError(errorKind: SqliteErrorKind, error: Error): void {
#onError(errorKind: SqliteErrorKind, error: Error): void {
if (errorKind === SqliteErrorKind.Unknown) {
return;
}
const resolvers = new Array<(error: Error) => void>();
this.errorResolvers = this.errorResolvers.filter(entry => {
this.#errorResolvers = this.#errorResolvers.filter(entry => {
if (entry.kind === errorKind) {
resolvers.push(entry.resolve);
return false;
@ -389,76 +385,73 @@ export class MainSQL {
}
}
private resetQueryStats() {
this.queryStats = { start: Date.now(), statsByQuery: new Map() };
#resetQueryStats() {
this.#queryStats = { start: Date.now(), statsByQuery: new Map() };
}
private roundDuration(duration: number): number {
#roundDuration(duration: number): number {
return Math.round(100 * duration) / 100;
}
private logQueryStats({
maxQueriesToLog = 10,
epochName,
}: QueryStatsOptions) {
if (!this.queryStats) {
#logQueryStats({ maxQueriesToLog = 10, epochName }: QueryStatsOptions) {
if (!this.#queryStats) {
return;
}
const epochDuration = Date.now() - this.queryStats.start;
const epochDuration = Date.now() - this.#queryStats.start;
const sortedByCumulativeDuration = [
...this.queryStats.statsByQuery.values(),
...this.#queryStats.statsByQuery.values(),
].sort((a, b) => (b.cumulative ?? 0) - (a.cumulative ?? 0));
const cumulativeDuration = sortedByCumulativeDuration.reduce(
(sum, stats) => sum + stats.cumulative,
0
);
this.logger?.info(
this.#logger?.info(
`Top ${maxQueriesToLog} queries by cumulative duration (ms) over last ${epochDuration}ms` +
`${epochName ? ` during '${epochName}'` : ''}: ` +
`${sortedByCumulativeDuration
.slice(0, maxQueriesToLog)
.map(stats => {
return (
`${stats.queryName}: cumulative ${this.roundDuration(stats.cumulative)} | ` +
`average: ${this.roundDuration(stats.cumulative / (stats.count || 1))} | ` +
`max: ${this.roundDuration(stats.max)} | ` +
`${stats.queryName}: cumulative ${this.#roundDuration(stats.cumulative)} | ` +
`average: ${this.#roundDuration(stats.cumulative / (stats.count || 1))} | ` +
`max: ${this.#roundDuration(stats.max)} | ` +
`count: ${stats.count}`
);
})
.join(' ||| ')}` +
`; Total cumulative duration of all SQL queries during this epoch: ${this.roundDuration(cumulativeDuration)}ms`
`; Total cumulative duration of all SQL queries during this epoch: ${this.#roundDuration(cumulativeDuration)}ms`
);
}
private traceDuration(method: string, duration: number): void {
if (this.shouldTrackQueryStats) {
if (!this.queryStats) {
this.resetQueryStats();
#traceDuration(method: string, duration: number): void {
if (this.#shouldTrackQueryStats) {
if (!this.#queryStats) {
this.#resetQueryStats();
}
strictAssert(this.queryStats, 'has been initialized');
let currentStats = this.queryStats.statsByQuery.get(method);
strictAssert(this.#queryStats, 'has been initialized');
let currentStats = this.#queryStats.statsByQuery.get(method);
if (!currentStats) {
currentStats = { count: 0, cumulative: 0, queryName: method, max: 0 };
this.queryStats.statsByQuery.set(method, currentStats);
this.#queryStats.statsByQuery.set(method, currentStats);
}
currentStats.count += 1;
currentStats.cumulative += duration;
currentStats.max = Math.max(currentStats.max, duration);
}
if (this.shouldTimeQueries && !app.isPackaged) {
const twoDecimals = this.roundDuration(duration);
this.logger?.info(`MainSQL query: ${method}, duration=${twoDecimals}ms`);
if (this.#shouldTimeQueries && !app.isPackaged) {
const twoDecimals = this.#roundDuration(duration);
this.#logger?.info(`MainSQL query: ${method}, duration=${twoDecimals}ms`);
}
if (duration > MIN_TRACE_DURATION) {
strictAssert(this.logger !== undefined, 'Logger not initialized');
this.logger.info(
strictAssert(this.#logger !== undefined, 'Logger not initialized');
this.#logger.info(
`MainSQL: slow query ${method} duration=${Math.round(duration)}ms`
);
}
}
private createWorker(): CreateWorkerResultType {
#createWorker(): CreateWorkerResultType {
const scriptPath = join(app.getAppPath(), 'ts', 'sql', 'mainWorker.js');
const worker = new Worker(scriptPath);
@ -466,15 +459,15 @@ export class MainSQL {
worker.on('message', (wrappedResponse: WrappedWorkerResponse) => {
if (wrappedResponse.type === 'log') {
const { level, args } = wrappedResponse;
strictAssert(this.logger !== undefined, 'Logger not initialized');
this.logger[level](`MainSQL: ${format(...args)}`);
strictAssert(this.#logger !== undefined, 'Logger not initialized');
this.#logger[level](`MainSQL: ${format(...args)}`);
return;
}
const { seq, error, errorKind, response } = wrappedResponse;
const pair = this.onResponse.get(seq);
this.onResponse.delete(seq);
const pair = this.#onResponse.get(seq);
this.#onResponse.delete(seq);
if (!pair) {
throw new Error(`Unexpected worker response with seq: ${seq}`);
}
@ -483,7 +476,7 @@ export class MainSQL {
const errorObj = new Error(error.message);
errorObj.stack = error.stack;
errorObj.name = error.name;
this.onError(errorKind ?? SqliteErrorKind.Unknown, errorObj);
this.#onError(errorKind ?? SqliteErrorKind.Unknown, errorObj);
pair.reject(errorObj);
} else {
@ -498,9 +491,9 @@ export class MainSQL {
}
// Find first pool entry with minimal load
private getWorker(): PoolEntry {
let min = this.pool[0];
for (const entry of this.pool) {
#getWorker(): PoolEntry {
let min = this.#pool[0];
for (const entry of this.#pool) {
if (min && min.load < entry.load) {
continue;
}

View file

@ -14,22 +14,21 @@ const COLORS: Array<[number, number, number]> = [
];
class FakeGroupCallVideoFrameSource implements VideoFrameSource {
private readonly sourceArray: Uint8Array;
private readonly dimensions: [number, number];
readonly #sourceArray: Uint8Array;
readonly #dimensions: [number, number];
constructor(width: number, height: number, r: number, g: number, b: number) {
const length = width * height * 4;
this.sourceArray = new Uint8Array(length);
this.#sourceArray = new Uint8Array(length);
for (let i = 0; i < length; i += 4) {
this.sourceArray[i] = r;
this.sourceArray[i + 1] = g;
this.sourceArray[i + 2] = b;
this.sourceArray[i + 3] = 255;
this.#sourceArray[i] = r;
this.#sourceArray[i + 1] = g;
this.#sourceArray[i + 2] = b;
this.#sourceArray[i + 3] = 255;
}
this.dimensions = [width, height];
this.#dimensions = [width, height];
}
receiveVideoFrame(
@ -42,8 +41,8 @@ class FakeGroupCallVideoFrameSource implements VideoFrameSource {
return undefined;
}
destinationBuffer.set(this.sourceArray);
return this.dimensions;
destinationBuffer.set(this.#sourceArray);
return this.#dimensions;
}
}

View file

@ -22,20 +22,20 @@ import { DataWriter } from '../../sql/Client';
const { BACKUP_INTEGRATION_DIR } = process.env;
class MemoryStream extends InputStream {
private offset = 0;
#offset = 0;
constructor(private readonly buffer: Buffer) {
super();
}
public override async read(amount: number): Promise<Buffer> {
const result = this.buffer.slice(this.offset, this.offset + amount);
this.offset += amount;
const result = this.buffer.slice(this.#offset, this.#offset + amount);
this.#offset += amount;
return result;
}
public override async skip(amount: number): Promise<void> {
this.offset += amount;
this.#offset += amount;
}
}

View file

@ -171,26 +171,29 @@ export class Bootstrap {
public readonly server: Server;
public readonly cdn3Path: string;
private readonly options: BootstrapInternalOptions;
private privContacts?: ReadonlyArray<PrimaryDevice>;
private privContactsWithoutProfileKey?: ReadonlyArray<PrimaryDevice>;
private privUnknownContacts?: ReadonlyArray<PrimaryDevice>;
private privPhone?: PrimaryDevice;
private privDesktop?: Device;
private storagePath?: string;
private timestamp: number = Date.now() - durations.WEEK;
private lastApp?: App;
private readonly randomId = crypto.randomBytes(8).toString('hex');
readonly #options: BootstrapInternalOptions;
#privContacts?: ReadonlyArray<PrimaryDevice>;
#privContactsWithoutProfileKey?: ReadonlyArray<PrimaryDevice>;
#privUnknownContacts?: ReadonlyArray<PrimaryDevice>;
#privPhone?: PrimaryDevice;
#privDesktop?: Device;
#storagePath?: string;
#timestamp: number = Date.now() - durations.WEEK;
#lastApp?: App;
readonly #randomId = crypto.randomBytes(8).toString('hex');
constructor(options: BootstrapOptions = {}) {
this.cdn3Path = path.join(os.tmpdir(), `mock-signal-cdn3-${this.randomId}`);
this.cdn3Path = path.join(
os.tmpdir(),
`mock-signal-cdn3-${this.#randomId}`
);
this.server = new Server({
// Limit number of storage read keys for easier testing
maxStorageReadKeys: MAX_STORAGE_READ_KEYS,
cdn3Path: this.cdn3Path,
});
this.options = {
this.#options = {
linkedDevices: 5,
contactCount: CONTACT_COUNT,
contactsWithoutProfileKey: 0,
@ -202,10 +205,10 @@ export class Bootstrap {
};
const totalContactCount =
this.options.contactCount +
this.options.contactsWithoutProfileKey +
this.options.unknownContactCount;
assert(totalContactCount <= this.options.contactNames.length);
this.#options.contactCount +
this.#options.contactsWithoutProfileKey +
this.#options.unknownContactCount;
assert(totalContactCount <= this.#options.contactNames.length);
assert(totalContactCount <= MAX_CONTACTS);
}
@ -218,19 +221,19 @@ export class Bootstrap {
debug('started server on port=%d', port);
const totalContactCount =
this.options.contactCount +
this.options.contactsWithoutProfileKey +
this.options.unknownContactCount;
this.#options.contactCount +
this.#options.contactsWithoutProfileKey +
this.#options.unknownContactCount;
const allContacts = await Promise.all(
this.options.contactNames
this.#options.contactNames
.slice(0, totalContactCount)
.map(async profileName => {
const primary = await this.server.createPrimaryDevice({
profileName,
});
for (let i = 0; i < this.options.linkedDevices; i += 1) {
for (let i = 0; i < this.#options.linkedDevices; i += 1) {
// eslint-disable-next-line no-await-in-loop
await this.server.createSecondaryDevice(primary);
}
@ -239,28 +242,30 @@ export class Bootstrap {
})
);
this.privContacts = allContacts.splice(0, this.options.contactCount);
this.privContactsWithoutProfileKey = allContacts.splice(
this.#privContacts = allContacts.splice(0, this.#options.contactCount);
this.#privContactsWithoutProfileKey = allContacts.splice(
0,
this.options.contactsWithoutProfileKey
this.#options.contactsWithoutProfileKey
);
this.privUnknownContacts = allContacts.splice(
this.#privUnknownContacts = allContacts.splice(
0,
this.options.unknownContactCount
this.#options.unknownContactCount
);
this.privPhone = await this.server.createPrimaryDevice({
this.#privPhone = await this.server.createPrimaryDevice({
profileName: 'Myself',
contacts: this.contacts,
contactsWithoutProfileKey: this.contactsWithoutProfileKey,
});
if (this.options.useLegacyStorageEncryption) {
this.privPhone.storageRecordIkm = undefined;
if (this.#options.useLegacyStorageEncryption) {
this.#privPhone.storageRecordIkm = undefined;
}
this.storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'mock-signal-'));
this.#storagePath = await fs.mkdtemp(
path.join(os.tmpdir(), 'mock-signal-')
);
debug('setting storage path=%j', this.storagePath);
debug('setting storage path=%j', this.#storagePath);
}
public static benchmark(
@ -272,34 +277,36 @@ export class Bootstrap {
public get logsDir(): string {
assert(
this.storagePath !== undefined,
this.#storagePath !== undefined,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
return path.join(this.storagePath, 'logs');
return path.join(this.#storagePath, 'logs');
}
public get ephemeralConfigPath(): string {
assert(
this.storagePath !== undefined,
this.#storagePath !== undefined,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
return path.join(this.storagePath, 'ephemeral.json');
return path.join(this.#storagePath, 'ephemeral.json');
}
public eraseStorage(): Promise<void> {
return this.resetAppStorage();
return this.#resetAppStorage();
}
private async resetAppStorage(): Promise<void> {
async #resetAppStorage(): Promise<void> {
assert(
this.storagePath !== undefined,
this.#storagePath !== undefined,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
await fs.rm(this.storagePath, { recursive: true });
this.storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'mock-signal-'));
await fs.rm(this.#storagePath, { recursive: true });
this.#storagePath = await fs.mkdtemp(
path.join(os.tmpdir(), 'mock-signal-')
);
}
public async teardown(): Promise<void> {
@ -307,11 +314,11 @@ export class Bootstrap {
await Promise.race([
Promise.all([
...[this.storagePath, this.cdn3Path].map(tmpPath =>
...[this.#storagePath, this.cdn3Path].map(tmpPath =>
tmpPath ? fs.rm(tmpPath, { recursive: true }) : Promise.resolve()
),
this.server.close(),
this.lastApp?.close(),
this.#lastApp?.close(),
]),
new Promise(resolve => setTimeout(resolve, CLOSE_TIMEOUT).unref()),
]);
@ -346,7 +353,7 @@ export class Bootstrap {
const provisionURL = await app.waitForProvisionURL();
debug('completing provision');
this.privDesktop = await provision.complete({
this.#privDesktop = await provision.complete({
provisionURL,
primaryDevice: this.phone,
});
@ -388,7 +395,7 @@ export class Bootstrap {
extraConfig?: Partial<RendererConfigType>
): Promise<App> {
assert(
this.storagePath !== undefined,
this.#storagePath !== undefined,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
@ -408,7 +415,7 @@ export class Bootstrap {
}
// eslint-disable-next-line no-await-in-loop
const config = await this.generateConfig(port, family, extraConfig);
const config = await this.#generateConfig(port, family, extraConfig);
const startedApp = new App({
main: ELECTRON,
@ -427,14 +434,14 @@ export class Bootstrap {
);
// eslint-disable-next-line no-await-in-loop
await this.resetAppStorage();
await this.#resetAppStorage();
continue;
}
this.lastApp = startedApp;
this.#lastApp = startedApp;
startedApp.on('close', () => {
if (this.lastApp === startedApp) {
this.lastApp = undefined;
if (this.#lastApp === startedApp) {
this.#lastApp = undefined;
}
});
@ -445,14 +452,14 @@ export class Bootstrap {
}
public getTimestamp(): number {
const result = this.timestamp;
this.timestamp += 1;
const result = this.#timestamp;
this.#timestamp += 1;
return result;
}
public async maybeSaveLogs(
test?: Mocha.Runnable,
app: App | undefined = this.lastApp
app: App | undefined = this.#lastApp
): Promise<void> {
const { FORCE_ARTIFACT_SAVE } = process.env;
if (test?.state !== 'passed' || FORCE_ARTIFACT_SAVE) {
@ -461,10 +468,10 @@ export class Bootstrap {
}
public async saveLogs(
app: App | undefined = this.lastApp,
app: App | undefined = this.#lastApp,
testName?: string
): Promise<void> {
const outDir = await this.getArtifactsDir(testName);
const outDir = await this.#getArtifactsDir(testName);
if (outDir == null) {
return;
}
@ -544,7 +551,7 @@ export class Bootstrap {
`screenshot difference for ${name}: ${numPixels}/${width * height}`
);
const outDir = await this.getArtifactsDir(test?.fullTitle());
const outDir = await this.#getArtifactsDir(test?.fullTitle());
if (outDir != null) {
debug('saving screenshots and diff');
const prefix = `${index}-${sanitizePathComponent(name)}`;
@ -565,8 +572,8 @@ export class Bootstrap {
}
public getAbsoluteAttachmentPath(relativePath: string): string {
strictAssert(this.storagePath, 'storagePath must exist');
return join(this.storagePath, 'attachments.noindex', relativePath);
strictAssert(this.#storagePath, 'storagePath must exist');
return join(this.#storagePath, 'attachments.noindex', relativePath);
}
public async storeAttachmentOnCDN(
@ -607,41 +614,41 @@ export class Bootstrap {
public get phone(): PrimaryDevice {
assert(
this.privPhone,
this.#privPhone,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
return this.privPhone;
return this.#privPhone;
}
public get desktop(): Device {
assert(
this.privDesktop,
this.#privDesktop,
'Bootstrap has to be linked first, see: bootstrap.link()'
);
return this.privDesktop;
return this.#privDesktop;
}
public get contacts(): ReadonlyArray<PrimaryDevice> {
assert(
this.privContacts,
this.#privContacts,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
return this.privContacts;
return this.#privContacts;
}
public get contactsWithoutProfileKey(): ReadonlyArray<PrimaryDevice> {
assert(
this.privContactsWithoutProfileKey,
this.#privContactsWithoutProfileKey,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
return this.privContactsWithoutProfileKey;
return this.#privContactsWithoutProfileKey;
}
public get unknownContacts(): ReadonlyArray<PrimaryDevice> {
assert(
this.privUnknownContacts,
this.#privUnknownContacts,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
return this.privUnknownContacts;
return this.#privUnknownContacts;
}
public get allContacts(): ReadonlyArray<PrimaryDevice> {
@ -656,9 +663,7 @@ export class Bootstrap {
// Private
//
private async getArtifactsDir(
testName?: string
): Promise<string | undefined> {
async #getArtifactsDir(testName?: string): Promise<string | undefined> {
const { ARTIFACTS_DIR } = process.env;
if (!ARTIFACTS_DIR) {
// eslint-disable-next-line no-console
@ -669,8 +674,8 @@ export class Bootstrap {
}
const normalizedPath = testName
? `${this.randomId}-${sanitizePathComponent(testName)}`
: this.randomId;
? `${this.#randomId}-${sanitizePathComponent(testName)}`
: this.#randomId;
const outDir = path.join(ARTIFACTS_DIR, normalizedPath);
await fs.mkdir(outDir, { recursive: true });
@ -701,7 +706,7 @@ export class Bootstrap {
}
}
private async generateConfig(
async #generateConfig(
port: number,
family: string,
extraConfig?: Partial<RendererConfigType>
@ -712,11 +717,11 @@ export class Bootstrap {
return JSON.stringify({
...(await loadCertificates()),
forcePreloadBundle: this.options.benchmark,
forcePreloadBundle: this.#options.benchmark,
ciMode: 'full',
buildExpiration: Date.now() + durations.MONTH,
storagePath: this.storagePath,
storagePath: this.#storagePath,
storageProfile: 'mock',
serverUrl: url,
storageUrl: url,

View file

@ -47,7 +47,7 @@ export type AppOptionsType = Readonly<{
const WAIT_FOR_EVENT_TIMEOUT = 30 * SECOND;
export class App extends EventEmitter {
private privApp: ElectronApplication | undefined;
#privApp: ElectronApplication | undefined;
constructor(private readonly options: AppOptionsType) {
super();
@ -56,7 +56,7 @@ export class App extends EventEmitter {
public async start(): Promise<void> {
try {
// launch the electron processs
this.privApp = await electron.launch({
this.#privApp = await electron.launch({
executablePath: this.options.main,
args: this.options.args.slice(),
env: {
@ -84,53 +84,53 @@ export class App extends EventEmitter {
20 * SECOND
);
} catch (e) {
this.privApp?.process().kill('SIGKILL');
this.#privApp?.process().kill('SIGKILL');
throw e;
}
this.privApp.on('close', () => this.emit('close'));
this.#privApp.on('close', () => this.emit('close'));
drop(this.printLoop());
drop(this.#printLoop());
}
public async waitForProvisionURL(): Promise<string> {
return this.waitForEvent('provisioning-url');
return this.#waitForEvent('provisioning-url');
}
public async waitForDbInitialized(): Promise<void> {
return this.waitForEvent('db-initialized');
return this.#waitForEvent('db-initialized');
}
public async waitUntilLoaded(): Promise<AppLoadedInfoType> {
return this.waitForEvent('app-loaded');
return this.#waitForEvent('app-loaded');
}
public async waitForContactSync(): Promise<void> {
return this.waitForEvent('contactSync');
return this.#waitForEvent('contactSync');
}
public async waitForBackupImportComplete(): Promise<{ duration: number }> {
return this.waitForEvent('backupImportComplete');
return this.#waitForEvent('backupImportComplete');
}
public async waitForMessageSend(): Promise<MessageSendInfoType> {
return this.waitForEvent('message:send-complete');
return this.#waitForEvent('message:send-complete');
}
public async waitForConversationOpen(): Promise<ConversationOpenInfoType> {
return this.waitForEvent('conversation:open');
return this.#waitForEvent('conversation:open');
}
public async waitForChallenge(): Promise<ChallengeRequestType> {
return this.waitForEvent('challenge');
return this.#waitForEvent('challenge');
}
public async waitForReceipts(): Promise<ReceiptsInfoType> {
return this.waitForEvent('receipts');
return this.#waitForEvent('receipts');
}
public async waitForStorageService(): Promise<StorageServiceInfoType> {
return this.waitForEvent('storageServiceComplete');
return this.#waitForEvent('storageServiceComplete');
}
public async waitForManifestVersion(version: number): Promise<void> {
@ -152,28 +152,28 @@ export class App extends EventEmitter {
);
}
private async checkForFatalTestErrors(): Promise<void> {
async #checkForFatalTestErrors(): Promise<void> {
const count = await this.getPendingEventCount('fatalTestError');
if (count === 0) {
return;
}
for (let i = 0; i < count; i += 1) {
// eslint-disable-next-line no-await-in-loop, no-console
console.error(await this.waitForEvent('fatalTestError'));
console.error(await this.#waitForEvent('fatalTestError'));
}
throw new Error('App had fatal test errors');
}
public async close(): Promise<void> {
try {
await this.checkForFatalTestErrors();
await this.#checkForFatalTestErrors();
} finally {
await this.app.close();
await this.#app.close();
}
}
public async getWindow(): Promise<Page> {
return this.app.firstWindow();
return this.#app.firstWindow();
}
public async openSignalRoute(url: URL | string): Promise<void> {
@ -206,11 +206,11 @@ export class App extends EventEmitter {
}
public async waitForUnlink(): Promise<void> {
return this.waitForEvent('unlinkCleanupComplete');
return this.#waitForEvent('unlinkCleanupComplete');
}
public async waitForConversationOpenComplete(): Promise<void> {
return this.waitForEvent('conversationOpenComplete');
return this.#waitForEvent('conversationOpenComplete');
}
// EventEmitter types
@ -245,7 +245,7 @@ export class App extends EventEmitter {
// Private
//
private async waitForEvent<T>(
async #waitForEvent<T>(
event: string,
timeout = WAIT_FOR_EVENT_TIMEOUT
): Promise<T> {
@ -259,15 +259,15 @@ export class App extends EventEmitter {
return result as T;
}
private get app(): ElectronApplication {
if (!this.privApp) {
get #app(): ElectronApplication {
if (!this.#privApp) {
throw new Error('Call ElectronWrap.start() first');
}
return this.privApp;
return this.#privApp;
}
private async printLoop(): Promise<void> {
async #printLoop(): Promise<void> {
const kClosed: unique symbol = Symbol('kClosed');
const onClose = (async (): Promise<typeof kClosed> => {
try {
@ -283,7 +283,7 @@ export class App extends EventEmitter {
try {
// eslint-disable-next-line no-await-in-loop
const value = await Promise.race([
this.waitForEvent<string>('print', 0),
this.#waitForEvent<string>('print', 0),
onClose,
]);

View file

@ -9,31 +9,31 @@ import { PreventDisplaySleepService } from '../../../app/PreventDisplaySleepServ
describe('PreventDisplaySleepService', () => {
class FakePowerSaveBlocker implements PowerSaveBlocker {
private nextId = 0;
private idsStarted = new Set<number>();
#nextId = 0;
#idsStarted = new Set<number>();
isStarted(id: number): boolean {
return this.idsStarted.has(id);
return this.#idsStarted.has(id);
}
start(type: 'prevent-app-suspension' | 'prevent-display-sleep'): number {
assert.strictEqual(type, 'prevent-display-sleep');
const result = this.nextId;
this.nextId += 1;
this.idsStarted.add(result);
const result = this.#nextId;
this.#nextId += 1;
this.#idsStarted.add(result);
return result;
}
stop(id: number): boolean {
assert(this.idsStarted.has(id), `${id} was never started`);
this.idsStarted.delete(id);
assert(this.#idsStarted.has(id), `${id} was never started`);
this.#idsStarted.delete(id);
return false;
}
// This is only for testing.
_idCount(): number {
return this.idsStarted.size;
return this.#idsStarted.size;
}
}

View file

@ -733,18 +733,18 @@ describe('JobQueue', () => {
});
class FakeStream implements AsyncIterable<StoredJob> {
private eventEmitter = new EventEmitter();
#eventEmitter = new EventEmitter();
async *[Symbol.asyncIterator]() {
while (true) {
// eslint-disable-next-line no-await-in-loop
const [job] = await once(this.eventEmitter, 'drip');
const [job] = await once(this.#eventEmitter, 'drip');
yield parseUnknown(storedJobSchema, job as unknown);
}
}
drip(job: Readonly<StoredJob>): void {
this.eventEmitter.emit('drip', job);
this.#eventEmitter.emit('drip', job);
}
}

View file

@ -13,9 +13,8 @@ import { drop } from '../../util/drop';
export class TestJobQueueStore implements JobQueueStore {
events = new EventEmitter();
private openStreams = new Set<string>();
private pipes = new Map<string, Pipe>();
#openStreams = new Set<string>();
#pipes = new Map<string, Pipe>();
storedJobs: Array<StoredJob> = [];
@ -41,7 +40,7 @@ export class TestJobQueueStore implements JobQueueStore {
this.storedJobs.push(job);
}
this.getPipe(job.queueType).add(job);
this.#getPipe(job.queueType).add(job);
this.events.emit('insert');
}
@ -55,78 +54,75 @@ export class TestJobQueueStore implements JobQueueStore {
}
stream(queueType: string): Pipe {
if (this.openStreams.has(queueType)) {
if (this.#openStreams.has(queueType)) {
throw new Error('Cannot stream the same queueType more than once');
}
this.openStreams.add(queueType);
this.#openStreams.add(queueType);
return this.getPipe(queueType);
return this.#getPipe(queueType);
}
pauseStream(queueType: string): void {
return this.getPipe(queueType).pause();
return this.#getPipe(queueType).pause();
}
resumeStream(queueType: string): void {
return this.getPipe(queueType).resume();
return this.#getPipe(queueType).resume();
}
private getPipe(queueType: string): Pipe {
const existingPipe = this.pipes.get(queueType);
#getPipe(queueType: string): Pipe {
const existingPipe = this.#pipes.get(queueType);
if (existingPipe) {
return existingPipe;
}
const result = new Pipe();
this.pipes.set(queueType, result);
this.#pipes.set(queueType, result);
return result;
}
}
class Pipe implements AsyncIterable<StoredJob> {
private queue: Array<StoredJob> = [];
private eventEmitter = new EventEmitter();
private isLocked = false;
private isPaused = false;
#queue: Array<StoredJob> = [];
#eventEmitter = new EventEmitter();
#isLocked = false;
#isPaused = false;
add(value: Readonly<StoredJob>) {
this.queue.push(value);
this.eventEmitter.emit('add');
this.#queue.push(value);
this.#eventEmitter.emit('add');
}
async *[Symbol.asyncIterator]() {
if (this.isLocked) {
if (this.#isLocked) {
throw new Error('Cannot iterate over a pipe more than once');
}
this.isLocked = true;
this.#isLocked = true;
while (true) {
for (const value of this.queue) {
await this.waitForUnpaused();
for (const value of this.#queue) {
await this.#waitForUnpaused();
yield value;
}
this.queue = [];
this.#queue = [];
// We do this because we want to yield values in series.
await once(this.eventEmitter, 'add');
await once(this.#eventEmitter, 'add');
}
}
pause(): void {
this.isPaused = true;
this.#isPaused = true;
}
resume(): void {
this.isPaused = false;
this.eventEmitter.emit('resume');
this.#isPaused = false;
this.#eventEmitter.emit('resume');
}
private async waitForUnpaused() {
if (this.isPaused) {
await once(this.eventEmitter, 'resume');
async #waitForUnpaused() {
if (this.#isPaused) {
await once(this.#eventEmitter, 'resume');
}
}
}

View file

@ -242,7 +242,7 @@ export default class AccountManager extends EventTarget {
this.pending = Promise.resolve();
}
private async queueTask<T>(task: () => Promise<T>): Promise<T> {
async #queueTask<T>(task: () => Promise<T>): Promise<T> {
this.pendingQueue = this.pendingQueue || new PQueue({ concurrency: 1 });
const taskWithTimeout = createTaskWithTimeout(task, 'AccountManager task');
@ -328,7 +328,7 @@ export default class AccountManager extends EventTarget {
verificationCode: string,
sessionId: string
): Promise<void> {
await this.queueTask(async () => {
await this.#queueTask(async () => {
const aciKeyPair = generateKeyPair();
const pniKeyPair = generateKeyPair();
const profileKey = getRandomBytes(PROFILE_KEY_LENGTH);
@ -337,7 +337,7 @@ export default class AccountManager extends EventTarget {
const accountEntropyPool = AccountEntropyPool.generate();
const mediaRootBackupKey = BackupKey.generateRandom().serialize();
await this.createAccount({
await this.#createAccount({
type: AccountType.Primary,
number,
verificationCode,
@ -358,12 +358,12 @@ export default class AccountManager extends EventTarget {
async registerSecondDevice(
options: CreateLinkedDeviceOptionsType
): Promise<void> {
await this.queueTask(async () => {
await this.createAccount(options);
await this.#queueTask(async () => {
await this.#createAccount(options);
});
}
private getIdentityKeyOrThrow(ourServiceId: ServiceIdString): KeyPairType {
#getIdentityKeyOrThrow(ourServiceId: ServiceIdString): KeyPairType {
const { storage } = window.textsecure;
const store = storage.protocol;
let identityKey: KeyPairType | undefined;
@ -383,7 +383,7 @@ export default class AccountManager extends EventTarget {
return identityKey;
}
private async generateNewPreKeys(
async #generateNewPreKeys(
serviceIdKind: ServiceIdKind,
count = PRE_KEY_GEN_BATCH_SIZE
): Promise<Array<UploadPreKeyType>> {
@ -418,7 +418,7 @@ export default class AccountManager extends EventTarget {
}));
}
private async generateNewKyberPreKeys(
async #generateNewKyberPreKeys(
serviceIdKind: ServiceIdKind,
count = PRE_KEY_GEN_BATCH_SIZE
): Promise<Array<UploadKyberPreKeyType>> {
@ -436,7 +436,7 @@ export default class AccountManager extends EventTarget {
}
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
const identityKey = this.#getIdentityKeyOrThrow(ourServiceId);
const toSave: Array<Omit<KyberPreKeyType, 'id'>> = [];
const toUpload: Array<UploadKyberPreKeyType> = [];
@ -471,13 +471,13 @@ export default class AccountManager extends EventTarget {
forceUpdate = false
): Promise<void> {
const logId = `maybeUpdateKeys(${serviceIdKind})`;
await this.queueTask(async () => {
await this.#queueTask(async () => {
const { storage } = window.textsecure;
let identityKey: KeyPairType;
try {
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
identityKey = this.getIdentityKeyOrThrow(ourServiceId);
identityKey = this.#getIdentityKeyOrThrow(ourServiceId);
} catch (error) {
if (serviceIdKind === ServiceIdKind.PNI) {
log.info(
@ -506,7 +506,7 @@ export default class AccountManager extends EventTarget {
log.info(
`${logId}: Server prekey count is ${preKeyCount}, generating a new set`
);
preKeys = await this.generateNewPreKeys(serviceIdKind);
preKeys = await this.#generateNewPreKeys(serviceIdKind);
}
let pqPreKeys: Array<UploadKyberPreKeyType> | undefined;
@ -518,14 +518,14 @@ export default class AccountManager extends EventTarget {
log.info(
`${logId}: Server kyber prekey count is ${kyberPreKeyCount}, generating a new set`
);
pqPreKeys = await this.generateNewKyberPreKeys(serviceIdKind);
pqPreKeys = await this.#generateNewKyberPreKeys(serviceIdKind);
}
const pqLastResortPreKey = await this.maybeUpdateLastResortKyberKey(
const pqLastResortPreKey = await this.#maybeUpdateLastResortKyberKey(
serviceIdKind,
forceUpdate
);
const signedPreKey = await this.maybeUpdateSignedPreKey(
const signedPreKey = await this.#maybeUpdateSignedPreKey(
serviceIdKind,
forceUpdate
);
@ -601,7 +601,7 @@ export default class AccountManager extends EventTarget {
return false;
}
private async generateSignedPreKey(
async #generateSignedPreKey(
serviceIdKind: ServiceIdKind,
identityKey: KeyPairType
): Promise<CompatSignedPreKeyType> {
@ -625,13 +625,13 @@ export default class AccountManager extends EventTarget {
return key;
}
private async maybeUpdateSignedPreKey(
async #maybeUpdateSignedPreKey(
serviceIdKind: ServiceIdKind,
forceUpdate = false
): Promise<UploadSignedPreKeyType | undefined> {
const ourServiceId =
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
const identityKey = this.#getIdentityKeyOrThrow(ourServiceId);
const logId = `AccountManager.maybeUpdateSignedPreKey(${serviceIdKind}, ${ourServiceId})`;
const store = window.textsecure.storage.protocol;
@ -662,7 +662,7 @@ export default class AccountManager extends EventTarget {
return;
}
const key = await this.generateSignedPreKey(serviceIdKind, identityKey);
const key = await this.#generateSignedPreKey(serviceIdKind, identityKey);
log.info(`${logId}: Saving new signed prekey`, key.keyId);
await store.storeSignedPreKey(ourServiceId, key.keyId, key.keyPair);
@ -670,7 +670,7 @@ export default class AccountManager extends EventTarget {
return signedPreKeyToUploadSignedPreKey(key);
}
private async generateLastResortKyberKey(
async #generateLastResortKyberKey(
serviceIdKind: ServiceIdKind,
identityKey: KeyPairType
): Promise<KyberPreKeyRecord> {
@ -695,13 +695,13 @@ export default class AccountManager extends EventTarget {
return record;
}
private async maybeUpdateLastResortKyberKey(
async #maybeUpdateLastResortKyberKey(
serviceIdKind: ServiceIdKind,
forceUpdate = false
): Promise<UploadSignedPreKeyType | undefined> {
const ourServiceId =
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
const identityKey = this.#getIdentityKeyOrThrow(ourServiceId);
const logId = `maybeUpdateLastResortKyberKey(${serviceIdKind}, ${ourServiceId})`;
const store = window.textsecure.storage.protocol;
@ -732,7 +732,7 @@ export default class AccountManager extends EventTarget {
return;
}
const record = await this.generateLastResortKyberKey(
const record = await this.#generateLastResortKyberKey(
serviceIdKind,
identityKey
);
@ -912,22 +912,18 @@ export default class AccountManager extends EventTarget {
}
}
private async createAccount(
options: CreateAccountOptionsType
): Promise<void> {
async #createAccount(options: CreateAccountOptionsType): Promise<void> {
this.dispatchEvent(new Event('startRegistration'));
const registrationBaton = this.server.startRegistration();
try {
await this.doCreateAccount(options);
await this.#doCreateAccount(options);
} finally {
this.server.finishRegistration(registrationBaton);
}
await this.registrationDone();
await this.#registrationDone();
}
private async doCreateAccount(
options: CreateAccountOptionsType
): Promise<void> {
async #doCreateAccount(options: CreateAccountOptionsType): Promise<void> {
const {
number,
verificationCode,
@ -1032,19 +1028,19 @@ export default class AccountManager extends EventTarget {
let ourPni: PniString;
let deviceId: number;
const aciPqLastResortPreKey = await this.generateLastResortKyberKey(
const aciPqLastResortPreKey = await this.#generateLastResortKyberKey(
ServiceIdKind.ACI,
aciKeyPair
);
const pniPqLastResortPreKey = await this.generateLastResortKyberKey(
const pniPqLastResortPreKey = await this.#generateLastResortKyberKey(
ServiceIdKind.PNI,
pniKeyPair
);
const aciSignedPreKey = await this.generateSignedPreKey(
const aciSignedPreKey = await this.#generateSignedPreKey(
ServiceIdKind.ACI,
aciKeyPair
);
const pniSignedPreKey = await this.generateSignedPreKey(
const pniSignedPreKey = await this.#generateSignedPreKey(
ServiceIdKind.PNI,
pniKeyPair
);
@ -1333,8 +1329,8 @@ export default class AccountManager extends EventTarget {
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const logId = `AccountManager.generateKeys(${serviceIdKind}, ${ourServiceId})`;
const preKeys = await this.generateNewPreKeys(serviceIdKind, count);
const pqPreKeys = await this.generateNewKyberPreKeys(serviceIdKind, count);
const preKeys = await this.#generateNewPreKeys(serviceIdKind, count);
const pqPreKeys = await this.#generateNewKyberPreKeys(serviceIdKind, count);
log.info(
`${logId}: Generated ` +
@ -1347,13 +1343,13 @@ export default class AccountManager extends EventTarget {
await this._cleanKyberPreKeys(serviceIdKind);
return {
identityKey: this.getIdentityKeyOrThrow(ourServiceId).pubKey,
identityKey: this.#getIdentityKeyOrThrow(ourServiceId).pubKey,
preKeys,
pqPreKeys,
};
}
private async registrationDone(): Promise<void> {
async #registrationDone(): Promise<void> {
log.info('registration done');
this.dispatchEvent(new Event('registration'));
}

View file

@ -79,7 +79,7 @@ export class ParseContactsTransform extends Transform {
public contacts: Array<ContactDetailsWithAvatar> = [];
public activeContact: Proto.ContactDetails | undefined;
private unused: Uint8Array | undefined;
#unused: Uint8Array | undefined;
override async _transform(
chunk: Buffer | undefined,
@ -93,9 +93,9 @@ export class ParseContactsTransform extends Transform {
try {
let data = chunk;
if (this.unused) {
data = Buffer.concat([this.unused, data]);
this.unused = undefined;
if (this.#unused) {
data = Buffer.concat([this.#unused, data]);
this.#unused = undefined;
}
const reader = Reader.create(data);
@ -110,7 +110,7 @@ export class ParseContactsTransform extends Transform {
if (err instanceof RangeError) {
// Note: A failed decodeDelimited() does in fact update reader.pos, so we
// must reset to startPos
this.unused = data.subarray(startPos);
this.#unused = data.subarray(startPos);
done();
return;
}
@ -174,7 +174,7 @@ export class ParseContactsTransform extends Transform {
} else {
// We have an attachment, but we haven't read enough data yet. We need to
// wait for another chunk.
this.unused = data.subarray(reader.pos);
this.#unused = data.subarray(reader.pos);
done();
return;
}

File diff suppressed because it is too large Load diff

View file

@ -82,27 +82,26 @@ export type ProvisionerOptionsType = Readonly<{
const INACTIVE_SOCKET_TIMEOUT = 30 * MINUTE;
export class Provisioner {
private readonly cipher = new ProvisioningCipher();
private readonly server: WebAPIType;
private readonly appVersion: string;
private state: StateType = { step: Step.Idle };
private wsr: IWebSocketResource | undefined;
readonly #cipher = new ProvisioningCipher();
readonly #server: WebAPIType;
readonly #appVersion: string;
#state: StateType = { step: Step.Idle };
#wsr: IWebSocketResource | undefined;
constructor(options: ProvisionerOptionsType) {
this.server = options.server;
this.appVersion = options.appVersion;
this.#server = options.server;
this.#appVersion = options.appVersion;
}
public close(error = new Error('Provisioner closed')): void {
try {
this.wsr?.close();
this.#wsr?.close();
} catch {
// Best effort
}
const prevState = this.state;
this.state = { step: Step.Done };
const prevState = this.#state;
this.#state = { step: Step.Done };
if (prevState.step === Step.WaitingForURL) {
prevState.url.reject(error);
@ -113,15 +112,15 @@ export class Provisioner {
public async getURL(): Promise<string> {
strictAssert(
this.state.step === Step.Idle,
`Invalid state for getURL: ${this.state.step}`
this.#state.step === Step.Idle,
`Invalid state for getURL: ${this.#state.step}`
);
this.state = { step: Step.Connecting };
this.#state = { step: Step.Connecting };
const wsr = await this.server.getProvisioningResource({
const wsr = await this.#server.getProvisioningResource({
handleRequest: (request: IncomingWebSocketRequest) => {
try {
this.handleRequest(request);
this.#handleRequest(request);
} catch (error) {
log.error(
'Provisioner.handleRequest: failure',
@ -131,7 +130,7 @@ export class Provisioner {
}
},
});
this.wsr = wsr;
this.#wsr = wsr;
let inactiveTimer: NodeJS.Timeout | undefined;
@ -159,12 +158,12 @@ export class Provisioner {
document.addEventListener('visibilitychange', onVisibilityChange);
if (this.state.step !== Step.Connecting) {
if (this.#state.step !== Step.Connecting) {
this.close();
throw new Error('Provisioner closed early');
}
this.state = {
this.#state = {
step: Step.WaitingForURL,
url: explodePromise(),
};
@ -177,7 +176,7 @@ export class Provisioner {
}
inactiveTimer = undefined;
if (this.state.step === Step.ReadyToLink) {
if (this.#state.step === Step.ReadyToLink) {
// WebSocket close is not an issue since we no longer need it
return;
}
@ -186,15 +185,15 @@ export class Provisioner {
this.close(new Error('websocket closed'));
});
return this.state.url.promise;
return this.#state.url.promise;
}
public async waitForEnvelope(): Promise<void> {
strictAssert(
this.state.step === Step.WaitingForEnvelope,
`Invalid state for waitForEnvelope: ${this.state.step}`
this.#state.step === Step.WaitingForEnvelope,
`Invalid state for waitForEnvelope: ${this.#state.step}`
);
await this.state.done.promise;
await this.#state.done.promise;
}
public prepareLinkData({
@ -202,11 +201,11 @@ export class Provisioner {
backupFile,
}: PrepareLinkDataOptionsType): CreateLinkedDeviceOptionsType {
strictAssert(
this.state.step === Step.ReadyToLink,
`Invalid state for prepareLinkData: ${this.state.step}`
this.#state.step === Step.ReadyToLink,
`Invalid state for prepareLinkData: ${this.#state.step}`
);
const { envelope } = this.state;
this.state = { step: Step.Done };
const { envelope } = this.#state;
this.#state = { step: Step.Done };
const {
number,
@ -273,31 +272,31 @@ export class Provisioner {
public isLinkAndSync(): boolean {
strictAssert(
this.state.step === Step.ReadyToLink,
`Invalid state for prepareLinkData: ${this.state.step}`
this.#state.step === Step.ReadyToLink,
`Invalid state for prepareLinkData: ${this.#state.step}`
);
const { envelope } = this.state;
const { envelope } = this.#state;
return (
isLinkAndSyncEnabled(this.appVersion) &&
isLinkAndSyncEnabled(this.#appVersion) &&
Bytes.isNotEmpty(envelope.ephemeralBackupKey)
);
}
private handleRequest(request: IncomingWebSocketRequest): void {
const pubKey = this.cipher.getPublicKey();
#handleRequest(request: IncomingWebSocketRequest): void {
const pubKey = this.#cipher.getPublicKey();
if (
request.requestType === ServerRequestType.ProvisioningAddress &&
request.body
) {
strictAssert(
this.state.step === Step.WaitingForURL,
`Unexpected provisioning address, state: ${this.state}`
this.#state.step === Step.WaitingForURL,
`Unexpected provisioning address, state: ${this.#state}`
);
const prevState = this.state;
this.state = { step: Step.WaitingForEnvelope, done: explodePromise() };
const prevState = this.#state;
this.#state = { step: Step.WaitingForEnvelope, done: explodePromise() };
const proto = Proto.ProvisioningUuid.decode(request.body);
const { uuid } = proto;
@ -307,7 +306,9 @@ export class Provisioner {
.toAppUrl({
uuid,
pubKey: Bytes.toBase64(pubKey),
capabilities: isLinkAndSyncEnabled(this.appVersion) ? ['backup'] : [],
capabilities: isLinkAndSyncEnabled(this.#appVersion)
? ['backup']
: [],
})
.toString();
@ -320,17 +321,17 @@ export class Provisioner {
request.body
) {
strictAssert(
this.state.step === Step.WaitingForEnvelope,
`Unexpected provisioning address, state: ${this.state}`
this.#state.step === Step.WaitingForEnvelope,
`Unexpected provisioning address, state: ${this.#state}`
);
const prevState = this.state;
const prevState = this.#state;
const ciphertext = Proto.ProvisionEnvelope.decode(request.body);
const message = this.cipher.decrypt(ciphertext);
const message = this.#cipher.decrypt(ciphertext);
this.state = { step: Step.ReadyToLink, envelope: message };
this.#state = { step: Step.ReadyToLink, envelope: message };
request.respond(200, 'OK');
this.wsr?.close();
this.#wsr?.close();
prevState.done.resolve();
} else {

View file

@ -1967,7 +1967,7 @@ export default class MessageSender {
options?: Readonly<SendOptionsType>;
}>
): Promise<CallbackResultType> {
return this.sendReceiptMessage({
return this.#sendReceiptMessage({
...options,
type: Proto.ReceiptMessage.Type.DELIVERY,
});
@ -1981,7 +1981,7 @@ export default class MessageSender {
options?: Readonly<SendOptionsType>;
}>
): Promise<CallbackResultType> {
return this.sendReceiptMessage({
return this.#sendReceiptMessage({
...options,
type: Proto.ReceiptMessage.Type.READ,
});
@ -1995,13 +1995,13 @@ export default class MessageSender {
options?: Readonly<SendOptionsType>;
}>
): Promise<CallbackResultType> {
return this.sendReceiptMessage({
return this.#sendReceiptMessage({
...options,
type: Proto.ReceiptMessage.Type.VIEWED,
});
}
private async sendReceiptMessage({
async #sendReceiptMessage({
senderAci,
timestamps,
type,

View file

@ -83,37 +83,24 @@ export type SocketManagerOptions = Readonly<{
// Incoming requests on unauthenticated resource are not currently supported.
// IWebSocketResource is responsible for their immediate termination.
export class SocketManager extends EventListener {
private backOff = new BackOff(FIBONACCI_TIMEOUTS, {
#backOff = new BackOff(FIBONACCI_TIMEOUTS, {
jitter: JITTER,
});
private authenticated?: AbortableProcess<IWebSocketResource>;
private unauthenticated?: AbortableProcess<IWebSocketResource>;
private unauthenticatedExpirationTimer?: NodeJS.Timeout;
private credentials?: WebAPICredentials;
private lazyProxyAgent?: Promise<ProxyAgent>;
private status = SocketStatus.CLOSED;
private requestHandlers = new Set<IRequestHandler>();
private incomingRequestQueue = new Array<IncomingWebSocketRequest>();
private isNavigatorOffline = false;
private privIsOnline: boolean | undefined;
private isRemotelyExpired = false;
private hasStoriesDisabled: boolean;
private reconnectController: AbortController | undefined;
private envelopeCount = 0;
#authenticated?: AbortableProcess<IWebSocketResource>;
#unauthenticated?: AbortableProcess<IWebSocketResource>;
#unauthenticatedExpirationTimer?: NodeJS.Timeout;
#credentials?: WebAPICredentials;
#lazyProxyAgent?: Promise<ProxyAgent>;
#status = SocketStatus.CLOSED;
#requestHandlers = new Set<IRequestHandler>();
#incomingRequestQueue = new Array<IncomingWebSocketRequest>();
#isNavigatorOffline = false;
#privIsOnline: boolean | undefined;
#isRemotelyExpired = false;
#hasStoriesDisabled: boolean;
#reconnectController: AbortController | undefined;
#envelopeCount = 0;
constructor(
private readonly libsignalNet: Net.Net,
@ -121,16 +108,16 @@ export class SocketManager extends EventListener {
) {
super();
this.hasStoriesDisabled = options.hasStoriesDisabled;
this.#hasStoriesDisabled = options.hasStoriesDisabled;
}
public getStatus(): SocketStatus {
return this.status;
return this.#status;
}
private markOffline() {
if (this.privIsOnline !== false) {
this.privIsOnline = false;
#markOffline() {
if (this.#privIsOnline !== false) {
this.#privIsOnline = false;
this.emit('offline');
}
}
@ -138,7 +125,7 @@ export class SocketManager extends EventListener {
// Update WebAPICredentials and reconnect authenticated resource if
// credentials changed
public async authenticate(credentials: WebAPICredentials): Promise<void> {
if (this.isRemotelyExpired) {
if (this.#isRemotelyExpired) {
throw new HTTPError('SocketManager remotely expired', {
code: 0,
headers: {},
@ -153,13 +140,13 @@ export class SocketManager extends EventListener {
}
if (
this.credentials &&
this.credentials.username === username &&
this.credentials.password === password &&
this.authenticated
this.#credentials &&
this.#credentials.username === username &&
this.#credentials.password === password &&
this.#authenticated
) {
try {
await this.authenticated.getResult();
await this.#authenticated.getResult();
} catch (error) {
log.warn(
'SocketManager: failed to wait for existing authenticated socket ' +
@ -169,61 +156,61 @@ export class SocketManager extends EventListener {
return;
}
this.credentials = credentials;
this.#credentials = credentials;
log.info(
'SocketManager: connecting authenticated socket ' +
`(hasStoriesDisabled=${this.hasStoriesDisabled})`
`(hasStoriesDisabled=${this.#hasStoriesDisabled})`
);
this.setStatus(SocketStatus.CONNECTING);
this.#setStatus(SocketStatus.CONNECTING);
const proxyAgent = await this.getProxyAgent();
const proxyAgent = await this.#getProxyAgent();
const useLibsignalTransport =
window.Signal.RemoteConfig.isEnabled(
'desktop.experimentalTransport.enableAuth'
) && this.transportOption(proxyAgent) === TransportOption.Libsignal;
) && this.#transportOption(proxyAgent) === TransportOption.Libsignal;
const process = useLibsignalTransport
? connectAuthenticatedLibsignal({
libsignalNet: this.libsignalNet,
name: AUTHENTICATED_CHANNEL_NAME,
credentials: this.credentials,
credentials: this.#credentials,
handler: (req: IncomingWebSocketRequest): void => {
this.queueOrHandleRequest(req);
this.#queueOrHandleRequest(req);
},
receiveStories: !this.hasStoriesDisabled,
receiveStories: !this.#hasStoriesDisabled,
keepalive: { path: '/v1/keepalive' },
})
: this.connectResource({
: this.#connectResource({
name: AUTHENTICATED_CHANNEL_NAME,
path: '/v1/websocket/',
resourceOptions: {
name: AUTHENTICATED_CHANNEL_NAME,
keepalive: { path: '/v1/keepalive' },
handleRequest: (req: IncomingWebSocketRequest): void => {
this.queueOrHandleRequest(req);
this.#queueOrHandleRequest(req);
},
},
extraHeaders: {
Authorization: getBasicAuth({ username, password }),
'X-Signal-Receive-Stories': String(!this.hasStoriesDisabled),
'X-Signal-Receive-Stories': String(!this.#hasStoriesDisabled),
},
proxyAgent,
});
// Cancel previous connect attempt or close socket
this.authenticated?.abort();
this.#authenticated?.abort();
this.authenticated = process;
this.#authenticated = process;
const reconnect = async (): Promise<void> => {
if (this.isRemotelyExpired) {
if (this.#isRemotelyExpired) {
log.info('SocketManager: remotely expired, not reconnecting');
return;
}
const timeout = this.backOff.getAndIncrement();
const timeout = this.#backOff.getAndIncrement();
log.info(
'SocketManager: reconnecting authenticated socket ' +
@ -231,7 +218,7 @@ export class SocketManager extends EventListener {
);
const reconnectController = new AbortController();
this.reconnectController = reconnectController;
this.#reconnectController = reconnectController;
try {
await sleep(timeout, reconnectController.signal);
@ -239,20 +226,20 @@ export class SocketManager extends EventListener {
log.info('SocketManager: reconnect cancelled');
return;
} finally {
if (this.reconnectController === reconnectController) {
this.reconnectController = undefined;
if (this.#reconnectController === reconnectController) {
this.#reconnectController = undefined;
}
}
if (this.authenticated) {
if (this.#authenticated) {
log.info('SocketManager: authenticated socket already connecting');
return;
}
strictAssert(this.credentials !== undefined, 'Missing credentials');
strictAssert(this.#credentials !== undefined, 'Missing credentials');
try {
await this.authenticate(this.credentials);
await this.authenticate(this.#credentials);
} catch (error) {
log.info(
'SocketManager: authenticated socket failed to reconnect ' +
@ -265,7 +252,7 @@ export class SocketManager extends EventListener {
let authenticated: IWebSocketResource;
try {
authenticated = await process.getResult();
this.setStatus(SocketStatus.OPEN);
this.#setStatus(SocketStatus.OPEN);
} catch (error) {
log.warn(
'SocketManager: authenticated socket connection failed with ' +
@ -273,11 +260,11 @@ export class SocketManager extends EventListener {
);
// The socket was deliberately closed, don't follow up
if (this.authenticated !== process) {
if (this.#authenticated !== process) {
return;
}
this.dropAuthenticated(process);
this.#dropAuthenticated(process);
if (error instanceof HTTPError) {
const { code } = error;
@ -293,10 +280,10 @@ export class SocketManager extends EventListener {
}
if (code === -1) {
this.markOffline();
this.#markOffline();
}
} else if (error instanceof ConnectTimeoutError) {
this.markOffline();
this.#markOffline();
} else if (
error instanceof LibSignalErrorBase &&
error.code === ErrorCode.DeviceDelinked
@ -320,11 +307,11 @@ export class SocketManager extends EventListener {
);
window.logAuthenticatedConnect?.();
this.envelopeCount = 0;
this.backOff.reset();
this.#envelopeCount = 0;
this.#backOff.reset();
authenticated.addEventListener('close', ({ code, reason }): void => {
if (this.authenticated !== process) {
if (this.#authenticated !== process) {
return;
}
@ -332,7 +319,7 @@ export class SocketManager extends EventListener {
'SocketManager: authenticated socket closed ' +
`with code=${code} and reason=${reason}`
);
this.dropAuthenticated(process);
this.#dropAuthenticated(process);
if (code === NORMAL_DISCONNECT_CODE) {
// Intentional disconnect
@ -351,27 +338,27 @@ export class SocketManager extends EventListener {
// Either returns currently connecting/active authenticated
// IWebSocketResource or connects a fresh one.
public async getAuthenticatedResource(): Promise<IWebSocketResource> {
if (!this.authenticated) {
strictAssert(this.credentials !== undefined, 'Missing credentials');
await this.authenticate(this.credentials);
if (!this.#authenticated) {
strictAssert(this.#credentials !== undefined, 'Missing credentials');
await this.authenticate(this.#credentials);
}
strictAssert(this.authenticated !== undefined, 'Authentication failed');
return this.authenticated.getResult();
strictAssert(this.#authenticated !== undefined, 'Authentication failed');
return this.#authenticated.getResult();
}
// Creates new IWebSocketResource for AccountManager's provisioning
public async getProvisioningResource(
handler: IRequestHandler
): Promise<IWebSocketResource> {
if (this.isRemotelyExpired) {
if (this.#isRemotelyExpired) {
throw new Error('Remotely expired, not connecting provisioning socket');
}
return this.connectResource({
return this.#connectResource({
name: 'provisioning',
path: '/v1/websocket/provisioning/',
proxyAgent: await this.getProxyAgent(),
proxyAgent: await this.#getProxyAgent(),
resourceOptions: {
name: 'provisioning',
handleRequest: (req: IncomingWebSocketRequest): void => {
@ -390,7 +377,7 @@ export class SocketManager extends EventListener {
url: string;
extraHeaders?: Record<string, string>;
}): Promise<WebSocket> {
const proxyAgent = await this.getProxyAgent();
const proxyAgent = await this.#getProxyAgent();
return connectWebSocket({
name: 'art-creator-provisioning',
@ -412,11 +399,11 @@ export class SocketManager extends EventListener {
const headers = new Headers(init.headers);
let resource: IWebSocketResource;
if (this.isAuthenticated(headers)) {
if (this.#isAuthenticated(headers)) {
resource = await this.getAuthenticatedResource();
} else {
resource = await this.getUnauthenticatedResource();
await this.startUnauthenticatedExpirationTimer(resource);
resource = await this.#getUnauthenticatedResource();
await this.#startUnauthenticatedExpirationTimer(resource);
}
const { path } = URL.parse(url);
@ -460,9 +447,9 @@ export class SocketManager extends EventListener {
}
public registerRequestHandler(handler: IRequestHandler): void {
this.requestHandlers.add(handler);
this.#requestHandlers.add(handler);
const queue = this.incomingRequestQueue;
const queue = this.#incomingRequestQueue;
if (queue.length === 0) {
return;
}
@ -470,22 +457,22 @@ export class SocketManager extends EventListener {
log.info(
`SocketManager: processing ${queue.length} queued incoming requests`
);
this.incomingRequestQueue = [];
this.#incomingRequestQueue = [];
for (const req of queue) {
this.queueOrHandleRequest(req);
this.#queueOrHandleRequest(req);
}
}
public unregisterRequestHandler(handler: IRequestHandler): void {
this.requestHandlers.delete(handler);
this.#requestHandlers.delete(handler);
}
public async onHasStoriesDisabledChange(newValue: boolean): Promise<void> {
if (this.hasStoriesDisabled === newValue) {
if (this.#hasStoriesDisabled === newValue) {
return;
}
this.hasStoriesDisabled = newValue;
this.#hasStoriesDisabled = newValue;
log.info(
`SocketManager: reconnecting after setting hasStoriesDisabled=${newValue}`
);
@ -495,24 +482,25 @@ export class SocketManager extends EventListener {
public async reconnect(): Promise<void> {
log.info('SocketManager.reconnect: starting...');
const { authenticated, unauthenticated } = this;
const unauthenticated = this.#unauthenticated;
const authenticated = this.#authenticated;
if (authenticated) {
authenticated.abort();
this.dropAuthenticated(authenticated);
this.#dropAuthenticated(authenticated);
}
if (unauthenticated) {
unauthenticated.abort();
this.dropUnauthenticated(unauthenticated);
this.#dropUnauthenticated(unauthenticated);
}
if (this.credentials) {
this.backOff.reset();
if (this.#credentials) {
this.#backOff.reset();
// Cancel old reconnect attempt
this.reconnectController?.abort();
this.#reconnectController?.abort();
// Start the new attempt
await this.authenticate(this.credentials);
await this.authenticate(this.#credentials);
}
log.info('SocketManager.reconnect: complete.');
@ -522,71 +510,71 @@ export class SocketManager extends EventListener {
public async check(): Promise<void> {
log.info('SocketManager.check');
await Promise.all([
this.checkResource(this.authenticated),
this.checkResource(this.unauthenticated),
this.#checkResource(this.#authenticated),
this.#checkResource(this.#unauthenticated),
]);
}
public async onNavigatorOnline(): Promise<void> {
log.info('SocketManager.onNavigatorOnline');
this.isNavigatorOffline = false;
this.backOff.reset(FIBONACCI_TIMEOUTS);
this.#isNavigatorOffline = false;
this.#backOff.reset(FIBONACCI_TIMEOUTS);
// Reconnect earlier if waiting
if (this.credentials !== undefined) {
this.reconnectController?.abort();
await this.authenticate(this.credentials);
if (this.#credentials !== undefined) {
this.#reconnectController?.abort();
await this.authenticate(this.#credentials);
}
}
public async onNavigatorOffline(): Promise<void> {
log.info('SocketManager.onNavigatorOffline');
this.isNavigatorOffline = true;
this.backOff.reset(EXTENDED_FIBONACCI_TIMEOUTS);
this.#isNavigatorOffline = true;
this.#backOff.reset(EXTENDED_FIBONACCI_TIMEOUTS);
await this.check();
}
public async onRemoteExpiration(): Promise<void> {
log.info('SocketManager.onRemoteExpiration');
this.isRemotelyExpired = true;
this.#isRemotelyExpired = true;
// Cancel reconnect attempt if any
this.reconnectController?.abort();
this.#reconnectController?.abort();
}
public async logout(): Promise<void> {
const { authenticated } = this;
const authenticated = this.#authenticated;
if (authenticated) {
authenticated.abort();
this.dropAuthenticated(authenticated);
this.#dropAuthenticated(authenticated);
}
this.credentials = undefined;
this.#credentials = undefined;
}
public get isOnline(): boolean | undefined {
return this.privIsOnline;
return this.#privIsOnline;
}
//
// Private
//
private setStatus(status: SocketStatus): void {
if (this.status === status) {
#setStatus(status: SocketStatus): void {
if (this.#status === status) {
return;
}
this.status = status;
this.#status = status;
this.emit('statusChange');
if (this.status === SocketStatus.OPEN && !this.privIsOnline) {
this.privIsOnline = true;
if (this.#status === SocketStatus.OPEN && !this.#privIsOnline) {
this.#privIsOnline = true;
this.emit('online');
}
}
private transportOption(proxyAgent: ProxyAgent | undefined): TransportOption {
#transportOption(proxyAgent: ProxyAgent | undefined): TransportOption {
const { hostname } = URL.parse(this.options.url);
// transport experiment doesn't support proxy
@ -629,17 +617,17 @@ export class SocketManager extends EventListener {
: TransportOption.Original;
}
private async getUnauthenticatedResource(): Promise<IWebSocketResource> {
async #getUnauthenticatedResource(): Promise<IWebSocketResource> {
// awaiting on `this.getProxyAgent()` needs to happen here
// so that there are no calls to `await` between checking
// the value of `this.unauthenticated` and assigning it later in this function
const proxyAgent = await this.getProxyAgent();
const proxyAgent = await this.#getProxyAgent();
if (this.unauthenticated) {
return this.unauthenticated.getResult();
if (this.#unauthenticated) {
return this.#unauthenticated.getResult();
}
if (this.isRemotelyExpired) {
if (this.#isRemotelyExpired) {
throw new HTTPError('SocketManager remotely expired', {
code: 0,
headers: {},
@ -649,7 +637,7 @@ export class SocketManager extends EventListener {
log.info('SocketManager: connecting unauthenticated socket');
const transportOption = this.transportOption(proxyAgent);
const transportOption = this.#transportOption(proxyAgent);
log.info(
`SocketManager: connecting unauthenticated socket, transport option [${transportOption}]`
);
@ -663,7 +651,7 @@ export class SocketManager extends EventListener {
keepalive: { path: '/v1/keepalive' },
});
} else {
process = this.connectResource({
process = this.#connectResource({
name: UNAUTHENTICATED_CHANNEL_NAME,
path: '/v1/websocket/',
proxyAgent,
@ -675,17 +663,17 @@ export class SocketManager extends EventListener {
});
}
this.unauthenticated = process;
this.#unauthenticated = process;
let unauthenticated: IWebSocketResource;
try {
unauthenticated = await this.unauthenticated.getResult();
unauthenticated = await this.#unauthenticated.getResult();
} catch (error) {
log.info(
'SocketManager: failed to connect unauthenticated socket ' +
` due to error: ${Errors.toLogFormat(error)}`
);
this.dropUnauthenticated(process);
this.#dropUnauthenticated(process);
throw error;
}
@ -694,7 +682,7 @@ export class SocketManager extends EventListener {
);
unauthenticated.addEventListener('close', ({ code, reason }): void => {
if (this.unauthenticated !== process) {
if (this.#unauthenticated !== process) {
return;
}
@ -703,13 +691,13 @@ export class SocketManager extends EventListener {
`with code=${code} and reason=${reason}`
);
this.dropUnauthenticated(process);
this.#dropUnauthenticated(process);
});
return this.unauthenticated.getResult();
return this.#unauthenticated.getResult();
}
private connectResource({
#connectResource({
name,
path,
proxyAgent,
@ -757,7 +745,10 @@ export class SocketManager extends EventListener {
resourceOptions.transportOption === TransportOption.Original;
return shadowingModeEnabled
? webSocketResourceConnection
: this.connectWithShadowing(webSocketResourceConnection, resourceOptions);
: this.#connectWithShadowing(
webSocketResourceConnection,
resourceOptions
);
}
/**
@ -774,7 +765,7 @@ export class SocketManager extends EventListener {
* @param options `WebSocketResourceOptions` options
* @private
*/
private connectWithShadowing(
#connectWithShadowing(
mainConnection: AbortableProcess<WebSocketResource>,
options: WebSocketResourceOptions
): AbortableProcess<IWebSocketResource> {
@ -809,7 +800,7 @@ export class SocketManager extends EventListener {
);
}
private async checkResource(
async #checkResource(
process?: AbortableProcess<IWebSocketResource>
): Promise<void> {
if (!process) {
@ -820,41 +811,37 @@ export class SocketManager extends EventListener {
// Force shorter timeout if we think we might be offline
resource.forceKeepAlive(
this.isNavigatorOffline ? OFFLINE_KEEPALIVE_TIMEOUT_MS : undefined
this.#isNavigatorOffline ? OFFLINE_KEEPALIVE_TIMEOUT_MS : undefined
);
}
private dropAuthenticated(
process: AbortableProcess<IWebSocketResource>
): void {
if (this.authenticated !== process) {
#dropAuthenticated(process: AbortableProcess<IWebSocketResource>): void {
if (this.#authenticated !== process) {
return;
}
this.incomingRequestQueue = [];
this.authenticated = undefined;
this.setStatus(SocketStatus.CLOSED);
this.#incomingRequestQueue = [];
this.#authenticated = undefined;
this.#setStatus(SocketStatus.CLOSED);
}
private dropUnauthenticated(
process: AbortableProcess<IWebSocketResource>
): void {
if (this.unauthenticated !== process) {
#dropUnauthenticated(process: AbortableProcess<IWebSocketResource>): void {
if (this.#unauthenticated !== process) {
return;
}
this.unauthenticated = undefined;
if (!this.unauthenticatedExpirationTimer) {
this.#unauthenticated = undefined;
if (!this.#unauthenticatedExpirationTimer) {
return;
}
clearTimeout(this.unauthenticatedExpirationTimer);
this.unauthenticatedExpirationTimer = undefined;
clearTimeout(this.#unauthenticatedExpirationTimer);
this.#unauthenticatedExpirationTimer = undefined;
}
private async startUnauthenticatedExpirationTimer(
async #startUnauthenticatedExpirationTimer(
expected: IWebSocketResource
): Promise<void> {
const process = this.unauthenticated;
const process = this.#unauthenticated;
strictAssert(
process !== undefined,
'Unauthenticated socket must be connected'
@ -866,28 +853,28 @@ export class SocketManager extends EventListener {
'Unauthenticated resource should be the same'
);
if (this.unauthenticatedExpirationTimer) {
if (this.#unauthenticatedExpirationTimer) {
return;
}
log.info(
'SocketManager: starting expiration timer for unauthenticated socket'
);
this.unauthenticatedExpirationTimer = setTimeout(async () => {
this.#unauthenticatedExpirationTimer = setTimeout(async () => {
log.info(
'SocketManager: shutting down unauthenticated socket after timeout'
);
unauthenticated.shutdown();
// The socket is either deliberately closed or reconnected already
if (this.unauthenticated !== process) {
if (this.#unauthenticated !== process) {
return;
}
this.dropUnauthenticated(process);
this.#dropUnauthenticated(process);
try {
await this.getUnauthenticatedResource();
await this.#getUnauthenticatedResource();
} catch (error) {
log.warn(
'SocketManager: failed to reconnect unauthenticated socket ' +
@ -897,22 +884,22 @@ export class SocketManager extends EventListener {
}, FIVE_MINUTES);
}
private queueOrHandleRequest(req: IncomingWebSocketRequest): void {
#queueOrHandleRequest(req: IncomingWebSocketRequest): void {
if (req.requestType === ServerRequestType.ApiMessage) {
this.envelopeCount += 1;
if (this.envelopeCount === 1) {
this.#envelopeCount += 1;
if (this.#envelopeCount === 1) {
this.emit('firstEnvelope', req);
}
}
if (this.requestHandlers.size === 0) {
this.incomingRequestQueue.push(req);
if (this.#requestHandlers.size === 0) {
this.#incomingRequestQueue.push(req);
log.info(
'SocketManager: request handler unavailable, ' +
`queued request. Queue size: ${this.incomingRequestQueue.length}`
`queued request. Queue size: ${this.#incomingRequestQueue.length}`
);
return;
}
for (const handlers of this.requestHandlers) {
for (const handlers of this.#requestHandlers) {
try {
handlers.handleRequest(req);
} catch (error) {
@ -924,8 +911,8 @@ export class SocketManager extends EventListener {
}
}
private isAuthenticated(headers: Headers): boolean {
if (!this.credentials) {
#isAuthenticated(headers: Headers): boolean {
if (!this.#credentials) {
return false;
}
@ -946,17 +933,17 @@ export class SocketManager extends EventListener {
);
return (
username === this.credentials.username &&
password === this.credentials.password
username === this.#credentials.username &&
password === this.#credentials.password
);
}
private async getProxyAgent(): Promise<ProxyAgent | undefined> {
if (this.options.proxyUrl && !this.lazyProxyAgent) {
async #getProxyAgent(): Promise<ProxyAgent | undefined> {
if (this.options.proxyUrl && !this.#lazyProxyAgent) {
// Cache the promise so that we don't import concurrently.
this.lazyProxyAgent = createProxyAgent(this.options.proxyUrl);
this.#lazyProxyAgent = createProxyAgent(this.options.proxyUrl);
}
return this.lazyProxyAgent;
return this.#lazyProxyAgent;
}
// EventEmitter types

View file

@ -18,13 +18,10 @@ export class Storage implements StorageInterface {
public readonly blocked: Blocked;
private ready = false;
private readyCallbacks: Array<() => void> = [];
private items: Partial<Access> = Object.create(null);
private privProtocol: SignalProtocolStore | undefined;
#ready = false;
#readyCallbacks: Array<() => void> = [];
#items: Partial<Access> = Object.create(null);
#privProtocol: SignalProtocolStore | undefined;
constructor() {
this.user = new User(this);
@ -35,14 +32,14 @@ export class Storage implements StorageInterface {
get protocol(): SignalProtocolStore {
assertDev(
this.privProtocol !== undefined,
this.#privProtocol !== undefined,
'SignalProtocolStore not initialized'
);
return this.privProtocol;
return this.#privProtocol;
}
set protocol(value: SignalProtocolStore) {
this.privProtocol = value;
this.#privProtocol = value;
}
// `StorageInterface` implementation
@ -60,11 +57,11 @@ export class Storage implements StorageInterface {
key: K,
defaultValue?: Access[K]
): Access[K] | undefined {
if (!this.ready) {
if (!this.#ready) {
log.warn('Called storage.get before storage is ready. key:', key);
}
const item = this.items[key];
const item = this.#items[key];
if (item === undefined) {
return defaultValue;
}
@ -76,22 +73,22 @@ export class Storage implements StorageInterface {
key: K,
value: Access[K]
): Promise<void> {
if (!this.ready) {
if (!this.#ready) {
log.warn('Called storage.put before storage is ready. key:', key);
}
this.items[key] = value;
this.#items[key] = value;
await DataWriter.createOrUpdateItem({ id: key, value });
window.reduxActions?.items.putItemExternal(key, value);
}
public async remove<K extends keyof Access>(key: K): Promise<void> {
if (!this.ready) {
if (!this.#ready) {
log.warn('Called storage.remove before storage is ready. key:', key);
}
delete this.items[key];
delete this.#items[key];
await DataWriter.removeItemById(key);
window.reduxActions?.items.removeItemExternal(key);
@ -100,29 +97,29 @@ export class Storage implements StorageInterface {
// Regular methods
public onready(callback: () => void): void {
if (this.ready) {
if (this.#ready) {
callback();
} else {
this.readyCallbacks.push(callback);
this.#readyCallbacks.push(callback);
}
}
public async fetch(): Promise<void> {
this.reset();
Object.assign(this.items, await DataReader.getAllItems());
Object.assign(this.#items, await DataReader.getAllItems());
this.ready = true;
this.callListeners();
this.#ready = true;
this.#callListeners();
}
public reset(): void {
this.ready = false;
this.items = Object.create(null);
this.#ready = false;
this.#items = Object.create(null);
}
public getItemsState(): Partial<Access> {
if (!this.ready) {
if (!this.#ready) {
log.warn('Called getItemsState before storage is ready');
}
@ -130,8 +127,7 @@ export class Storage implements StorageInterface {
const state = Object.create(null);
// TypeScript isn't smart enough to figure out the types automatically.
const { items } = this;
const items = this.#items;
const allKeys = Object.keys(items) as Array<keyof typeof items>;
for (const key of allKeys) {
@ -141,12 +137,12 @@ export class Storage implements StorageInterface {
return state;
}
private callListeners(): void {
if (!this.ready) {
#callListeners(): void {
if (!this.#ready) {
return;
}
const callbacks = this.readyCallbacks;
this.readyCallbacks = [];
const callbacks = this.#readyCallbacks;
this.#readyCallbacks = [];
callbacks.forEach(callback => callback());
}
}

View file

@ -15,7 +15,7 @@ import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
import * as Errors from '../types/errors';
class SyncRequestInner extends EventTarget {
private started = false;
#started = false;
contactSync?: boolean;
@ -44,14 +44,14 @@ class SyncRequestInner extends EventTarget {
}
async start(): Promise<void> {
if (this.started) {
if (this.#started) {
assertDev(
false,
'SyncRequestInner: started more than once. Doing nothing'
);
return;
}
this.started = true;
this.#started = true;
if (window.ConversationController.areWePrimaryDevice()) {
log.warn('SyncRequest.start: We are primary device; returning early');
@ -108,7 +108,7 @@ class SyncRequestInner extends EventTarget {
}
export default class SyncRequest {
private inner: SyncRequestInner;
#inner: SyncRequestInner;
addEventListener: (
name: 'success' | 'timeout',
@ -122,12 +122,12 @@ export default class SyncRequest {
constructor(receiver: MessageReceiver, timeoutMillis?: number) {
const inner = new SyncRequestInner(receiver, timeoutMillis);
this.inner = inner;
this.#inner = inner;
this.addEventListener = inner.addEventListener.bind(inner);
this.removeEventListener = inner.removeEventListener.bind(inner);
}
start(): void {
void this.inner.start();
void this.#inner.start();
}
}

View file

@ -41,16 +41,16 @@ export class UpdateKeysListener {
}
clearTimeoutIfNecessary(this.timeout);
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
this.timeout = setTimeout(() => this.#runWhenOnline(), waitTime);
}
private scheduleNextUpdate(): void {
#scheduleNextUpdate(): void {
const now = Date.now();
const nextTime = now + UPDATE_INTERVAL;
void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, nextTime);
}
private async run(): Promise<void> {
async #run(): Promise<void> {
log.info('UpdateKeysListener: Updating keys...');
try {
const accountManager = window.getAccountManager();
@ -79,7 +79,7 @@ export class UpdateKeysListener {
}
}
this.scheduleNextUpdate();
this.#scheduleNextUpdate();
this.setTimeoutForNextRun();
} catch (error) {
const errorString =
@ -93,9 +93,9 @@ export class UpdateKeysListener {
}
}
private runWhenOnline() {
#runWhenOnline() {
if (window.textsecure.server?.isOnline()) {
void this.run();
void this.#run();
} else {
log.info(
'UpdateKeysListener: We are offline; will update keys when we are next online'

View file

@ -193,7 +193,7 @@ export class IncomingWebSocketRequestLibsignal
export class IncomingWebSocketRequestLegacy
implements IncomingWebSocketRequest
{
private readonly id: Long;
readonly #id: Long;
public readonly requestType: ServerRequestType;
@ -209,7 +209,7 @@ export class IncomingWebSocketRequestLegacy
strictAssert(request.verb, 'request without verb');
strictAssert(request.path, 'request without path');
this.id = request.id;
this.#id = request.id;
this.requestType = resolveType(request.path, request.verb);
this.body = dropNull(request.body);
this.timestamp = resolveTimestamp(request.headers || []);
@ -218,7 +218,7 @@ export class IncomingWebSocketRequestLegacy
public respond(status: number, message: string): void {
const bytes = Proto.WebSocketMessage.encode({
type: Proto.WebSocketMessage.Type.RESPONSE,
response: { id: this.id, message, status },
response: { id: this.#id, message, status },
}).finish();
this.sendBytes(Buffer.from(bytes));
@ -479,7 +479,7 @@ export class LibsignalWebSocketResource
// socket alive using websocket pings, so we don't need a timer-based
// keepalive mechanism. But we still send one-off keepalive requests when
// things change (see forceKeepAlive()).
private keepalive: KeepAliveSender;
#keepalive: KeepAliveSender;
constructor(
private readonly chatService: Net.ChatConnection,
@ -490,7 +490,7 @@ export class LibsignalWebSocketResource
) {
super();
this.keepalive = new KeepAliveSender(this, this.logId, keepalive);
this.#keepalive = new KeepAliveSender(this, this.logId, keepalive);
}
public localPort(): number {
@ -551,7 +551,7 @@ export class LibsignalWebSocketResource
}
public forceKeepAlive(timeout?: number): void {
drop(this.keepalive.send(timeout));
drop(this.#keepalive.send(timeout));
}
public async sendRequest(options: SendRequestOptions): Promise<Response> {
@ -578,28 +578,24 @@ export class LibsignalWebSocketResource
}
export class WebSocketResourceWithShadowing implements IWebSocketResource {
private shadowing: LibsignalWebSocketResource | undefined;
private stats: AggregatedStats;
private statsTimer: NodeJS.Timeout;
private shadowingWithReporting: boolean;
private logId: string;
#shadowing: LibsignalWebSocketResource | undefined;
#stats: AggregatedStats;
#statsTimer: NodeJS.Timeout;
#shadowingWithReporting: boolean;
#logId: string;
constructor(
private readonly main: WebSocketResource,
private readonly shadowingConnection: AbortableProcess<LibsignalWebSocketResource>,
options: WebSocketResourceOptions
) {
this.stats = AggregatedStats.createEmpty();
this.logId = `WebSocketResourceWithShadowing(${options.name})`;
this.statsTimer = setInterval(
() => this.updateStats(options.name),
this.#stats = AggregatedStats.createEmpty();
this.#logId = `WebSocketResourceWithShadowing(${options.name})`;
this.#statsTimer = setInterval(
() => this.#updateStats(options.name),
STATS_UPDATE_INTERVAL
);
this.shadowingWithReporting =
this.#shadowingWithReporting =
options.transportOption === TransportOption.ShadowingHigh;
// the idea is that we want to keep the shadowing connection process
@ -608,33 +604,33 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
// or an error reported in case of connection failure
const initializeAfterConnected = async () => {
try {
this.shadowing = await shadowingConnection.resultPromise;
this.#shadowing = await shadowingConnection.resultPromise;
// checking IP one time per connection
if (this.main.ipVersion() !== this.shadowing.ipVersion()) {
this.stats.ipVersionMismatches += 1;
if (this.main.ipVersion() !== this.#shadowing.ipVersion()) {
this.#stats.ipVersionMismatches += 1;
const mainIpType = this.main.ipVersion();
const shadowIpType = this.shadowing.ipVersion();
const shadowIpType = this.#shadowing.ipVersion();
log.warn(
`${this.logId}: libsignal websocket IP [${shadowIpType}], Desktop websocket IP [${mainIpType}]`
`${this.#logId}: libsignal websocket IP [${shadowIpType}], Desktop websocket IP [${mainIpType}]`
);
}
} catch (error) {
this.stats.connectionFailures += 1;
this.#stats.connectionFailures += 1;
}
};
drop(initializeAfterConnected());
this.addEventListener('close', (_ev): void => {
clearInterval(this.statsTimer);
this.updateStats(options.name);
clearInterval(this.#statsTimer);
this.#updateStats(options.name);
});
}
private updateStats(name: string) {
#updateStats(name: string) {
const storedStats = AggregatedStats.loadOrCreateEmpty(name);
let updatedStats = AggregatedStats.add(storedStats, this.stats);
let updatedStats = AggregatedStats.add(storedStats, this.#stats);
if (
this.shadowingWithReporting &&
this.#shadowingWithReporting &&
AggregatedStats.shouldReportError(updatedStats) &&
!isProduction(window.getVersion())
) {
@ -642,14 +638,14 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
toastType: ToastType.TransportError,
});
log.warn(
`${this.logId}: experimental transport toast displayed, flushing transport statistics before resetting`,
`${this.#logId}: experimental transport toast displayed, flushing transport statistics before resetting`,
updatedStats
);
updatedStats = AggregatedStats.createEmpty();
updatedStats.lastToastTimestamp = Date.now();
}
AggregatedStats.store(updatedStats, name);
this.stats = AggregatedStats.createEmpty();
this.#stats = AggregatedStats.createEmpty();
}
public localPort(): number | undefined {
@ -665,9 +661,9 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
public close(code = NORMAL_DISCONNECT_CODE, reason?: string): void {
this.main.close(code, reason);
if (this.shadowing) {
this.shadowing.close(code, reason);
this.shadowing = undefined;
if (this.#shadowing) {
this.#shadowing.close(code, reason);
this.#shadowing = undefined;
} else {
this.shadowingConnection.abort();
}
@ -675,9 +671,9 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
public shutdown(): void {
this.main.shutdown();
if (this.shadowing) {
this.shadowing.shutdown();
this.shadowing = undefined;
if (this.#shadowing) {
this.#shadowing.shutdown();
this.#shadowing = undefined;
} else {
this.shadowingConnection.abort();
}
@ -695,48 +691,48 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
// attempting to run a healthcheck on a libsignal transport.
if (
isSuccessfulStatusCode(response.status) &&
this.shouldSendShadowRequest()
this.#shouldSendShadowRequest()
) {
drop(this.sendShadowRequest());
drop(this.#sendShadowRequest());
}
return response;
}
private async sendShadowRequest(): Promise<void> {
async #sendShadowRequest(): Promise<void> {
// In the shadowing mode, it could be that we're either
// still connecting libsignal websocket or have already closed it.
// In those cases we're not running shadowing check.
if (!this.shadowing) {
if (!this.#shadowing) {
log.info(
`${this.logId}: skipping healthcheck - websocket not connected or already closed`
`${this.#logId}: skipping healthcheck - websocket not connected or already closed`
);
return;
}
try {
const healthCheckResult = await this.shadowing.sendRequest({
const healthCheckResult = await this.#shadowing.sendRequest({
verb: 'GET',
path: '/v1/keepalive',
timeout: KEEPALIVE_TIMEOUT_MS,
});
this.stats.requestsCompared += 1;
this.#stats.requestsCompared += 1;
if (!isSuccessfulStatusCode(healthCheckResult.status)) {
this.stats.healthcheckBadStatus += 1;
this.#stats.healthcheckBadStatus += 1;
log.warn(
`${this.logId}: keepalive via libsignal responded with status [${healthCheckResult.status}]`
`${this.#logId}: keepalive via libsignal responded with status [${healthCheckResult.status}]`
);
}
} catch (error) {
this.stats.healthcheckFailures += 1;
this.#stats.healthcheckFailures += 1;
log.warn(
`${this.logId}: failed to send keepalive via libsignal`,
`${this.#logId}: failed to send keepalive via libsignal`,
Errors.toLogFormat(error)
);
}
}
private shouldSendShadowRequest(): boolean {
return this.shadowingWithReporting || random(0, 100) < 10;
#shouldSendShadowRequest(): boolean {
return this.#shadowingWithReporting || random(0, 100) < 10;
}
}
@ -748,28 +744,21 @@ export default class WebSocketResource
extends EventTarget
implements IWebSocketResource
{
private outgoingId = Long.fromNumber(1, true);
#outgoingId = Long.fromNumber(1, true);
#closed = false;
private closed = false;
private readonly outgoingMap = new Map<
readonly #outgoingMap = new Map<
string,
(result: SendRequestResult) => void
>();
private readonly boundOnMessage: (message: IMessage) => void;
private activeRequests = new Set<IncomingWebSocketRequest | string>();
private shuttingDown = false;
private shutdownTimer?: Timers.Timeout;
private readonly logId: string;
private readonly localSocketPort: number | undefined;
private readonly socketIpVersion: IpVersion | undefined;
readonly #boundOnMessage: (message: IMessage) => void;
#activeRequests = new Set<IncomingWebSocketRequest | string>();
#shuttingDown = false;
#shutdownTimer?: Timers.Timeout;
readonly #logId: string;
readonly #localSocketPort: number | undefined;
readonly #socketIpVersion: IpVersion | undefined;
// Public for tests
public readonly keepalive?: KeepAlive;
@ -780,25 +769,25 @@ export default class WebSocketResource
) {
super();
this.logId = `WebSocketResource(${options.name})`;
this.localSocketPort = socket.socket.localPort;
this.#logId = `WebSocketResource(${options.name})`;
this.#localSocketPort = socket.socket.localPort;
if (!socket.socket.localAddress) {
this.socketIpVersion = undefined;
this.#socketIpVersion = undefined;
}
if (socket.socket.localAddress == null) {
this.socketIpVersion = undefined;
this.#socketIpVersion = undefined;
} else if (net.isIPv4(socket.socket.localAddress)) {
this.socketIpVersion = IpVersion.IPv4;
this.#socketIpVersion = IpVersion.IPv4;
} else if (net.isIPv6(socket.socket.localAddress)) {
this.socketIpVersion = IpVersion.IPv6;
this.#socketIpVersion = IpVersion.IPv6;
} else {
this.socketIpVersion = undefined;
this.#socketIpVersion = undefined;
}
this.boundOnMessage = this.onMessage.bind(this);
this.#boundOnMessage = this.#onMessage.bind(this);
socket.on('message', this.boundOnMessage);
socket.on('message', this.#boundOnMessage);
if (options.keepalive) {
const keepalive = new KeepAlive(
@ -811,26 +800,26 @@ export default class WebSocketResource
keepalive.reset();
socket.on('close', () => this.keepalive?.stop());
socket.on('error', (error: Error) => {
log.warn(`${this.logId}: WebSocket error`, Errors.toLogFormat(error));
log.warn(`${this.#logId}: WebSocket error`, Errors.toLogFormat(error));
});
}
socket.on('close', (code, reason) => {
this.closed = true;
this.#closed = true;
log.warn(`${this.logId}: Socket closed`);
log.warn(`${this.#logId}: Socket closed`);
this.dispatchEvent(new CloseEvent(code, reason || 'normal'));
});
this.addEventListener('close', () => this.onClose());
this.addEventListener('close', () => this.#onClose());
}
public ipVersion(): IpVersion | undefined {
return this.socketIpVersion;
return this.#socketIpVersion;
}
public localPort(): number | undefined {
return this.localSocketPort;
return this.#localSocketPort;
}
public override addEventListener(
@ -843,12 +832,15 @@ export default class WebSocketResource
}
public async sendRequest(options: SendRequestOptions): Promise<Response> {
const id = this.outgoingId;
const id = this.#outgoingId;
const idString = id.toString();
strictAssert(!this.outgoingMap.has(idString), 'Duplicate outgoing request');
strictAssert(
!this.#outgoingMap.has(idString),
'Duplicate outgoing request'
);
// Note that this automatically wraps
this.outgoingId = this.outgoingId.add(1);
this.#outgoingId = this.#outgoingId.add(1);
const bytes = Proto.WebSocketMessage.encode({
type: Proto.WebSocketMessage.Type.REQUEST,
@ -871,25 +863,25 @@ export default class WebSocketResource
'WebSocket request byte size exceeded'
);
strictAssert(!this.shuttingDown, 'Cannot send request, shutting down');
this.addActive(idString);
strictAssert(!this.#shuttingDown, 'Cannot send request, shutting down');
this.#addActive(idString);
const promise = new Promise<SendRequestResult>((resolve, reject) => {
let timer = options.timeout
? Timers.setTimeout(() => {
this.removeActive(idString);
this.#removeActive(idString);
this.close(UNEXPECTED_DISCONNECT_CODE, 'Request timed out');
reject(new Error(`Request timed out; id: [${idString}]`));
}, options.timeout)
: undefined;
this.outgoingMap.set(idString, result => {
this.#outgoingMap.set(idString, result => {
if (timer !== undefined) {
Timers.clearTimeout(timer);
timer = undefined;
}
this.keepalive?.reset();
this.removeActive(idString);
this.#removeActive(idString);
resolve(result);
});
});
@ -908,58 +900,58 @@ export default class WebSocketResource
}
public close(code = NORMAL_DISCONNECT_CODE, reason?: string): void {
if (this.closed) {
log.info(`${this.logId}.close: Already closed! ${code}/${reason}`);
if (this.#closed) {
log.info(`${this.#logId}.close: Already closed! ${code}/${reason}`);
return;
}
log.info(`${this.logId}.close(${code})`);
log.info(`${this.#logId}.close(${code})`);
if (this.keepalive) {
this.keepalive.stop();
}
this.socket.close(code, reason);
this.socket.removeListener('message', this.boundOnMessage);
this.socket.removeListener('message', this.#boundOnMessage);
// On linux the socket can wait a long time to emit its close event if we've
// lost the internet connection. On the order of minutes. This speeds that
// process up.
Timers.setTimeout(() => {
if (this.closed) {
if (this.#closed) {
return;
}
log.warn(`${this.logId}.close: Dispatching our own socket close event`);
log.warn(`${this.#logId}.close: Dispatching our own socket close event`);
this.dispatchEvent(new CloseEvent(code, reason || 'normal'));
}, 5 * durations.SECOND);
}
public shutdown(): void {
if (this.closed) {
if (this.#closed) {
return;
}
if (this.activeRequests.size === 0) {
log.info(`${this.logId}.shutdown: no active requests, closing`);
if (this.#activeRequests.size === 0) {
log.info(`${this.#logId}.shutdown: no active requests, closing`);
this.close(NORMAL_DISCONNECT_CODE, 'Shutdown');
return;
}
this.shuttingDown = true;
this.#shuttingDown = true;
log.info(`${this.logId}.shutdown: shutting down`);
this.shutdownTimer = Timers.setTimeout(() => {
if (this.closed) {
log.info(`${this.#logId}.shutdown: shutting down`);
this.#shutdownTimer = Timers.setTimeout(() => {
if (this.#closed) {
return;
}
log.warn(`${this.logId}.shutdown: Failed to shutdown gracefully`);
log.warn(`${this.#logId}.shutdown: Failed to shutdown gracefully`);
this.close(NORMAL_DISCONNECT_CODE, 'Shutdown');
}, THIRTY_SECONDS);
}
private onMessage({ type, binaryData }: IMessage): void {
#onMessage({ type, binaryData }: IMessage): void {
if (type !== 'binary' || !binaryData) {
throw new Error(`Unsupported websocket message type: ${type}`);
}
@ -976,7 +968,7 @@ export default class WebSocketResource
const incomingRequest = new IncomingWebSocketRequestLegacy(
message.request,
(bytes: Buffer): void => {
this.removeActive(incomingRequest);
this.#removeActive(incomingRequest);
strictAssert(
bytes.length <= MAX_MESSAGE_SIZE,
@ -986,12 +978,12 @@ export default class WebSocketResource
}
);
if (this.shuttingDown) {
if (this.#shuttingDown) {
incomingRequest.respond(-1, 'Shutting down');
return;
}
this.addActive(incomingRequest);
this.#addActive(incomingRequest);
handleRequest(incomingRequest);
} else if (
message.type === Proto.WebSocketMessage.Type.RESPONSE &&
@ -1001,8 +993,8 @@ export default class WebSocketResource
strictAssert(response.id, 'response without id');
const responseIdString = response.id.toString();
const resolve = this.outgoingMap.get(responseIdString);
this.outgoingMap.delete(responseIdString);
const resolve = this.#outgoingMap.get(responseIdString);
this.#outgoingMap.delete(responseIdString);
if (!resolve) {
throw new Error(`Received response for unknown request ${response.id}`);
@ -1017,9 +1009,9 @@ export default class WebSocketResource
}
}
private onClose(): void {
const outgoing = new Map(this.outgoingMap);
this.outgoingMap.clear();
#onClose(): void {
const outgoing = new Map(this.#outgoingMap);
this.#outgoingMap.clear();
for (const resolve of outgoing.values()) {
resolve({
@ -1031,30 +1023,30 @@ export default class WebSocketResource
}
}
private addActive(request: IncomingWebSocketRequest | string): void {
this.activeRequests.add(request);
#addActive(request: IncomingWebSocketRequest | string): void {
this.#activeRequests.add(request);
}
private removeActive(request: IncomingWebSocketRequest | string): void {
if (!this.activeRequests.has(request)) {
log.warn(`${this.logId}.removeActive: removing unknown request`);
#removeActive(request: IncomingWebSocketRequest | string): void {
if (!this.#activeRequests.has(request)) {
log.warn(`${this.#logId}.removeActive: removing unknown request`);
return;
}
this.activeRequests.delete(request);
if (this.activeRequests.size !== 0) {
this.#activeRequests.delete(request);
if (this.#activeRequests.size !== 0) {
return;
}
if (!this.shuttingDown) {
if (!this.#shuttingDown) {
return;
}
if (this.shutdownTimer) {
Timers.clearTimeout(this.shutdownTimer);
this.shutdownTimer = undefined;
if (this.#shutdownTimer) {
Timers.clearTimeout(this.#shutdownTimer);
this.#shutdownTimer = undefined;
}
log.info(`${this.logId}.removeActive: shutdown complete`);
log.info(`${this.#logId}.removeActive: shutdown complete`);
this.close(NORMAL_DISCONNECT_CODE, 'Shutdown');
}
@ -1109,7 +1101,7 @@ const LOG_KEEPALIVE_AFTER_MS = 500;
* intervals.
*/
class KeepAliveSender {
private path: string;
#path: string;
protected wsr: IWebSocketResource;
@ -1121,7 +1113,7 @@ class KeepAliveSender {
opts: KeepAliveOptionsType = {}
) {
this.logId = `WebSocketResources.KeepAlive(${name})`;
this.path = opts.path ?? '/';
this.#path = opts.path ?? '/';
this.wsr = websocketResource;
}
@ -1133,7 +1125,7 @@ class KeepAliveSender {
const { status } = await pTimeout(
this.wsr.sendRequest({
verb: 'GET',
path: this.path,
path: this.#path,
}),
timeout
);
@ -1176,9 +1168,8 @@ class KeepAliveSender {
* {@link KeepAliveSender}.
*/
class KeepAlive extends KeepAliveSender {
private keepAliveTimer: Timers.Timeout | undefined;
private lastAliveAt: number = Date.now();
#keepAliveTimer: Timers.Timeout | undefined;
#lastAliveAt: number = Date.now();
constructor(
websocketResource: WebSocketResource,
@ -1189,18 +1180,18 @@ class KeepAlive extends KeepAliveSender {
}
public stop(): void {
this.clearTimers();
this.#clearTimers();
}
public override async send(timeout = KEEPALIVE_TIMEOUT_MS): Promise<boolean> {
this.clearTimers();
this.#clearTimers();
const isStale = isOlderThan(this.lastAliveAt, STALE_THRESHOLD_MS);
const isStale = isOlderThan(this.#lastAliveAt, STALE_THRESHOLD_MS);
if (isStale) {
log.info(`${this.logId}.send: disconnecting due to stale state`);
this.wsr.close(
UNEXPECTED_DISCONNECT_CODE,
`Last keepalive request was too far in the past: ${this.lastAliveAt}`
`Last keepalive request was too far in the past: ${this.#lastAliveAt}`
);
return false;
}
@ -1216,20 +1207,20 @@ class KeepAlive extends KeepAliveSender {
}
public reset(): void {
this.lastAliveAt = Date.now();
this.#lastAliveAt = Date.now();
this.clearTimers();
this.#clearTimers();
this.keepAliveTimer = Timers.setTimeout(
this.#keepAliveTimer = Timers.setTimeout(
() => this.send(),
KEEPALIVE_INTERVAL_MS
);
}
private clearTimers(): void {
if (this.keepAliveTimer) {
Timers.clearTimeout(this.keepAliveTimer);
this.keepAliveTimer = undefined;
#clearTimers(): void {
if (this.#keepAliveTimer) {
Timers.clearTimeout(this.#keepAliveTimer);
this.#keepAliveTimer = undefined;
}
}
}

View file

@ -15,12 +15,12 @@ export type CDSIOptionsType = Readonly<{
CDSSocketManagerBaseOptionsType;
export class CDSI extends CDSSocketManagerBase<CDSISocket, CDSIOptionsType> {
private readonly mrenclave: Buffer;
readonly #mrenclave: Buffer;
constructor(libsignalNet: Net.Net, options: CDSIOptionsType) {
super(libsignalNet, options);
this.mrenclave = Buffer.from(Bytes.fromHex(options.mrenclave));
this.#mrenclave = Buffer.from(Bytes.fromHex(options.mrenclave));
}
protected override getSocketUrl(): string {
@ -33,7 +33,7 @@ export class CDSI extends CDSSocketManagerBase<CDSISocket, CDSIOptionsType> {
return new CDSISocket({
logger: this.logger,
socket,
mrenclave: this.mrenclave,
mrenclave: this.#mrenclave,
});
}
}

View file

@ -14,7 +14,7 @@ export type CDSISocketOptionsType = Readonly<{
CDSSocketBaseOptionsType;
export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
private privCdsClient: Cds2Client | undefined;
#privCdsClient: Cds2Client | undefined;
public override async handshake(): Promise<void> {
strictAssert(
@ -31,23 +31,23 @@ export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
const earliestValidTimestamp = new Date();
strictAssert(
this.privCdsClient === undefined,
this.#privCdsClient === undefined,
'CDSI handshake called twice'
);
this.privCdsClient = Cds2Client.new(
this.#privCdsClient = Cds2Client.new(
this.options.mrenclave,
attestationMessage,
earliestValidTimestamp
);
}
this.socket.sendBytes(this.cdsClient.initialRequest());
this.socket.sendBytes(this.#cdsClient.initialRequest());
{
const { done, value: message } = await this.socketIterator.next();
strictAssert(!done, 'CDSI socket expected handshake data');
this.cdsClient.completeHandshake(message);
this.#cdsClient.completeHandshake(message);
}
this.state = CDSSocketState.Established;
@ -57,7 +57,7 @@ export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
_version: number,
request: Buffer
): Promise<void> {
this.socket.sendBytes(this.cdsClient.establishedSend(request));
this.socket.sendBytes(this.#cdsClient.establishedSend(request));
const { done, value: ciphertext } = await this.socketIterator.next();
strictAssert(!done, 'CDSISocket.sendRequest(): expected token message');
@ -70,7 +70,7 @@ export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
strictAssert(token, 'CDSISocket.sendRequest(): expected token');
this.socket.sendBytes(
this.cdsClient.establishedSend(
this.#cdsClient.establishedSend(
Buffer.from(
Proto.CDSClientRequest.encode({
tokenAck: true,
@ -83,15 +83,15 @@ export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
protected override async decryptResponse(
ciphertext: Buffer
): Promise<Buffer> {
return this.cdsClient.establishedRecv(ciphertext);
return this.#cdsClient.establishedRecv(ciphertext);
}
//
// Private
//
private get cdsClient(): Cds2Client {
strictAssert(this.privCdsClient, 'CDSISocket did not start handshake');
return this.privCdsClient;
get #cdsClient(): Cds2Client {
strictAssert(this.#privCdsClient, 'CDSISocket did not start handshake');
return this.#privCdsClient;
}
}

View file

@ -56,7 +56,7 @@ export abstract class CDSSocketBase<
this.logger = options.logger;
this.socket = options.socket;
this.socketIterator = this.iterateSocket();
this.socketIterator = this.#iterateSocket();
}
public async close(code: number, reason: string): Promise<void> {
@ -161,7 +161,7 @@ export abstract class CDSSocketBase<
// Private
//
private iterateSocket(): AsyncIterator<Buffer> {
#iterateSocket(): AsyncIterator<Buffer> {
const stream = new Readable({ read: noop, objectMode: true });
this.socket.on('message', ({ type, binaryData }) => {

View file

@ -41,7 +41,7 @@ export abstract class CDSSocketManagerBase<
Socket extends CDSSocketBase,
Options extends CDSSocketManagerBaseOptionsType,
> extends CDSBase<Options> {
private retryAfter?: number;
#retryAfter?: number;
constructor(
private readonly libsignalNet: Net.Net,
@ -55,27 +55,27 @@ export abstract class CDSSocketManagerBase<
): Promise<CDSResponseType> {
const log = this.logger;
if (this.retryAfter !== undefined) {
const delay = Math.max(0, this.retryAfter - Date.now());
if (this.#retryAfter !== undefined) {
const delay = Math.max(0, this.#retryAfter - Date.now());
log.info(`CDSSocketManager: waiting ${delay}ms before retrying`);
await sleep(delay);
}
if (options.useLibsignal) {
return this.requestViaLibsignal(options);
return this.#requestViaLibsignal(options);
}
return this.requestViaNativeSocket(options);
return this.#requestViaNativeSocket(options);
}
private async requestViaNativeSocket(
async #requestViaNativeSocket(
options: CDSRequestOptionsType
): Promise<CDSResponseType> {
const log = this.logger;
const auth = await this.getAuth();
log.info('CDSSocketManager: connecting socket');
const socket = await this.connect(auth).getResult();
const socket = await this.#connect(auth).getResult();
log.info('CDSSocketManager: connected socket');
try {
@ -97,8 +97,8 @@ export abstract class CDSSocketManagerBase<
} catch (error) {
if (error instanceof RateLimitedError) {
if (error.retryAfterSecs > 0) {
this.retryAfter = Math.max(
this.retryAfter ?? Date.now(),
this.#retryAfter = Math.max(
this.#retryAfter ?? Date.now(),
Date.now() + error.retryAfterSecs * durations.SECOND
);
}
@ -110,7 +110,7 @@ export abstract class CDSSocketManagerBase<
}
}
private async requestViaLibsignal(
async #requestViaLibsignal(
options: CDSRequestOptionsType
): Promise<CDSResponseType> {
const log = this.logger;
@ -139,8 +139,8 @@ export abstract class CDSSocketManagerBase<
error.code === LibSignalErrorCode.RateLimitedError
) {
const retryError = error as NetRateLimitedError;
this.retryAfter = Math.max(
this.retryAfter ?? Date.now(),
this.#retryAfter = Math.max(
this.#retryAfter ?? Date.now(),
Date.now() + retryError.retryAfterSecs * durations.SECOND
);
}
@ -148,7 +148,7 @@ export abstract class CDSSocketManagerBase<
}
}
private connect(auth: CDSAuthType): AbortableProcess<Socket> {
#connect(auth: CDSAuthType): AbortableProcess<Socket> {
return connectWebSocket<Socket>({
name: 'CDSSocket',
url: this.getSocketUrl(),

View file

@ -142,7 +142,8 @@ export class User {
}
public getDeviceId(): number | undefined {
const value = this._getDeviceIdFromUuid() || this._getDeviceIdFromNumber();
const value =
this.#_getDeviceIdFromUuid() || this.#_getDeviceIdFromNumber();
if (value === undefined) {
return undefined;
}
@ -202,7 +203,7 @@ export class User {
};
}
private _getDeviceIdFromUuid(): string | undefined {
#_getDeviceIdFromUuid(): string | undefined {
const uuid = this.storage.get('uuid_id');
if (uuid === undefined) {
return undefined;
@ -210,7 +211,7 @@ export class User {
return Helpers.unencodeNumber(uuid)[1];
}
private _getDeviceIdFromNumber(): string | undefined {
#_getDeviceIdFromNumber(): string | undefined {
const numberId = this.storage.get('number_id');
if (numberId === undefined) {
return undefined;

View file

@ -124,28 +124,23 @@ export abstract class Updater {
protected readonly logger: LoggerType;
private readonly settingsChannel: SettingsChannel;
readonly #settingsChannel: SettingsChannel;
protected readonly getMainWindow: () => BrowserWindow | undefined;
private throttledSendDownloadingUpdate: ((
#throttledSendDownloadingUpdate: ((
downloadedSize: number,
downloadSize: number
) => void) & {
cancel: () => void;
};
private activeDownload: Promise<boolean> | undefined;
private markedCannotUpdate = false;
private restarting = false;
private readonly canRunSilently: () => boolean;
private autoRetryAttempts = 0;
private autoRetryAfter: number | undefined;
#activeDownload: Promise<boolean> | undefined;
#markedCannotUpdate = false;
#restarting = false;
readonly #canRunSilently: () => boolean;
#autoRetryAttempts = 0;
#autoRetryAfter: number | undefined;
constructor({
settingsChannel,
@ -153,12 +148,12 @@ export abstract class Updater {
getMainWindow,
canRunSilently,
}: UpdaterOptionsType) {
this.settingsChannel = settingsChannel;
this.#settingsChannel = settingsChannel;
this.logger = logger;
this.getMainWindow = getMainWindow;
this.canRunSilently = canRunSilently;
this.#canRunSilently = canRunSilently;
this.throttledSendDownloadingUpdate = throttle(
this.#throttledSendDownloadingUpdate = throttle(
(downloadedSize: number, downloadSize: number) => {
const mainWindow = this.getMainWindow();
mainWindow?.webContents.send(
@ -178,32 +173,32 @@ export abstract class Updater {
//
public async force(): Promise<void> {
this.markedCannotUpdate = false;
return this.checkForUpdatesMaybeInstall(CheckType.ForceDownload);
this.#markedCannotUpdate = false;
return this.#checkForUpdatesMaybeInstall(CheckType.ForceDownload);
}
// If the updater was about to restart the app but the user cancelled it, show dialog
// to let them retry the restart
public onRestartCancelled(): void {
if (!this.restarting) {
if (!this.#restarting) {
return;
}
this.logger.info(
'updater/onRestartCancelled: restart was cancelled. forcing update to reset updater state'
);
this.restarting = false;
this.#restarting = false;
markShouldNotQuit();
drop(this.checkForUpdatesMaybeInstall(CheckType.AllowSameVersion));
drop(this.#checkForUpdatesMaybeInstall(CheckType.AllowSameVersion));
}
public async start(): Promise<void> {
this.logger.info('updater/start: starting checks...');
this.schedulePoll();
this.#schedulePoll();
await this.deletePreviousInstallers();
await this.checkForUpdatesMaybeInstall(CheckType.Normal);
await this.#checkForUpdatesMaybeInstall(CheckType.Normal);
}
//
@ -232,14 +227,14 @@ export abstract class Updater {
error: Error,
dialogType = DialogType.Cannot_Update
): void {
if (this.markedCannotUpdate) {
if (this.#markedCannotUpdate) {
this.logger.warn(
'updater/markCannotUpdate: already marked',
Errors.toLogFormat(error)
);
return;
}
this.markedCannotUpdate = true;
this.#markedCannotUpdate = true;
this.logger.error(
'updater/markCannotUpdate: marking due to error: ' +
@ -253,13 +248,13 @@ export abstract class Updater {
this.setUpdateListener(async () => {
this.logger.info('updater/markCannotUpdate: retrying after user action');
this.markedCannotUpdate = false;
await this.checkForUpdatesMaybeInstall(CheckType.Normal);
this.#markedCannotUpdate = false;
await this.#checkForUpdatesMaybeInstall(CheckType.Normal);
});
}
protected markRestarting(): void {
this.restarting = true;
this.#restarting = true;
markShouldQuit();
}
@ -267,7 +262,7 @@ export abstract class Updater {
// Private methods
//
private schedulePoll(): void {
#schedulePoll(): void {
const now = Date.now();
const earliestPollTime = now - (now % POLL_INTERVAL) + POLL_INTERVAL;
@ -279,46 +274,46 @@ export abstract class Updater {
this.logger.info(`updater/schedulePoll: polling in ${timeoutMs}ms`);
setTimeout(() => {
drop(this.safePoll());
drop(this.#safePoll());
}, timeoutMs);
}
private async safePoll(): Promise<void> {
async #safePoll(): Promise<void> {
try {
if (this.autoRetryAfter != null && Date.now() < this.autoRetryAfter) {
if (this.#autoRetryAfter != null && Date.now() < this.#autoRetryAfter) {
this.logger.info(
`updater/safePoll: not polling until ${this.autoRetryAfter}`
`updater/safePoll: not polling until ${this.#autoRetryAfter}`
);
return;
}
this.logger.info('updater/safePoll: polling now');
await this.checkForUpdatesMaybeInstall(CheckType.Normal);
await this.#checkForUpdatesMaybeInstall(CheckType.Normal);
} catch (error) {
this.logger.error(`updater/safePoll: ${Errors.toLogFormat(error)}`);
} finally {
this.schedulePoll();
this.#schedulePoll();
}
}
private async downloadAndInstall(
async #downloadAndInstall(
updateInfo: UpdateInformationType,
mode: DownloadMode
): Promise<boolean> {
if (this.activeDownload) {
return this.activeDownload;
if (this.#activeDownload) {
return this.#activeDownload;
}
try {
this.activeDownload = this.doDownloadAndInstall(updateInfo, mode);
this.#activeDownload = this.#doDownloadAndInstall(updateInfo, mode);
return await this.activeDownload;
return await this.#activeDownload;
} finally {
this.activeDownload = undefined;
this.#activeDownload = undefined;
}
}
private async doDownloadAndInstall(
async #doDownloadAndInstall(
updateInfo: UpdateInformationType,
mode: DownloadMode
): Promise<boolean> {
@ -333,22 +328,22 @@ export abstract class Updater {
let downloadResult: DownloadUpdateResultType | undefined;
try {
downloadResult = await this.downloadUpdate(updateInfo, mode);
downloadResult = await this.#downloadUpdate(updateInfo, mode);
} catch (error) {
// Restore state in case of download error
this.version = oldVersion;
if (
mode === DownloadMode.Automatic &&
this.autoRetryAttempts < MAX_AUTO_RETRY_ATTEMPTS
this.#autoRetryAttempts < MAX_AUTO_RETRY_ATTEMPTS
) {
this.autoRetryAttempts += 1;
this.autoRetryAfter = Date.now() + AUTO_RETRY_DELAY;
this.#autoRetryAttempts += 1;
this.#autoRetryAfter = Date.now() + AUTO_RETRY_DELAY;
logger.warn(
'downloadAndInstall: transient error ' +
`${Errors.toLogFormat(error)}, ` +
`attempts=${this.autoRetryAttempts}, ` +
`retryAfter=${this.autoRetryAfter}`
`attempts=${this.#autoRetryAttempts}, ` +
`retryAfter=${this.#autoRetryAfter}`
);
return false;
}
@ -356,8 +351,8 @@ export abstract class Updater {
throw error;
}
this.autoRetryAttempts = 0;
this.autoRetryAfter = undefined;
this.#autoRetryAttempts = 0;
this.#autoRetryAfter = undefined;
if (!downloadResult) {
logger.warn('downloadAndInstall: no update was downloaded');
@ -391,7 +386,7 @@ export abstract class Updater {
const isSilent =
updateInfo.vendor?.requireUserConfirmation !== 'true' &&
this.canRunSilently();
this.#canRunSilently();
const handler = await this.installUpdate(updateFilePath, isSilent);
if (isSilent || mode === DownloadMode.ForceUpdate) {
@ -430,13 +425,11 @@ export abstract class Updater {
}
}
private async checkForUpdatesMaybeInstall(
checkType: CheckType
): Promise<void> {
async #checkForUpdatesMaybeInstall(checkType: CheckType): Promise<void> {
const { logger } = this;
logger.info('checkForUpdatesMaybeInstall: checking for update...');
const updateInfo = await this.checkForUpdates(checkType);
const updateInfo = await this.#checkForUpdates(checkType);
if (!updateInfo) {
return;
}
@ -444,7 +437,7 @@ export abstract class Updater {
const { version: newVersion } = updateInfo;
if (checkType === CheckType.ForceDownload) {
await this.downloadAndInstall(updateInfo, DownloadMode.ForceUpdate);
await this.#downloadAndInstall(updateInfo, DownloadMode.ForceUpdate);
return;
}
@ -462,9 +455,9 @@ export abstract class Updater {
throw missingCaseError(checkType);
}
const autoDownloadUpdates = await this.getAutoDownloadUpdateSetting();
const autoDownloadUpdates = await this.#getAutoDownloadUpdateSetting();
if (autoDownloadUpdates) {
await this.downloadAndInstall(updateInfo, DownloadMode.Automatic);
await this.#downloadAndInstall(updateInfo, DownloadMode.Automatic);
return;
}
@ -473,10 +466,10 @@ export abstract class Updater {
mode = DownloadMode.DifferentialOnly;
}
await this.offerUpdate(updateInfo, mode, 0);
await this.#offerUpdate(updateInfo, mode, 0);
}
private async offerUpdate(
async #offerUpdate(
updateInfo: UpdateInformationType,
mode: DownloadMode,
attempt: number
@ -486,13 +479,17 @@ export abstract class Updater {
this.setUpdateListener(async () => {
logger.info('offerUpdate: have not downloaded update, going to download');
const didDownload = await this.downloadAndInstall(updateInfo, mode);
const didDownload = await this.#downloadAndInstall(updateInfo, mode);
if (!didDownload && mode === DownloadMode.DifferentialOnly) {
this.logger.warn(
'offerUpdate: Failed to download differential update, offering full'
);
this.throttledSendDownloadingUpdate.cancel();
return this.offerUpdate(updateInfo, DownloadMode.FullOnly, attempt + 1);
this.#throttledSendDownloadingUpdate.cancel();
return this.#offerUpdate(
updateInfo,
DownloadMode.FullOnly,
attempt + 1
);
}
strictAssert(didDownload, 'FullOnly must always download update');
@ -527,7 +524,7 @@ export abstract class Updater {
);
}
private async checkForUpdates(
async #checkForUpdates(
checkType: CheckType
): Promise<UpdateInformationType | undefined> {
if (isAdhoc(packageJson.version)) {
@ -591,13 +588,13 @@ export abstract class Updater {
const fileName = getUpdateFileName(
parsedYaml,
process.platform,
await this.getArch()
await this.#getArch()
);
const sha512 = getSHA512(parsedYaml, fileName);
strictAssert(sha512 !== undefined, 'Missing required hash');
const latestInstaller = await this.getLatestCachedInstaller(
const latestInstaller = await this.#getLatestCachedInstaller(
extname(fileName)
);
@ -650,7 +647,7 @@ export abstract class Updater {
};
}
private async getLatestCachedInstaller(
async #getLatestCachedInstaller(
extension: string
): Promise<string | undefined> {
const cacheDir = await createUpdateCacheDirIfNeeded();
@ -661,7 +658,7 @@ export abstract class Updater {
return oldFiles.find(fileName => extname(fileName) === extension);
}
private async downloadUpdate(
async #downloadUpdate(
{ fileName, sha512, differentialData, size }: UpdateInformationType,
mode: DownloadMode
): Promise<DownloadUpdateResultType | undefined> {
@ -757,7 +754,7 @@ export abstract class Updater {
try {
await downloadDifferentialData(tempUpdatePath, differentialData, {
statusCallback: updateOnProgress
? this.throttledSendDownloadingUpdate
? this.#throttledSendDownloadingUpdate
: undefined,
logger: this.logger,
});
@ -783,7 +780,7 @@ export abstract class Updater {
await gracefulRmRecursive(this.logger, cacheDir);
cacheDir = await createUpdateCacheDirIfNeeded();
await this.downloadAndReport(
await this.#downloadAndReport(
updateFileUrl,
size,
tempUpdatePath,
@ -854,7 +851,7 @@ export abstract class Updater {
}
}
private async downloadAndReport(
async #downloadAndReport(
updateFileUrl: string,
downloadSize: number,
targetUpdatePath: string,
@ -869,7 +866,7 @@ export abstract class Updater {
downloadStream.on('data', data => {
downloadedSize += data.length;
this.throttledSendDownloadingUpdate(downloadedSize, downloadSize);
this.#throttledSendDownloadingUpdate(downloadedSize, downloadSize);
});
}
@ -888,9 +885,9 @@ export abstract class Updater {
});
}
private async getAutoDownloadUpdateSetting(): Promise<boolean> {
async #getAutoDownloadUpdateSetting(): Promise<boolean> {
try {
return await this.settingsChannel.getSettingFromMainWindow(
return await this.#settingsChannel.getSettingFromMainWindow(
'autoDownloadUpdate'
);
} catch (error) {
@ -902,7 +899,7 @@ export abstract class Updater {
}
}
private async getArch(): Promise<typeof process.arch> {
async #getArch(): Promise<typeof process.arch> {
if (process.arch === 'arm64') {
return process.arch;
}

View file

@ -23,7 +23,7 @@ export class MacOSUpdater extends Updater {
logger.info('downloadAndInstall: handing download to electron...');
try {
await this.handToAutoUpdate(updateFilePath);
await this.#handToAutoUpdate(updateFilePath);
} catch (error) {
const readOnly = 'Cannot update while running on a read-only volume';
const message: string = error.message || '';
@ -47,7 +47,7 @@ export class MacOSUpdater extends Updater {
};
}
private async handToAutoUpdate(filePath: string): Promise<void> {
async #handToAutoUpdate(filePath: string): Promise<void> {
const { logger } = this;
const { promise, resolve, reject } = explodePromise<void>();

View file

@ -17,7 +17,7 @@ const unlink = pify(unlinkCallback);
const IS_EXE = /\.exe$/i;
export class WindowsUpdater extends Updater {
private installing = false;
#installing = false;
// This is fixed by our new install mechanisms...
// https://github.com/signalapp/Signal-Desktop/issues/2369
@ -52,8 +52,8 @@ export class WindowsUpdater extends Updater {
return async () => {
logger.info('downloadAndInstall: installing...');
try {
await this.install(updateFilePath, isSilent);
this.installing = true;
await this.#install(updateFilePath, isSilent);
this.#installing = true;
} catch (error) {
this.markCannotUpdate(error);
@ -72,8 +72,8 @@ export class WindowsUpdater extends Updater {
app.quit();
}
private async install(filePath: string, isSilent: boolean): Promise<void> {
if (this.installing) {
async #install(filePath: string, isSilent: boolean): Promise<void> {
if (this.#installing) {
return;
}

View file

@ -9,7 +9,7 @@ export interface IController {
}
export class AbortableProcess<Result> implements IController {
private abortReject: (error: Error) => void;
#abortReject: (error: Error) => void;
public readonly resultPromise: Promise<Result>;
@ -21,13 +21,13 @@ export class AbortableProcess<Result> implements IController {
const { promise: abortPromise, reject: abortReject } =
explodePromise<Result>();
this.abortReject = abortReject;
this.#abortReject = abortReject;
this.resultPromise = Promise.race([abortPromise, resultPromise]);
}
public abort(): void {
this.controller.abort();
this.abortReject(new Error(`Process "${this.name}" was aborted`));
this.#abortReject(new Error(`Process "${this.name}" was aborted`));
}
public getResult(): Promise<Result> {

View file

@ -16,32 +16,30 @@ import { once, noop } from 'lodash';
* See the tests to see how this works.
*/
export class AsyncQueue<T> implements AsyncIterable<T> {
private onAdd: () => void = noop;
private queue: Array<T> = [];
private isReading = false;
#onAdd: () => void = noop;
#queue: Array<T> = [];
#isReading = false;
add(value: Readonly<T>): void {
this.queue.push(value);
this.onAdd();
this.#queue.push(value);
this.#onAdd();
}
async *[Symbol.asyncIterator](): AsyncIterator<T> {
if (this.isReading) {
if (this.#isReading) {
throw new Error('Cannot iterate over a queue more than once');
}
this.isReading = true;
this.#isReading = true;
while (true) {
yield* this.queue;
yield* this.#queue;
this.queue = [];
this.#queue = [];
// We want to iterate over the queue in series.
// eslint-disable-next-line no-await-in-loop
await new Promise<void>(resolve => {
this.onAdd = once(resolve);
this.#onAdd = once(resolve);
});
}
}

View file

@ -38,7 +38,7 @@ export type BackOffOptionsType = Readonly<{
const DEFAULT_RANDOM = () => Math.random();
export class BackOff {
private count = 0;
#count = 0;
constructor(
private timeouts: ReadonlyArray<number>,
@ -46,7 +46,7 @@ export class BackOff {
) {}
public get(): number {
let result = this.timeouts[this.count];
let result = this.timeouts[this.#count];
const { jitter = 0, random = DEFAULT_RANDOM } = this.options;
// Do not apply jitter larger than the timeout value. It is supposed to be
@ -60,7 +60,7 @@ export class BackOff {
public getAndIncrement(): number {
const result = this.get();
if (!this.isFull()) {
this.count += 1;
this.#count += 1;
}
return result;
@ -70,14 +70,14 @@ export class BackOff {
if (newTimeouts !== undefined) {
this.timeouts = newTimeouts;
}
this.count = 0;
this.#count = 0;
}
public isFull(): boolean {
return this.count === this.timeouts.length - 1;
return this.#count === this.timeouts.length - 1;
}
public getIndex(): number {
return this.count;
return this.#count;
}
}

View file

@ -11,10 +11,10 @@ enum State {
}
export class DelimitedStream extends Transform {
private state = State.Prefix;
private prefixValue = 0;
private prefixSize = 0;
private parts = new Array<Buffer>();
#state = State.Prefix;
#prefixValue = 0;
#prefixSize = 0;
#parts = new Array<Buffer>();
constructor() {
super({ readableObjectMode: true });
@ -27,7 +27,7 @@ export class DelimitedStream extends Transform {
): void {
let offset = 0;
while (offset < chunk.length) {
if (this.state === State.Prefix) {
if (this.#state === State.Prefix) {
const b = chunk[offset];
offset += 1;
@ -38,50 +38,50 @@ export class DelimitedStream extends Transform {
const value = b & 0x7f;
// eslint-disable-next-line no-bitwise
this.prefixValue |= value << (7 * this.prefixSize);
this.prefixSize += 1;
this.#prefixValue |= value << (7 * this.#prefixSize);
this.#prefixSize += 1;
// Check that we didn't go over 32bits. Node.js buffers can never
// be larger than 2gb anyway!
if (this.prefixSize > 4) {
if (this.#prefixSize > 4) {
done(new Error('Delimiter encoding overflow'));
return;
}
if (isLast) {
this.state = State.Data;
this.#state = State.Data;
}
} else if (this.state === State.Data) {
const toTake = Math.min(this.prefixValue, chunk.length - offset);
} else if (this.#state === State.Data) {
const toTake = Math.min(this.#prefixValue, chunk.length - offset);
const part = chunk.slice(offset, offset + toTake);
offset += toTake;
this.prefixValue -= toTake;
this.#prefixValue -= toTake;
this.parts.push(part);
this.#parts.push(part);
if (this.prefixValue <= 0) {
this.state = State.Prefix;
this.prefixSize = 0;
this.prefixValue = 0;
if (this.#prefixValue <= 0) {
this.#state = State.Prefix;
this.#prefixSize = 0;
this.#prefixValue = 0;
const whole = Buffer.concat(this.parts);
this.parts = [];
const whole = Buffer.concat(this.#parts);
this.#parts = [];
this.push(whole);
}
} else {
throw missingCaseError(this.state);
throw missingCaseError(this.#state);
}
}
done();
}
override _flush(done: (error?: Error) => void): void {
if (this.state !== State.Prefix) {
if (this.#state !== State.Prefix) {
done(new Error('Unfinished data'));
return;
}
if (this.prefixSize !== 0) {
if (this.#prefixSize !== 0) {
done(new Error('Unfinished prefix'));
return;
}

View file

@ -20,15 +20,13 @@
import { drop } from './drop';
export class LatestQueue {
private isRunning: boolean;
private queuedTask?: () => Promise<void>;
private onceEmptyCallbacks: Array<() => unknown>;
#isRunning: boolean;
#queuedTask?: () => Promise<void>;
#onceEmptyCallbacks: Array<() => unknown>;
constructor() {
this.isRunning = false;
this.onceEmptyCallbacks = [];
this.#isRunning = false;
this.#onceEmptyCallbacks = [];
}
/**
@ -39,25 +37,25 @@ export class LatestQueue {
* tasks will be enqueued at a time.
*/
add(task: () => Promise<void>): void {
if (this.isRunning) {
this.queuedTask = task;
if (this.#isRunning) {
this.#queuedTask = task;
} else {
this.isRunning = true;
this.#isRunning = true;
drop(
task().finally(() => {
this.isRunning = false;
this.#isRunning = false;
const { queuedTask } = this;
const queuedTask = this.#queuedTask;
if (queuedTask) {
this.queuedTask = undefined;
this.#queuedTask = undefined;
this.add(queuedTask);
} else {
try {
this.onceEmptyCallbacks.forEach(callback => {
this.#onceEmptyCallbacks.forEach(callback => {
callback();
});
} finally {
this.onceEmptyCallbacks = [];
this.#onceEmptyCallbacks = [];
}
}
})
@ -69,6 +67,6 @@ export class LatestQueue {
* Adds a callback to be called the first time the queue goes from "running" to "empty".
*/
onceEmpty(callback: () => unknown): void {
this.onceEmptyCallbacks.push(callback);
this.#onceEmptyCallbacks.push(callback);
}
}

View file

@ -26,26 +26,24 @@ export class Sound {
private static context: AudioContext | undefined;
private readonly loop: boolean;
private node?: AudioBufferSourceNode;
private readonly soundType: SoundType;
readonly #loop: boolean;
#node?: AudioBufferSourceNode;
readonly #soundType: SoundType;
constructor(options: SoundOpts) {
this.loop = Boolean(options.loop);
this.soundType = options.soundType;
this.#loop = Boolean(options.loop);
this.#soundType = options.soundType;
}
async play(): Promise<void> {
let soundBuffer = Sound.sounds.get(this.soundType);
let soundBuffer = Sound.sounds.get(this.#soundType);
if (!soundBuffer) {
try {
const src = Sound.getSrc(this.soundType);
const src = Sound.getSrc(this.#soundType);
const buffer = await Sound.loadSoundFile(src);
const decodedBuffer = await this.context.decodeAudioData(buffer);
Sound.sounds.set(this.soundType, decodedBuffer);
const decodedBuffer = await this.#context.decodeAudioData(buffer);
Sound.sounds.set(this.#soundType, decodedBuffer);
soundBuffer = decodedBuffer;
} catch (err) {
log.error(`Sound error: ${err}`);
@ -53,28 +51,28 @@ export class Sound {
}
}
const soundNode = this.context.createBufferSource();
const soundNode = this.#context.createBufferSource();
soundNode.buffer = soundBuffer;
const volumeNode = this.context.createGain();
const volumeNode = this.#context.createGain();
soundNode.connect(volumeNode);
volumeNode.connect(this.context.destination);
volumeNode.connect(this.#context.destination);
soundNode.loop = this.loop;
soundNode.loop = this.#loop;
soundNode.start(0, 0);
this.node = soundNode;
this.#node = soundNode;
}
stop(): void {
if (this.node) {
this.node.stop(0);
this.node = undefined;
if (this.#node) {
this.#node.stop(0);
this.#node = undefined;
}
}
private get context(): AudioContext {
get #context(): AudioContext {
if (!Sound.context) {
Sound.context = new AudioContext();
}

View file

@ -13,30 +13,31 @@ type EntryType = Readonly<{
let startupProcessingQueue: StartupQueue | undefined;
export class StartupQueue {
private readonly map = new Map<string, EntryType>();
private readonly running: PQueue = new PQueue({
readonly #map = new Map<string, EntryType>();
readonly #running: PQueue = new PQueue({
// mostly io-bound work that is not very parallelizable
// small number should be sufficient
concurrency: 5,
});
public add(id: string, value: number, f: () => Promise<void>): void {
const existing = this.map.get(id);
const existing = this.#map.get(id);
if (existing && existing.value >= value) {
return;
}
this.map.set(id, { value, callback: f });
this.#map.set(id, { value, callback: f });
}
public flush(): void {
log.info('StartupQueue: Processing', this.map.size, 'actions');
log.info('StartupQueue: Processing', this.#map.size, 'actions');
const values = Array.from(this.map.values());
this.map.clear();
const values = Array.from(this.#map.values());
this.#map.clear();
for (const { callback } of values) {
void this.running.add(async () => {
void this.#running.add(async () => {
try {
return callback();
} catch (error) {
@ -50,11 +51,11 @@ export class StartupQueue {
}
}
private shutdown(): Promise<void> {
#shutdown(): Promise<void> {
log.info(
`StartupQueue: Waiting for ${this.running.pending} tasks to drain`
`StartupQueue: Waiting for ${this.#running.pending} tasks to drain`
);
return this.running.onIdle();
return this.#running.onIdle();
}
static initialize(): void {
@ -75,6 +76,8 @@ export class StartupQueue {
}
static async shutdown(): Promise<void> {
await startupProcessingQueue?.shutdown();
if (startupProcessingQueue != null) {
await startupProcessingQueue.#shutdown();
}
}
}

View file

@ -12,7 +12,7 @@ const ringtoneEventQueue = new PQueue({
});
class CallingTones {
private ringtone?: Sound;
#ringtone?: Sound;
async handRaised() {
const canPlayTone = window.Events.getCallRingtoneNotification();
@ -41,9 +41,9 @@ class CallingTones {
async playRingtone() {
await ringtoneEventQueue.add(async () => {
if (this.ringtone) {
this.ringtone.stop();
this.ringtone = undefined;
if (this.#ringtone) {
this.#ringtone.stop();
this.#ringtone = undefined;
}
const canPlayTone = window.Events.getCallRingtoneNotification();
@ -51,20 +51,20 @@ class CallingTones {
return;
}
this.ringtone = new Sound({
this.#ringtone = new Sound({
loop: true,
soundType: SoundType.Ringtone,
});
await this.ringtone.play();
await this.#ringtone.play();
});
}
async stopRingtone() {
await ringtoneEventQueue.add(async () => {
if (this.ringtone) {
this.ringtone.stop();
this.ringtone = undefined;
if (this.#ringtone) {
this.#ringtone.stop();
this.#ringtone = undefined;
}
});
}

View file

@ -87,7 +87,7 @@ export type DesktopCapturerBaton = Readonly<{
}>;
export class DesktopCapturer {
private state: State;
#state: State;
private static getDisplayMediaPromise: Promise<MediaStream> | undefined;
@ -102,47 +102,47 @@ export class DesktopCapturer {
}
if (macScreenShare.isSupported) {
this.state = {
this.#state = {
step: Step.NativeMacOS,
stream: this.getNativeMacOSStream(),
stream: this.#getNativeMacOSStream(),
};
} else {
this.state = { step: Step.RequestingMedia, promise: this.getStream() };
this.#state = { step: Step.RequestingMedia, promise: this.#getStream() };
}
}
public abort(): void {
if (this.state.step === Step.NativeMacOS) {
this.state.stream.stop();
if (this.#state.step === Step.NativeMacOS) {
this.#state.stream.stop();
}
if (this.state.step === Step.SelectingSource) {
this.state.onSource(undefined);
if (this.#state.step === Step.SelectingSource) {
this.#state.onSource(undefined);
}
this.state = { step: Step.Error };
this.#state = { step: Step.Error };
}
public selectSource(id: string): void {
strictAssert(
this.state.step === Step.SelectingSource,
`Invalid state in "selectSource" ${this.state.step}`
this.#state.step === Step.SelectingSource,
`Invalid state in "selectSource" ${this.#state.step}`
);
const { promise, sources, onSource } = this.state;
const { promise, sources, onSource } = this.#state;
const source = id == null ? undefined : sources.find(s => s.id === id);
this.state = { step: Step.SelectedSource, promise };
this.#state = { step: Step.SelectedSource, promise };
onSource(source);
}
/** @internal */
private onSources(
#onSources(
sources: ReadonlyArray<DesktopCapturerSource>
): Promise<DesktopCapturerSource | undefined> {
strictAssert(
this.state.step === Step.RequestingMedia,
`Invalid state in "onSources" ${this.state.step}`
this.#state.step === Step.RequestingMedia,
`Invalid state in "onSources" ${this.#state.step}`
);
const presentableSources = sources
@ -158,25 +158,25 @@ export class DesktopCapturer {
? source.appIcon.toDataURL()
: undefined,
id: source.id,
name: this.translateSourceName(source),
name: this.#translateSourceName(source),
isScreen: isScreenSource(source),
thumbnail: source.thumbnail.toDataURL(),
};
})
.filter(isNotNil);
const { promise } = this.state;
const { promise } = this.#state;
const { promise: source, resolve: onSource } = explodePromise<
DesktopCapturerSource | undefined
>();
this.state = { step: Step.SelectingSource, promise, sources, onSource };
this.#state = { step: Step.SelectingSource, promise, sources, onSource };
this.options.onPresentableSources(presentableSources);
return source;
}
private async getStream(): Promise<void> {
async #getStream(): Promise<void> {
liveCapturers.add(this);
try {
// Only allow one global getDisplayMedia() request at a time
@ -210,28 +210,28 @@ export class DesktopCapturer {
});
strictAssert(
this.state.step === Step.RequestingMedia ||
this.state.step === Step.SelectedSource,
`Invalid state in "getStream.success" ${this.state.step}`
this.#state.step === Step.RequestingMedia ||
this.#state.step === Step.SelectedSource,
`Invalid state in "getStream.success" ${this.#state.step}`
);
this.options.onMediaStream(stream);
this.state = { step: Step.Done };
this.#state = { step: Step.Done };
} catch (error) {
strictAssert(
this.state.step === Step.RequestingMedia ||
this.state.step === Step.SelectedSource,
`Invalid state in "getStream.error" ${this.state.step}`
this.#state.step === Step.RequestingMedia ||
this.#state.step === Step.SelectedSource,
`Invalid state in "getStream.error" ${this.#state.step}`
);
this.options.onError(error);
this.state = { step: Step.Error };
this.#state = { step: Step.Error };
} finally {
liveCapturers.delete(this);
DesktopCapturer.getDisplayMediaPromise = undefined;
}
}
private getNativeMacOSStream(): macScreenShare.Stream {
#getNativeMacOSStream(): macScreenShare.Stream {
const track = new MediaStreamTrackGenerator({ kind: 'video' });
const writer = track.writable.getWriter();
@ -311,7 +311,7 @@ export class DesktopCapturer {
return stream;
}
private translateSourceName(source: DesktopCapturerSource): string {
#translateSourceName(source: DesktopCapturerSource): string {
const { i18n } = this.options;
const { name } = source;
@ -345,7 +345,7 @@ export class DesktopCapturer {
strictAssert(!done, 'No capturer available for incoming sources');
liveCapturers.delete(capturer);
selected = await capturer.onSources(sources);
selected = await capturer.#onSources(sources);
} catch (error) {
log.error(
'desktopCapturer: failed to get the source',

View file

@ -283,17 +283,17 @@ class RepeatIterable<T> implements Iterable<T> {
}
class RepeatIterator<T> implements Iterator<T> {
private readonly iteratorResult: IteratorResult<T>;
readonly #iteratorResult: IteratorResult<T>;
constructor(value: Readonly<T>) {
this.iteratorResult = {
this.#iteratorResult = {
done: false,
value,
};
}
next(): IteratorResult<T> {
return this.iteratorResult;
return this.#iteratorResult;
}
}

View file

@ -2483,7 +2483,7 @@
{
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " private metadataRef: React.RefObject<HTMLDivElement> = React.createRef();",
"line": " #metadataRef: React.RefObject<HTMLDivElement> = React.createRef();",
"reasonCategory": "usageTrusted",
"updated": "2023-06-30T22:12:49.259Z",
"reasonDetail": "Used for excluding the message metadata from triple-click selections."

View file

@ -39,13 +39,10 @@ export function getDeltaIntoPast(delta?: number): number {
}
export class RetryPlaceholders {
private items: Array<RetryItemType>;
private byConversation: ByConversationLookupType;
private byMessage: ByMessageLookupType;
private retryReceiptLifespan: number;
#items: Array<RetryItemType>;
#byConversation: ByConversationLookupType;
#byMessage: ByMessageLookupType;
#retryReceiptLifespan: number;
constructor(options: { retryReceiptLifespan?: number } = {}) {
if (!window.storage) {
@ -66,41 +63,41 @@ export class RetryPlaceholders {
);
}
this.items = parsed.success ? parsed.data : [];
this.#items = parsed.success ? parsed.data : [];
this.sortByExpiresAtAsc();
this.byConversation = this.makeByConversationLookup();
this.byMessage = this.makeByMessageLookup();
this.retryReceiptLifespan = options.retryReceiptLifespan || HOUR;
this.#byConversation = this.makeByConversationLookup();
this.#byMessage = this.makeByMessageLookup();
this.#retryReceiptLifespan = options.retryReceiptLifespan || HOUR;
log.info(
`RetryPlaceholders.constructor: Started with ${this.items.length} items, lifespan of ${this.retryReceiptLifespan}`
`RetryPlaceholders.constructor: Started with ${this.#items.length} items, lifespan of ${this.#retryReceiptLifespan}`
);
}
// Arranging local data for efficiency
sortByExpiresAtAsc(): void {
this.items.sort(
this.#items.sort(
(left: RetryItemType, right: RetryItemType) =>
left.receivedAt - right.receivedAt
);
}
makeByConversationLookup(): ByConversationLookupType {
return groupBy(this.items, item => item.conversationId);
return groupBy(this.#items, item => item.conversationId);
}
makeByMessageLookup(): ByMessageLookupType {
const lookup = new Map<string, RetryItemType>();
this.items.forEach(item => {
this.#items.forEach(item => {
lookup.set(getItemId(item.conversationId, item.sentAt), item);
});
return lookup;
}
makeLookups(): void {
this.byConversation = this.makeByConversationLookup();
this.byMessage = this.makeByMessageLookup();
this.#byConversation = this.makeByConversationLookup();
this.#byMessage = this.makeByMessageLookup();
}
// Basic data management
@ -115,33 +112,33 @@ export class RetryPlaceholders {
);
}
this.items.push(item);
this.#items.push(item);
this.sortByExpiresAtAsc();
this.makeLookups();
await this.save();
}
async save(): Promise<void> {
await window.storage.put(STORAGE_KEY, this.items);
await window.storage.put(STORAGE_KEY, this.#items);
}
// Finding items in different ways
getCount(): number {
return this.items.length;
return this.#items.length;
}
getNextToExpire(): RetryItemType | undefined {
return this.items[0];
return this.#items[0];
}
async getExpiredAndRemove(): Promise<Array<RetryItemType>> {
const expiration = getDeltaIntoPast(this.retryReceiptLifespan);
const max = this.items.length;
const expiration = getDeltaIntoPast(this.#retryReceiptLifespan);
const max = this.#items.length;
const result: Array<RetryItemType> = [];
for (let i = 0; i < max; i += 1) {
const item = this.items[i];
const item = this.#items[i];
if (item.receivedAt <= expiration) {
result.push(item);
} else {
@ -153,7 +150,7 @@ export class RetryPlaceholders {
`RetryPlaceholders.getExpiredAndRemove: Found ${result.length} expired items`
);
this.items.splice(0, result.length);
this.#items.splice(0, result.length);
this.makeLookups();
await this.save();
@ -162,7 +159,7 @@ export class RetryPlaceholders {
async findByConversationAndMarkOpened(conversationId: string): Promise<void> {
let changed = 0;
const items = this.byConversation[conversationId];
const items = this.#byConversation[conversationId];
(items || []).forEach(item => {
if (!item.wasOpened) {
changed += 1;
@ -184,14 +181,14 @@ export class RetryPlaceholders {
conversationId: string,
sentAt: number
): Promise<RetryItemType | undefined> {
const result = this.byMessage.get(getItemId(conversationId, sentAt));
const result = this.#byMessage.get(getItemId(conversationId, sentAt));
if (!result) {
return undefined;
}
const index = this.items.findIndex(item => item === result);
const index = this.#items.findIndex(item => item === result);
this.items.splice(index, 1);
this.#items.splice(index, 1);
this.makeLookups();
log.info(

View file

@ -9,8 +9,8 @@ import * as Errors from '../types/errors';
* but also a way to force sleeping tasks to immediately resolve/reject on shutdown
*/
export class Sleeper {
private shuttingDown = false;
private shutdownCallbacks: Set<() => void> = new Set();
#shuttingDown = false;
#shutdownCallbacks: Set<() => void> = new Set();
/**
* delay by ms, careful when using on a loop if resolving on shutdown (default)
@ -46,7 +46,7 @@ export class Sleeper {
}
};
if (this.shuttingDown) {
if (this.#shuttingDown) {
log.info(
`Sleeper: sleep called when shutdown is in progress, scheduling immediate ${
resolveOnShutdown ? 'resolution' : 'rejection'
@ -58,30 +58,30 @@ export class Sleeper {
timeout = setTimeout(() => {
resolve();
this.removeShutdownCallback(shutdownCallback);
this.#removeShutdownCallback(shutdownCallback);
}, ms);
this.addShutdownCallback(shutdownCallback);
this.#addShutdownCallback(shutdownCallback);
});
}
private addShutdownCallback(callback: () => void) {
this.shutdownCallbacks.add(callback);
#addShutdownCallback(callback: () => void) {
this.#shutdownCallbacks.add(callback);
}
private removeShutdownCallback(callback: () => void) {
this.shutdownCallbacks.delete(callback);
#removeShutdownCallback(callback: () => void) {
this.#shutdownCallbacks.delete(callback);
}
shutdown(): void {
if (this.shuttingDown) {
if (this.#shuttingDown) {
return;
}
log.info(
`Sleeper: shutting down, settling ${this.shutdownCallbacks.size} in-progress sleep calls`
`Sleeper: shutting down, settling ${this.#shutdownCallbacks.size} in-progress sleep calls`
);
this.shuttingDown = true;
this.shutdownCallbacks.forEach(cb => {
this.#shuttingDown = true;
this.#shutdownCallbacks.forEach(cb => {
try {
cb();
} catch (error) {