Migrate to private class properties/methods
This commit is contained in:
parent
7dbe57084b
commit
aa9f53df57
100 changed files with 3795 additions and 3944 deletions
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
141
ts/challenge.ts
141
ts/challenge.ts
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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}:`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
217
ts/sql/main.ts
217
ts/sql/main.ts
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue