conversationJobQueue: Only show captcha for bubble messages
This commit is contained in:
parent
e69e8f3c9d
commit
2da49456c6
14 changed files with 721 additions and 126 deletions
|
@ -22,6 +22,7 @@ import * as Errors from './types/errors';
|
||||||
import { HTTPError } from './textsecure/Errors';
|
import { HTTPError } from './textsecure/Errors';
|
||||||
import type { SendMessageChallengeData } from './textsecure/Errors';
|
import type { SendMessageChallengeData } from './textsecure/Errors';
|
||||||
import * as log from './logging/log';
|
import * as log from './logging/log';
|
||||||
|
import { drop } from './util/drop';
|
||||||
|
|
||||||
export type ChallengeResponse = Readonly<{
|
export type ChallengeResponse = Readonly<{
|
||||||
captcha: string;
|
captcha: string;
|
||||||
|
@ -75,6 +76,7 @@ export type RegisteredChallengeType = Readonly<{
|
||||||
reason: string;
|
reason: string;
|
||||||
retryAt?: number;
|
retryAt?: number;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
silent: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type SolveOptionsType = Readonly<{
|
type SolveOptionsType = Readonly<{
|
||||||
|
@ -210,7 +212,7 @@ export class ChallengeHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (challenge.token) {
|
if (challenge.token) {
|
||||||
void this.solve({ reason, token: challenge.token });
|
drop(this.solve({ reason, token: challenge.token }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,7 +249,7 @@ export class ChallengeHandler {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.startTimers.delete(conversationId);
|
this.startTimers.delete(conversationId);
|
||||||
|
|
||||||
void this.startQueue(conversationId);
|
drop(this.startQueue(conversationId));
|
||||||
}, waitTime)
|
}, waitTime)
|
||||||
);
|
);
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -269,7 +271,9 @@ export class ChallengeHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.solve({ token: challenge.token, reason });
|
if (!challenge.silent) {
|
||||||
|
drop(this.solve({ token: challenge.token, reason }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onResponse(response: IPCResponse): void {
|
public onResponse(response: IPCResponse): void {
|
||||||
|
@ -282,8 +286,13 @@ export class ChallengeHandler {
|
||||||
handler.resolve(response.data);
|
handler.resolve(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unregister(conversationId: string): Promise<void> {
|
public async unregister(
|
||||||
log.info(`challenge: unregistered conversation ${conversationId}`);
|
conversationId: string,
|
||||||
|
source: string
|
||||||
|
): Promise<void> {
|
||||||
|
log.info(
|
||||||
|
`challenge: unregistered conversation ${conversationId} via ${source}`
|
||||||
|
);
|
||||||
this.registeredConversations.delete(conversationId);
|
this.registeredConversations.delete(conversationId);
|
||||||
this.pendingStarts.delete(conversationId);
|
this.pendingStarts.delete(conversationId);
|
||||||
|
|
||||||
|
@ -343,7 +352,7 @@ export class ChallengeHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.unregister(conversationId);
|
await this.unregister(conversationId, 'startQueue');
|
||||||
|
|
||||||
if (this.registeredConversations.size === 0) {
|
if (this.registeredConversations.size === 0) {
|
||||||
this.options.setChallengeStatus('idle');
|
this.options.setChallengeStatus('idle');
|
||||||
|
|
|
@ -13,6 +13,7 @@ import * as log from '../logging/log';
|
||||||
import { JobLogger } from './JobLogger';
|
import { JobLogger } from './JobLogger';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import type { LoggerType } from '../types/Logging';
|
import type { LoggerType } from '../types/Logging';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
|
||||||
const noopOnCompleteCallbacks = {
|
const noopOnCompleteCallbacks = {
|
||||||
resolve: noop,
|
resolve: noop,
|
||||||
|
@ -43,6 +44,12 @@ type JobQueueOptions = {
|
||||||
logger?: LoggerType;
|
logger?: LoggerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum JOB_STATUS {
|
||||||
|
SUCCESS = 'SUCCESS',
|
||||||
|
NEEDS_RETRY = 'NEEDS_RETRY',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class JobQueue<T> {
|
export abstract class JobQueue<T> {
|
||||||
private readonly maxAttempts: number;
|
private readonly maxAttempts: number;
|
||||||
|
|
||||||
|
@ -119,7 +126,7 @@ export abstract class JobQueue<T> {
|
||||||
protected abstract run(
|
protected abstract run(
|
||||||
job: Readonly<ParsedJob<T>>,
|
job: Readonly<ParsedJob<T>>,
|
||||||
extra?: Readonly<{ attempt?: number; log?: LoggerType }>
|
extra?: Readonly<{ attempt?: number; log?: LoggerType }>
|
||||||
): Promise<void>;
|
): Promise<JOB_STATUS.NEEDS_RETRY | undefined>;
|
||||||
|
|
||||||
protected getQueues(): ReadonlySet<PQueue> {
|
protected getQueues(): ReadonlySet<PQueue> {
|
||||||
return new Set([this.defaultInMemoryQueue]);
|
return new Set([this.defaultInMemoryQueue]);
|
||||||
|
@ -144,7 +151,7 @@ export abstract class JobQueue<T> {
|
||||||
log.info(`${this.logPrefix} is shutting down. Can't accept more work.`);
|
log.info(`${this.logPrefix} is shutting down. Can't accept more work.`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
void this.enqueueStoredJob(storedJob);
|
drop(this.enqueueStoredJob(storedJob));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +208,9 @@ export abstract class JobQueue<T> {
|
||||||
return this.defaultInMemoryQueue;
|
return this.defaultInMemoryQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async enqueueStoredJob(storedJob: Readonly<StoredJob>) {
|
protected async enqueueStoredJob(
|
||||||
|
storedJob: Readonly<StoredJob>
|
||||||
|
): Promise<void> {
|
||||||
assertDev(
|
assertDev(
|
||||||
storedJob.queueType === this.queueType,
|
storedJob.queueType === this.queueType,
|
||||||
'Received a mis-matched queue type'
|
'Received a mis-matched queue type'
|
||||||
|
@ -242,8 +251,10 @@ export abstract class JobQueue<T> {
|
||||||
|
|
||||||
const result:
|
const result:
|
||||||
| undefined
|
| undefined
|
||||||
| { success: true }
|
| { status: JOB_STATUS.SUCCESS }
|
||||||
| { success: false; err: unknown } = await queue.add(async () => {
|
| { status: JOB_STATUS.NEEDS_RETRY }
|
||||||
|
| { status: JOB_STATUS.ERROR; err: unknown } = await queue.add(
|
||||||
|
async () => {
|
||||||
for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
|
for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
|
||||||
const isFinalAttempt = attempt === this.maxAttempts;
|
const isFinalAttempt = attempt === this.maxAttempts;
|
||||||
|
|
||||||
|
@ -257,18 +268,30 @@ export abstract class JobQueue<T> {
|
||||||
log.warn(
|
log.warn(
|
||||||
`${this.logPrefix} returning early for job ${storedJob.id}; shutting down`
|
`${this.logPrefix} returning early for job ${storedJob.id}; shutting down`
|
||||||
);
|
);
|
||||||
return { success: false, err: new Error('Shutting down') };
|
return {
|
||||||
|
status: JOB_STATUS.ERROR,
|
||||||
|
err: new Error('Shutting down'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// We want an `await` in the loop, as we don't want a single job running more
|
// We want an `await` in the loop, as we don't want a single job running more
|
||||||
// than once at a time. Ideally, the job will succeed on the first attempt.
|
// than once at a time. Ideally, the job will succeed on the first attempt.
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await this.run(parsedJob, { attempt, log: logger });
|
const jobStatus = await this.run(parsedJob, {
|
||||||
|
attempt,
|
||||||
|
log: logger,
|
||||||
|
});
|
||||||
|
if (!jobStatus) {
|
||||||
log.info(
|
log.info(
|
||||||
`${this.logPrefix} job ${storedJob.id} succeeded on attempt ${attempt}`
|
`${this.logPrefix} job ${storedJob.id} succeeded on attempt ${attempt}`
|
||||||
);
|
);
|
||||||
return { success: true };
|
return { status: JOB_STATUS.SUCCESS };
|
||||||
|
}
|
||||||
|
log.info(
|
||||||
|
`${this.logPrefix} job ${storedJob.id} returned status ${jobStatus} on attempt ${attempt}`
|
||||||
|
);
|
||||||
|
return { status: jobStatus };
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
log.error(
|
log.error(
|
||||||
`${this.logPrefix} job ${
|
`${this.logPrefix} job ${
|
||||||
|
@ -276,16 +299,30 @@ export abstract class JobQueue<T> {
|
||||||
} failed on attempt ${attempt}. ${Errors.toLogFormat(err)}`
|
} failed on attempt ${attempt}. ${Errors.toLogFormat(err)}`
|
||||||
);
|
);
|
||||||
if (isFinalAttempt) {
|
if (isFinalAttempt) {
|
||||||
return { success: false, err };
|
return { status: JOB_STATUS.ERROR, err };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should never happen. See the assertion below.
|
// This should never happen. See the assertion below.
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (result?.success || !this.isShuttingDown) {
|
if (result?.status === JOB_STATUS.NEEDS_RETRY) {
|
||||||
|
const addJobSuccess = await this.retryJobOnQueueIdle({
|
||||||
|
storedJob,
|
||||||
|
job: parsedJob,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
if (!addJobSuccess) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,13 +330,26 @@ export abstract class JobQueue<T> {
|
||||||
result,
|
result,
|
||||||
'The job never ran. This indicates a developer error in the job queue'
|
'The job never ran. This indicates a developer error in the job queue'
|
||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.status === JOB_STATUS.ERROR) {
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(result.err);
|
reject(result.err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async retryJobOnQueueIdle({
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
job: Readonly<ParsedJob<T>>;
|
||||||
|
storedJob: Readonly<StoredJob>;
|
||||||
|
logger: LoggerType;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
logger.error(
|
||||||
|
`retryJobOnQueueIdle: not implemented for queue ${this.queueType}; dropping job`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
const queues = this.getQueues();
|
const queues = this.getQueues();
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
@ -9,7 +9,7 @@ import * as durations from '../util/durations';
|
||||||
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
|
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
|
||||||
import { InMemoryQueues } from './helpers/InMemoryQueues';
|
import { InMemoryQueues } from './helpers/InMemoryQueues';
|
||||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||||
import { JobQueue } from './JobQueue';
|
import { JOB_STATUS, JobQueue } from './JobQueue';
|
||||||
|
|
||||||
import { sendNormalMessage } from './helpers/sendNormalMessage';
|
import { sendNormalMessage } from './helpers/sendNormalMessage';
|
||||||
import { sendDirectExpirationTimerUpdate } from './helpers/sendDirectExpirationTimerUpdate';
|
import { sendDirectExpirationTimerUpdate } from './helpers/sendDirectExpirationTimerUpdate';
|
||||||
|
@ -33,7 +33,7 @@ import { strictAssert } from '../util/assert';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { explodePromise } from '../util/explodePromise';
|
import { explodePromise } from '../util/explodePromise';
|
||||||
import type { Job } from './Job';
|
import type { Job } from './Job';
|
||||||
import type { ParsedJob } from './types';
|
import type { ParsedJob, StoredJob } from './types';
|
||||||
import type SendMessage from '../textsecure/SendMessage';
|
import type SendMessage from '../textsecure/SendMessage';
|
||||||
import type { ServiceIdString } from '../types/ServiceId';
|
import type { ServiceIdString } from '../types/ServiceId';
|
||||||
import { commonShouldJobContinue } from './helpers/commonShouldJobContinue';
|
import { commonShouldJobContinue } from './helpers/commonShouldJobContinue';
|
||||||
|
@ -44,6 +44,9 @@ import { sendResendRequest } from './helpers/sendResendRequest';
|
||||||
import { sendNullMessage } from './helpers/sendNullMessage';
|
import { sendNullMessage } from './helpers/sendNullMessage';
|
||||||
import { sendSenderKeyDistribution } from './helpers/sendSenderKeyDistribution';
|
import { sendSenderKeyDistribution } from './helpers/sendSenderKeyDistribution';
|
||||||
import { sendSavedProto } from './helpers/sendSavedProto';
|
import { sendSavedProto } from './helpers/sendSavedProto';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
import { isInPast } from '../util/timestamp';
|
||||||
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
|
|
||||||
// Note: generally, we only want to add to this list. If you do need to change one of
|
// Note: generally, we only want to add to this list. If you do need to change one of
|
||||||
// these values, you'll likely need to write a database migration.
|
// these values, you'll likely need to write a database migration.
|
||||||
|
@ -62,6 +65,7 @@ export const conversationQueueJobEnum = z.enum([
|
||||||
'Story',
|
'Story',
|
||||||
'Receipts',
|
'Receipts',
|
||||||
]);
|
]);
|
||||||
|
type ConversationQueueJobEnum = z.infer<typeof conversationQueueJobEnum>;
|
||||||
|
|
||||||
const deleteForEveryoneJobDataSchema = z.object({
|
const deleteForEveryoneJobDataSchema = z.object({
|
||||||
type: z.literal(conversationQueueJobEnum.enum.DeleteForEveryone),
|
type: z.literal(conversationQueueJobEnum.enum.DeleteForEveryone),
|
||||||
|
@ -234,7 +238,92 @@ export type ConversationQueueJobBundle = {
|
||||||
const MAX_RETRY_TIME = durations.DAY;
|
const MAX_RETRY_TIME = durations.DAY;
|
||||||
const MAX_ATTEMPTS = exponentialBackoffMaxAttempts(MAX_RETRY_TIME);
|
const MAX_ATTEMPTS = exponentialBackoffMaxAttempts(MAX_RETRY_TIME);
|
||||||
|
|
||||||
|
function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean {
|
||||||
|
if (type === 'DeleteForEveryone') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type === 'DeleteStoryForEveryone') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type === 'DirectExpirationTimerUpdate') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type === 'GroupUpdate') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type === 'NormalMessage') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type === 'NullMessage') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type === 'ProfileKey') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type === 'Reaction') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type === 'ResendRequest') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type === 'SavedProto') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type === 'SenderKeyDistribution') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type === 'Story') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type === 'Receipts') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw missingCaseError(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RETRY_STATUS {
|
||||||
|
BLOCKED = 'BLOCKED',
|
||||||
|
BLOCKED_WITH_JOBS = 'BLOCKED_WITH_JOBS',
|
||||||
|
UNBLOCKED = 'UNBLOCKED',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConversationData = Readonly<
|
||||||
|
| {
|
||||||
|
// When we get a retryAt from a 428 error, we immediately record it, but we don't
|
||||||
|
// yet have a job to retry. We should, very soon, when the job returns
|
||||||
|
// JOB_STATUS.NEEDS_RETRY. This should be a very short-lived state.
|
||||||
|
status: RETRY_STATUS.BLOCKED;
|
||||||
|
callback: undefined;
|
||||||
|
jobsNeedingRetry: undefined;
|
||||||
|
retryAt: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// This is the next stage, when we've added at least one job needing retry, and we
|
||||||
|
// have a callback registered to run on queue idle (or be called directly).
|
||||||
|
status: RETRY_STATUS.BLOCKED_WITH_JOBS;
|
||||||
|
callback: () => void;
|
||||||
|
jobsNeedingRetry: Array<Readonly<StoredJob>>;
|
||||||
|
retryAt: number;
|
||||||
|
retryAtTimeout?: NodeJS.Timeout;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// When we discover that we can now run these deferred jobs, we flip into this
|
||||||
|
// state, which should be short-lived. We very quickly re-enqueue all
|
||||||
|
// jobsNeedingRetry, and erase perConversationData for this conversation.
|
||||||
|
status: RETRY_STATUS.UNBLOCKED;
|
||||||
|
callback: () => void;
|
||||||
|
jobsNeedingRetry: Array<Readonly<StoredJob>>;
|
||||||
|
retryAt: undefined;
|
||||||
|
retryAtTimeout?: NodeJS.Timeout;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
|
private readonly perConversationData = new Map<
|
||||||
|
string,
|
||||||
|
ConversationData | undefined
|
||||||
|
>();
|
||||||
private readonly inMemoryQueues = new InMemoryQueues();
|
private readonly inMemoryQueues = new InMemoryQueues();
|
||||||
private readonly verificationWaitMap = new Map<
|
private readonly verificationWaitMap = new Map<
|
||||||
string,
|
string,
|
||||||
|
@ -244,6 +333,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
promise: Promise<unknown>;
|
promise: Promise<unknown>;
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
private callbackCount = 0;
|
||||||
|
|
||||||
override getQueues(): ReadonlySet<PQueue> {
|
override getQueues(): ReadonlySet<PQueue> {
|
||||||
return this.inMemoryQueues.allQueues;
|
return this.inMemoryQueues.allQueues;
|
||||||
|
@ -254,6 +344,8 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
insert?: (job: ParsedJob<ConversationQueueJobData>) => Promise<void>
|
insert?: (job: ParsedJob<ConversationQueueJobData>) => Promise<void>
|
||||||
): Promise<Job<ConversationQueueJobData>> {
|
): Promise<Job<ConversationQueueJobData>> {
|
||||||
const { conversationId, type } = data;
|
const { conversationId, type } = data;
|
||||||
|
|
||||||
|
if (shouldSendShowCaptcha(data.type)) {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
window.Signal.challengeHandler,
|
window.Signal.challengeHandler,
|
||||||
'conversationJobQueue.add: Missing challengeHandler!'
|
'conversationJobQueue.add: Missing challengeHandler!'
|
||||||
|
@ -262,6 +354,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
conversationId,
|
conversationId,
|
||||||
reason: `conversationJobQueue.add(${conversationId}, ${type})`,
|
reason: `conversationJobQueue.add(${conversationId}, ${type})`,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return super.add(data, insert);
|
return super.add(data, insert);
|
||||||
}
|
}
|
||||||
|
@ -310,18 +403,239 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
globalLogger.warn(
|
globalLogger.warn(
|
||||||
`resolveVerificationWaiter: Missing waiter for conversation ${conversationId}.`
|
`resolveVerificationWaiter: Missing waiter for conversation ${conversationId}.`
|
||||||
);
|
);
|
||||||
|
this.unblockConversationRetries(conversationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unblockConversationRetries(conversationId: string) {
|
||||||
|
const logId = `unblockConversationRetries/${conversationId}`;
|
||||||
|
|
||||||
|
const perConversationData = this.perConversationData.get(conversationId);
|
||||||
|
if (!perConversationData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, callback } = perConversationData;
|
||||||
|
if (status === RETRY_STATUS.BLOCKED) {
|
||||||
|
globalLogger.info(
|
||||||
|
`${logId}: Deleting previous BLOCKED state; had no jobs`
|
||||||
|
);
|
||||||
|
this.perConversationData.delete(conversationId);
|
||||||
|
} else if (status === RETRY_STATUS.BLOCKED_WITH_JOBS) {
|
||||||
|
globalLogger.info(
|
||||||
|
`${logId}: Moving previous WAITING state to UNBLOCKED, calling callback directly`
|
||||||
|
);
|
||||||
|
this.perConversationData.set(conversationId, {
|
||||||
|
...perConversationData,
|
||||||
|
status: RETRY_STATUS.UNBLOCKED,
|
||||||
|
retryAt: undefined,
|
||||||
|
});
|
||||||
|
callback();
|
||||||
|
} else if (status === RETRY_STATUS.UNBLOCKED) {
|
||||||
|
globalLogger.warn(
|
||||||
|
`${logId}: We're still in UNBLOCKED state; calling callback directly`
|
||||||
|
);
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private captureRetryAt(conversationId: string, retryAt: number | undefined) {
|
||||||
|
const logId = `captureRetryAt/${conversationId}`;
|
||||||
|
|
||||||
|
const newRetryAt = retryAt || Date.now() + MINUTE;
|
||||||
|
const perConversationData = this.perConversationData.get(conversationId);
|
||||||
|
if (!perConversationData) {
|
||||||
|
if (!retryAt) {
|
||||||
|
globalLogger.warn(
|
||||||
|
`${logId}: No existing data, using retryAt of ${newRetryAt}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.perConversationData.set(conversationId, {
|
||||||
|
status: RETRY_STATUS.BLOCKED,
|
||||||
|
retryAt: newRetryAt,
|
||||||
|
callback: undefined,
|
||||||
|
jobsNeedingRetry: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, retryAt: existingRetryAt } = perConversationData;
|
||||||
|
if (existingRetryAt && existingRetryAt >= newRetryAt) {
|
||||||
|
globalLogger.warn(
|
||||||
|
`${logId}: New newRetryAt ${newRetryAt} isn't after existing retryAt ${existingRetryAt}, dropping`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
status === RETRY_STATUS.BLOCKED ||
|
||||||
|
status === RETRY_STATUS.BLOCKED_WITH_JOBS
|
||||||
|
) {
|
||||||
|
globalLogger.info(
|
||||||
|
`${logId}: Updating to newRetryAt ${newRetryAt} from existing retryAt ${existingRetryAt}, status ${status}`
|
||||||
|
);
|
||||||
|
this.perConversationData.set(conversationId, {
|
||||||
|
...perConversationData,
|
||||||
|
retryAt: newRetryAt,
|
||||||
|
});
|
||||||
|
} else if (status === RETRY_STATUS.UNBLOCKED) {
|
||||||
|
globalLogger.info(
|
||||||
|
`${logId}: Updating to newRetryAt ${newRetryAt} from previous UNBLOCKED status`
|
||||||
|
);
|
||||||
|
this.perConversationData.set(conversationId, {
|
||||||
|
...perConversationData,
|
||||||
|
status: RETRY_STATUS.BLOCKED_WITH_JOBS,
|
||||||
|
retryAt: newRetryAt,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override async retryJobOnQueueIdle({
|
||||||
|
job,
|
||||||
|
storedJob,
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
job: Readonly<ParsedJob<ConversationQueueJobData>>;
|
||||||
|
storedJob: Readonly<StoredJob>;
|
||||||
|
logger: LoggerType;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const { conversationId } = job.data;
|
||||||
|
const logId = `retryJobOnQueueIdle/${conversationId}/${job.id}`;
|
||||||
|
const perConversationData = this.perConversationData.get(conversationId);
|
||||||
|
|
||||||
|
if (!perConversationData) {
|
||||||
|
logger.warn(`${logId}: no data for conversation; using default retryAt`);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`${logId}: adding to existing data with status ${perConversationData.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, retryAt, jobsNeedingRetry, callback } =
|
||||||
|
perConversationData || {
|
||||||
|
status: RETRY_STATUS.BLOCKED,
|
||||||
|
retryAt: Date.now() + MINUTE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newJobsNeedingRetry = (jobsNeedingRetry || []).concat([storedJob]);
|
||||||
|
logger.info(
|
||||||
|
`${logId}: job added to retry queue with status ${status}; ${newJobsNeedingRetry.length} items now in queue`
|
||||||
|
);
|
||||||
|
|
||||||
|
const newCallback =
|
||||||
|
callback || this.createRetryCallback(conversationId, job.id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
status === RETRY_STATUS.BLOCKED ||
|
||||||
|
status === RETRY_STATUS.BLOCKED_WITH_JOBS
|
||||||
|
) {
|
||||||
|
this.perConversationData.set(conversationId, {
|
||||||
|
status: RETRY_STATUS.BLOCKED_WITH_JOBS,
|
||||||
|
retryAt,
|
||||||
|
jobsNeedingRetry: newJobsNeedingRetry,
|
||||||
|
callback: newCallback,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.perConversationData.set(conversationId, {
|
||||||
|
status: RETRY_STATUS.UNBLOCKED,
|
||||||
|
retryAt,
|
||||||
|
jobsNeedingRetry: newJobsNeedingRetry,
|
||||||
|
callback: newCallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newCallback !== callback) {
|
||||||
|
const queue = this.getInMemoryQueue(job);
|
||||||
|
drop(
|
||||||
|
// eslint-disable-next-line more/no-then
|
||||||
|
queue.onIdle().then(() => {
|
||||||
|
globalLogger.info(`${logId}: Running callback due to queue.onIdle`);
|
||||||
|
newCallback();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRetryCallback(conversationId: string, jobId: string) {
|
||||||
|
this.callbackCount += 1;
|
||||||
|
const id = this.callbackCount;
|
||||||
|
|
||||||
|
globalLogger.info(
|
||||||
|
`createRetryCallback/${conversationId}/${id}: callback created for job ${jobId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const logId = `retryCallback/${conversationId}/${id}`;
|
||||||
|
|
||||||
|
const perConversationData = this.perConversationData.get(conversationId);
|
||||||
|
if (!perConversationData) {
|
||||||
|
globalLogger.warn(`${logId}: no perConversationData, returning early.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, retryAt } = perConversationData;
|
||||||
|
if (status === RETRY_STATUS.BLOCKED) {
|
||||||
|
globalLogger.warn(
|
||||||
|
`${logId}: Still in blocked state, no jobs to retry. Clearing perConversationData.`
|
||||||
|
);
|
||||||
|
this.perConversationData.delete(conversationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { callback, jobsNeedingRetry, retryAtTimeout } =
|
||||||
|
perConversationData;
|
||||||
|
|
||||||
|
if (retryAtTimeout) {
|
||||||
|
clearTimeoutIfNecessary(retryAtTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!retryAt || isInPast(retryAt)) {
|
||||||
|
globalLogger.info(
|
||||||
|
`${logId}: retryAt is ${retryAt}; queueing ${jobsNeedingRetry?.length} jobs needing retry`
|
||||||
|
);
|
||||||
|
|
||||||
|
// We're starting to retry jobs; remove the challenge handler
|
||||||
|
drop(window.Signal.challengeHandler?.unregister(conversationId, logId));
|
||||||
|
|
||||||
|
jobsNeedingRetry?.forEach(job => {
|
||||||
|
drop(this.enqueueStoredJob(job));
|
||||||
|
});
|
||||||
|
this.perConversationData.delete(conversationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeLeft = retryAt - Date.now();
|
||||||
|
globalLogger.info(
|
||||||
|
`${logId}: retryAt ${retryAt} is in the future, scheduling timeout for ${timeLeft}ms`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.perConversationData.set(conversationId, {
|
||||||
|
...perConversationData,
|
||||||
|
retryAtTimeout: setTimeout(() => {
|
||||||
|
globalLogger.info(`${logId}: Running callback due to timeout`);
|
||||||
|
callback();
|
||||||
|
}, timeLeft),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected async run(
|
protected async run(
|
||||||
{
|
{
|
||||||
data,
|
data,
|
||||||
timestamp,
|
timestamp,
|
||||||
}: Readonly<{ data: ConversationQueueJobData; timestamp: number }>,
|
}: Readonly<{ data: ConversationQueueJobData; timestamp: number }>,
|
||||||
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
||||||
): Promise<void> {
|
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
const { type, conversationId } = data;
|
const { type, conversationId } = data;
|
||||||
const isFinalAttempt = attempt >= MAX_ATTEMPTS;
|
const isFinalAttempt = attempt >= MAX_ATTEMPTS;
|
||||||
|
const perConversationData = this.perConversationData.get(conversationId);
|
||||||
|
|
||||||
await window.ConversationController.load();
|
await window.ConversationController.load();
|
||||||
|
|
||||||
|
@ -330,6 +644,11 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
throw new Error(`Failed to find conversation ${conversationId}`);
|
throw new Error(`Failed to find conversation ${conversationId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (perConversationData?.retryAt && !shouldSendShowCaptcha(type)) {
|
||||||
|
// If we return this value, JobQueue will call retryJobOnQueueIdle for this job
|
||||||
|
return JOB_STATUS.NEEDS_RETRY;
|
||||||
|
}
|
||||||
|
|
||||||
let timeRemaining: number;
|
let timeRemaining: number;
|
||||||
let shouldContinue: boolean;
|
let shouldContinue: boolean;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
@ -350,10 +669,24 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.Signal.challengeHandler?.isRegistered(conversationId)) {
|
const isChallengeRegistered =
|
||||||
|
window.Signal.challengeHandler?.isRegistered(conversationId);
|
||||||
|
if (!isChallengeRegistered) {
|
||||||
|
this.unblockConversationRetries(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChallengeRegistered && shouldSendShowCaptcha(type)) {
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
throw new Error("Shutting down, can't wait for captcha challenge.");
|
throw new Error("Shutting down, can't wait for captcha challenge.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.Signal.challengeHandler?.maybeSolve({
|
||||||
|
conversationId,
|
||||||
|
reason:
|
||||||
|
'conversationJobQueue.run/addWaiter(' +
|
||||||
|
`${conversation.idForLogging()}, ${type}, ${timestamp})`,
|
||||||
|
});
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
'captcha challenge is pending for this conversation; waiting at most 5m...'
|
'captcha challenge is pending for this conversation; waiting at most 5m...'
|
||||||
);
|
);
|
||||||
|
@ -386,7 +719,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Cancelling profile share, we don't want to wait for pending verification."
|
"Cancelling profile share, we don't want to wait for pending verification."
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
|
@ -498,10 +831,14 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const untrustedServiceIds: Array<ServiceIdString> = [];
|
const untrustedServiceIds: Array<ServiceIdString> = [];
|
||||||
|
|
||||||
const processError = (toProcess: unknown) => {
|
const processError = (
|
||||||
|
toProcess: unknown
|
||||||
|
): undefined | typeof JOB_STATUS.NEEDS_RETRY => {
|
||||||
if (toProcess instanceof OutgoingIdentityKeyError) {
|
if (toProcess instanceof OutgoingIdentityKeyError) {
|
||||||
const failedConversation = window.ConversationController.getOrCreate(
|
const failedConversation = window.ConversationController.getOrCreate(
|
||||||
toProcess.identifier,
|
toProcess.identifier,
|
||||||
|
@ -513,11 +850,14 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
log.error(
|
log.error(
|
||||||
`failedConversation: Conversation ${failedConversation.idForLogging()} missing serviceId!`
|
`failedConversation: Conversation ${failedConversation.idForLogging()} missing serviceId!`
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
untrustedServiceIds.push(serviceId);
|
untrustedServiceIds.push(serviceId);
|
||||||
} else if (toProcess instanceof SendMessageChallengeError) {
|
} else if (toProcess instanceof SendMessageChallengeError) {
|
||||||
void window.Signal.challengeHandler?.register(
|
const silent = !shouldSendShowCaptcha(type);
|
||||||
|
|
||||||
|
drop(
|
||||||
|
window.Signal.challengeHandler?.register(
|
||||||
{
|
{
|
||||||
conversationId,
|
conversationId,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
@ -526,15 +866,31 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
reason:
|
reason:
|
||||||
'conversationJobQueue.run(' +
|
'conversationJobQueue.run(' +
|
||||||
`${conversation.idForLogging()}, ${type}, ${timestamp})`,
|
`${conversation.idForLogging()}, ${type}, ${timestamp})`,
|
||||||
|
silent,
|
||||||
},
|
},
|
||||||
toProcess.data
|
toProcess.data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
this.captureRetryAt(conversationId, toProcess.retryAt);
|
||||||
|
return JOB_STATUS.NEEDS_RETRY;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
processError(error);
|
const value = processError(error);
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof SendMessageProtoError) {
|
if (error instanceof SendMessageProtoError) {
|
||||||
(error.errors || []).forEach(processError);
|
const values = (error.errors || []).map(processError);
|
||||||
|
const innerValue = values.find(item => Boolean(item));
|
||||||
|
if (innerValue) {
|
||||||
|
return innerValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (untrustedServiceIds.length) {
|
if (untrustedServiceIds.length) {
|
||||||
|
@ -542,14 +898,14 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||||
log.warn(
|
log.warn(
|
||||||
`Cancelling profile share, since there were ${untrustedServiceIds.length} untrusted send targets.`
|
`Cancelling profile share, since there were ${untrustedServiceIds.length} untrusted send targets.`
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === jobSet.Receipts) {
|
if (type === jobSet.Receipts) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`Cancelling receipt send, since there were ${untrustedServiceIds.length} untrusted send targets.`
|
`Cancelling receipt send, since there were ${untrustedServiceIds.length} untrusted send targets.`
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error(
|
log.error(
|
||||||
|
|
|
@ -444,6 +444,7 @@ export async function sendStory(
|
||||||
reason:
|
reason:
|
||||||
'conversationJobQueue.run(' +
|
'conversationJobQueue.run(' +
|
||||||
`${conversation.idForLogging()}, story, ${timestamp}/${distributionId})`,
|
`${conversation.idForLogging()}, story, ${timestamp}/${distributionId})`,
|
||||||
|
silent: false,
|
||||||
},
|
},
|
||||||
error.data
|
error.data
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { isRecord } from '../util/isRecord';
|
import { isRecord } from '../util/isRecord';
|
||||||
|
|
||||||
|
import type { JOB_STATUS } from './JobQueue';
|
||||||
import { JobQueue } from './JobQueue';
|
import { JobQueue } from './JobQueue';
|
||||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ export class ReadSyncJobQueue extends JobQueue<ReadSyncJobData> {
|
||||||
protected async run(
|
protected async run(
|
||||||
{ data, timestamp }: Readonly<{ data: ReadSyncJobData; timestamp: number }>,
|
{ data, timestamp }: Readonly<{ data: ReadSyncJobData; timestamp: number }>,
|
||||||
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
||||||
): Promise<void> {
|
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
await runSyncJob({
|
await runSyncJob({
|
||||||
attempt,
|
attempt,
|
||||||
log,
|
log,
|
||||||
|
@ -40,6 +41,8 @@ export class ReadSyncJobQueue extends JobQueue<ReadSyncJobData> {
|
||||||
timestamp,
|
timestamp,
|
||||||
type: SyncTypeList.Read,
|
type: SyncTypeList.Read,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JOB_STATUS } from './JobQueue';
|
||||||
import { JobQueue } from './JobQueue';
|
import { JobQueue } from './JobQueue';
|
||||||
|
|
||||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||||
|
|
||||||
const removeStorageKeyJobDataSchema = z.object({
|
const removeStorageKeyJobDataSchema = z.object({
|
||||||
|
@ -24,12 +26,16 @@ export class RemoveStorageKeyJobQueue extends JobQueue<RemoveStorageKeyJobData>
|
||||||
|
|
||||||
protected async run({
|
protected async run({
|
||||||
data,
|
data,
|
||||||
}: Readonly<{ data: RemoveStorageKeyJobData }>): Promise<void> {
|
}: Readonly<{ data: RemoveStorageKeyJobData }>): Promise<
|
||||||
|
typeof JOB_STATUS.NEEDS_RETRY | undefined
|
||||||
|
> {
|
||||||
await new Promise<void>(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
window.storage.onready(resolve);
|
window.storage.onready(resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
await window.storage.remove(data.key);
|
await window.storage.remove(data.key);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { LoggerType } from '../types/Logging';
|
||||||
import { aciSchema } from '../types/ServiceId';
|
import { aciSchema } from '../types/ServiceId';
|
||||||
import { map } from '../util/iterables';
|
import { map } from '../util/iterables';
|
||||||
|
|
||||||
|
import type { JOB_STATUS } from './JobQueue';
|
||||||
import { JobQueue } from './JobQueue';
|
import { JobQueue } from './JobQueue';
|
||||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||||
import { parseIntWithFallback } from '../util/parseIntWithFallback';
|
import { parseIntWithFallback } from '../util/parseIntWithFallback';
|
||||||
|
@ -49,7 +50,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
|
||||||
protected async run(
|
protected async run(
|
||||||
{ data }: Readonly<{ data: ReportSpamJobData }>,
|
{ data }: Readonly<{ data: ReportSpamJobData }>,
|
||||||
{ log }: Readonly<{ log: LoggerType }>
|
{ log }: Readonly<{ log: LoggerType }>
|
||||||
): Promise<void> {
|
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
const { aci: senderAci, token, serverGuids } = data;
|
const { aci: senderAci, token, serverGuids } = data;
|
||||||
|
|
||||||
await new Promise<void>(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
|
@ -58,7 +59,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
|
||||||
|
|
||||||
if (!isDeviceLinked()) {
|
if (!isDeviceLinked()) {
|
||||||
log.info("reportSpamJobQueue: skipping this job because we're unlinked");
|
log.info("reportSpamJobQueue: skipping this job because we're unlinked");
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitForOnline(window.navigator, window);
|
await waitForOnline(window.navigator, window);
|
||||||
|
@ -72,6 +73,8 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
|
||||||
server.reportMessage({ senderAci, serverGuid, token })
|
server.reportMessage({ senderAci, serverGuid, token })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (!(err instanceof HTTPError)) {
|
if (!(err instanceof HTTPError)) {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -88,7 +91,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
|
||||||
log.info(
|
log.info(
|
||||||
'reportSpamJobQueue: server responded with 508. Giving up on this job'
|
'reportSpamJobQueue: server responded with 508. Giving up on this job'
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRetriable4xxStatus(code) || is5xxStatus(code)) {
|
if (isRetriable4xxStatus(code) || is5xxStatus(code)) {
|
||||||
|
@ -106,7 +109,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
|
||||||
log.error(
|
log.error(
|
||||||
`reportSpamJobQueue: server responded with ${code} status code. Giving up on this job`
|
`reportSpamJobQueue: server responded with ${code} status code. Giving up on this job`
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import * as Bytes from '../Bytes';
|
||||||
import type { LoggerType } from '../types/Logging';
|
import type { LoggerType } from '../types/Logging';
|
||||||
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
|
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
|
||||||
import type { ParsedJob } from './types';
|
import type { ParsedJob } from './types';
|
||||||
|
import type { JOB_STATUS } from './JobQueue';
|
||||||
import { JobQueue } from './JobQueue';
|
import { JobQueue } from './JobQueue';
|
||||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||||
import { DAY } from '../util/durations';
|
import { DAY } from '../util/durations';
|
||||||
|
@ -51,7 +52,7 @@ export class SingleProtoJobQueue extends JobQueue<SingleProtoJobData> {
|
||||||
timestamp,
|
timestamp,
|
||||||
}: Readonly<{ data: SingleProtoJobData; timestamp: number }>,
|
}: Readonly<{ data: SingleProtoJobData; timestamp: number }>,
|
||||||
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
||||||
): Promise<void> {
|
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now();
|
const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now();
|
||||||
const isFinalAttempt = attempt >= MAX_ATTEMPTS;
|
const isFinalAttempt = attempt >= MAX_ATTEMPTS;
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ export class SingleProtoJobQueue extends JobQueue<SingleProtoJobData> {
|
||||||
skipWait: false,
|
skipWait: false,
|
||||||
});
|
});
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -87,19 +88,19 @@ export class SingleProtoJobQueue extends JobQueue<SingleProtoJobData> {
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (isConversationUnregistered(conversation.attributes)) {
|
if (isConversationUnregistered(conversation.attributes)) {
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (conversation.isBlocked()) {
|
if (conversation.isBlocked()) {
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||||
);
|
);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proto = Proto.Content.decode(Bytes.fromBase64(protoBase64));
|
const proto = Proto.Content.decode(Bytes.fromBase64(protoBase64));
|
||||||
|
@ -133,6 +134,8 @@ export class SingleProtoJobQueue extends JobQueue<SingleProtoJobData> {
|
||||||
toThrow: error,
|
toThrow: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { isRecord } from '../util/isRecord';
|
import { isRecord } from '../util/isRecord';
|
||||||
|
|
||||||
|
import type { JOB_STATUS } from './JobQueue';
|
||||||
import { JobQueue } from './JobQueue';
|
import { JobQueue } from './JobQueue';
|
||||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ export class ViewOnceOpenJobQueue extends JobQueue<ViewOnceOpenJobData> {
|
||||||
timestamp,
|
timestamp,
|
||||||
}: Readonly<{ data: ViewOnceOpenJobData; timestamp: number }>,
|
}: Readonly<{ data: ViewOnceOpenJobData; timestamp: number }>,
|
||||||
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
||||||
): Promise<void> {
|
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
await runSyncJob({
|
await runSyncJob({
|
||||||
attempt,
|
attempt,
|
||||||
log,
|
log,
|
||||||
|
@ -43,6 +44,8 @@ export class ViewOnceOpenJobQueue extends JobQueue<ViewOnceOpenJobData> {
|
||||||
timestamp,
|
timestamp,
|
||||||
type: SyncTypeList.ViewOnceOpen,
|
type: SyncTypeList.ViewOnceOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { isRecord } from '../util/isRecord';
|
import { isRecord } from '../util/isRecord';
|
||||||
|
|
||||||
|
import type { JOB_STATUS } from './JobQueue';
|
||||||
import { JobQueue } from './JobQueue';
|
import { JobQueue } from './JobQueue';
|
||||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ export class ViewSyncJobQueue extends JobQueue<ViewSyncJobData> {
|
||||||
protected async run(
|
protected async run(
|
||||||
{ data, timestamp }: Readonly<{ data: ViewSyncJobData; timestamp: number }>,
|
{ data, timestamp }: Readonly<{ data: ViewSyncJobData; timestamp: number }>,
|
||||||
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
||||||
): Promise<void> {
|
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
await runSyncJob({
|
await runSyncJob({
|
||||||
attempt,
|
attempt,
|
||||||
log,
|
log,
|
||||||
|
@ -40,6 +41,8 @@ export class ViewSyncJobQueue extends JobQueue<ViewSyncJobData> {
|
||||||
timestamp,
|
timestamp,
|
||||||
type: SyncTypeList.View,
|
type: SyncTypeList.View,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ describe('ChallengeHandler', () => {
|
||||||
retryAt: NOW + DEFAULT_RETRY_AFTER,
|
retryAt: NOW + DEFAULT_RETRY_AFTER,
|
||||||
createdAt: NOW - SECOND,
|
createdAt: NOW - SECOND,
|
||||||
reason: 'test',
|
reason: 'test',
|
||||||
|
silent: false,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -185,7 +186,7 @@ describe('ChallengeHandler', () => {
|
||||||
await createHandler();
|
await createHandler();
|
||||||
|
|
||||||
for (const challenge of challenges) {
|
for (const challenge of challenges) {
|
||||||
await handler.unregister(challenge.conversationId);
|
await handler.unregister(challenge.conversationId, 'test');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const challenge of challenges) {
|
for (const challenge of challenges) {
|
||||||
|
@ -223,7 +224,7 @@ describe('ChallengeHandler', () => {
|
||||||
autoSolve: true,
|
autoSolve: true,
|
||||||
expireAfter: -1,
|
expireAfter: -1,
|
||||||
});
|
});
|
||||||
await handler.unregister(one.conversationId);
|
await handler.unregister(one.conversationId, 'test');
|
||||||
|
|
||||||
challengeStatus = 'idle';
|
challengeStatus = 'idle';
|
||||||
await newHandler.load();
|
await newHandler.load();
|
||||||
|
|
|
@ -147,7 +147,7 @@ describe('editing', function (this: Mocha.Suite) {
|
||||||
.locator(`.module-message__text >> "${initialMessageBody}"`)
|
.locator(`.module-message__text >> "${initialMessageBody}"`)
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|
||||||
debug('waiting for receipts for original message');
|
debug('waiting for outgoing receipts for original message');
|
||||||
const receipts = await app.waitForReceipts();
|
const receipts = await app.waitForReceipts();
|
||||||
assert.strictEqual(receipts.type, ReceiptType.Read);
|
assert.strictEqual(receipts.type, ReceiptType.Read);
|
||||||
assert.strictEqual(receipts.timestamps.length, 1);
|
assert.strictEqual(receipts.timestamps.length, 1);
|
||||||
|
|
|
@ -20,6 +20,7 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
|
||||||
let bootstrap: Bootstrap;
|
let bootstrap: Bootstrap;
|
||||||
let app: App;
|
let app: App;
|
||||||
let contact: PrimaryDevice;
|
let contact: PrimaryDevice;
|
||||||
|
let contactB: PrimaryDevice;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
bootstrap = new Bootstrap({
|
bootstrap = new Bootstrap({
|
||||||
|
@ -34,6 +35,9 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
|
||||||
contact = await server.createPrimaryDevice({
|
contact = await server.createPrimaryDevice({
|
||||||
profileName: 'Jamie',
|
profileName: 'Jamie',
|
||||||
});
|
});
|
||||||
|
contactB = await server.createPrimaryDevice({
|
||||||
|
profileName: 'Kim',
|
||||||
|
});
|
||||||
|
|
||||||
let state = StorageState.getEmpty();
|
let state = StorageState.getEmpty();
|
||||||
|
|
||||||
|
@ -55,13 +59,28 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
|
||||||
},
|
},
|
||||||
ServiceIdKind.PNI
|
ServiceIdKind.PNI
|
||||||
);
|
);
|
||||||
|
state = state.addContact(
|
||||||
|
contactB,
|
||||||
|
{
|
||||||
|
whitelisted: true,
|
||||||
|
serviceE164: contactB.device.number,
|
||||||
|
identityKey: contactB.getPublicKey(ServiceIdKind.PNI).serialize(),
|
||||||
|
pni: toUntaggedPni(contactB.device.pni),
|
||||||
|
givenName: 'Kim',
|
||||||
|
},
|
||||||
|
ServiceIdKind.PNI
|
||||||
|
);
|
||||||
|
|
||||||
// Just to make PNI Contact visible in the left pane
|
// Just to make PNI Contact visible in the left pane
|
||||||
state = state.pin(contact, ServiceIdKind.PNI);
|
state = state.pin(contact, ServiceIdKind.PNI);
|
||||||
|
state = state.pin(contactB, ServiceIdKind.PNI);
|
||||||
|
|
||||||
const ourKey = await desktop.popSingleUseKey();
|
const ourKey = await desktop.popSingleUseKey();
|
||||||
await contact.addSingleUseKey(desktop, ourKey);
|
await contact.addSingleUseKey(desktop, ourKey);
|
||||||
|
|
||||||
|
const ourKeyB = await desktop.popSingleUseKey();
|
||||||
|
await contactB.addSingleUseKey(desktop, ourKeyB);
|
||||||
|
|
||||||
await phone.setStorageState(state);
|
await phone.setStorageState(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -95,11 +114,18 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
|
||||||
.locator(`[data-testid="${contact.toContact().aci}"]`)
|
.locator(`[data-testid="${contact.toContact().aci}"]`)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
debug('Accept conversation from contact');
|
debug('Accept conversation from contact - does not trigger captcha!');
|
||||||
await conversationStack
|
await conversationStack
|
||||||
.locator('.module-message-request-actions button >> "Accept"')
|
.locator('.module-message-request-actions button >> "Accept"')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
debug('Sending a message back to user - will trigger captcha!');
|
||||||
|
{
|
||||||
|
const input = await app.waitForEnabledComposer();
|
||||||
|
await input.type('Hi, good to hear from you!');
|
||||||
|
await input.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
debug('Waiting for challenge');
|
debug('Waiting for challenge');
|
||||||
const request = await app.waitForChallenge();
|
const request = await app.waitForChallenge();
|
||||||
|
|
||||||
|
@ -114,14 +140,122 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
|
||||||
target: contact.device.aci,
|
target: contact.device.aci,
|
||||||
});
|
});
|
||||||
|
|
||||||
debug(`rate limited requests: ${requests}`);
|
debug(`Rate-limited requests: ${requests}`);
|
||||||
assert.strictEqual(requests, 1);
|
assert.strictEqual(requests, 1, 'rate limit requests');
|
||||||
|
|
||||||
debug('Waiting for receipts');
|
debug('Waiting for outgoing read receipt');
|
||||||
const receipts = await app.waitForReceipts();
|
const receipts = await app.waitForReceipts();
|
||||||
|
|
||||||
assert.strictEqual(receipts.type, ReceiptType.Read);
|
assert.strictEqual(receipts.type, ReceiptType.Read);
|
||||||
assert.strictEqual(receipts.timestamps.length, 1);
|
assert.strictEqual(receipts.timestamps.length, 1, 'receipts');
|
||||||
assert.strictEqual(receipts.timestamps[0], timestamp);
|
assert.strictEqual(receipts.timestamps[0], timestamp);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should send non-bubble in ConvoA when ConvoB completes challenge', async () => {
|
||||||
|
const { server, desktop } = bootstrap;
|
||||||
|
|
||||||
|
debug(
|
||||||
|
`Rate limiting (desktop: ${desktop.aci}) -> (ContactA: ${contact.device.aci})`
|
||||||
|
);
|
||||||
|
server.rateLimit({ source: desktop.aci, target: contact.device.aci });
|
||||||
|
debug(
|
||||||
|
`Rate limiting (desktop: ${desktop.aci}) -> (ContactB: ${contactB.device.aci})`
|
||||||
|
);
|
||||||
|
server.rateLimit({ source: desktop.aci, target: contactB.device.aci });
|
||||||
|
|
||||||
|
const window = await app.getWindow();
|
||||||
|
const leftPane = window.locator('#LeftPane');
|
||||||
|
const conversationStack = window.locator('.Inbox__conversation-stack');
|
||||||
|
|
||||||
|
debug('Sending a message from ContactA');
|
||||||
|
const timestampA = bootstrap.getTimestamp();
|
||||||
|
await contact.sendText(desktop, 'Hello there!', {
|
||||||
|
timestamp: timestampA,
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(`Opening conversation with ContactA (${contact.toContact().aci})`);
|
||||||
|
await leftPane
|
||||||
|
.locator(`[data-testid="${contact.toContact().aci}"]`)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
debug('Accept conversation from ContactA - does not trigger captcha!');
|
||||||
|
await conversationStack
|
||||||
|
.locator('.module-message-request-actions button >> "Accept"')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
debug('Sending a message from ContactB');
|
||||||
|
const timestampB = bootstrap.getTimestamp();
|
||||||
|
await contactB.sendText(desktop, 'Hey there!', {
|
||||||
|
timestamp: timestampB,
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(`Opening conversation with ContactB (${contact.toContact().aci})`);
|
||||||
|
await leftPane
|
||||||
|
.locator(`[data-testid="${contactB.toContact().aci}"]`)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
debug('Accept conversation from ContactB - does not trigger captcha!');
|
||||||
|
await conversationStack
|
||||||
|
.locator('.module-message-request-actions button >> "Accept"')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
debug('Sending a message back to ContactB - will trigger captcha!');
|
||||||
|
{
|
||||||
|
const input = await app.waitForEnabledComposer();
|
||||||
|
await input.type('Hi, good to hear from you!');
|
||||||
|
await input.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Waiting for challenge');
|
||||||
|
const request = await app.waitForChallenge();
|
||||||
|
|
||||||
|
debug('Solving challenge');
|
||||||
|
await app.solveChallenge({
|
||||||
|
seq: request.seq,
|
||||||
|
data: { captcha: 'anything' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestsA = server.stopRateLimiting({
|
||||||
|
source: desktop.aci,
|
||||||
|
target: contact.device.aci,
|
||||||
|
});
|
||||||
|
const requestsB = server.stopRateLimiting({
|
||||||
|
source: desktop.aci,
|
||||||
|
target: contactB.device.aci,
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(`Rate-limited requests to A: ${requestsA}`);
|
||||||
|
assert.strictEqual(requestsA, 1, 'rate limit requests');
|
||||||
|
|
||||||
|
debug(`Rate-limited requests to B: ${requestsA}`);
|
||||||
|
assert.strictEqual(requestsB, 1, 'rate limit requests');
|
||||||
|
|
||||||
|
debug('Waiting for outgoing read receipt #1');
|
||||||
|
const receipts1 = await app.waitForReceipts();
|
||||||
|
|
||||||
|
assert.strictEqual(receipts1.type, ReceiptType.Read);
|
||||||
|
assert.strictEqual(receipts1.timestamps.length, 1, 'receipts');
|
||||||
|
if (
|
||||||
|
!receipts1.timestamps.includes(timestampA) &&
|
||||||
|
!receipts1.timestamps.includes(timestampB)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'receipts1: Failed to find both timestampA and timestampB'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Waiting for outgoing read receipt #2');
|
||||||
|
const receipts2 = await app.waitForReceipts();
|
||||||
|
|
||||||
|
assert.strictEqual(receipts2.type, ReceiptType.Read);
|
||||||
|
assert.strictEqual(receipts2.timestamps.length, 1, 'receipts');
|
||||||
|
if (
|
||||||
|
!receipts2.timestamps.includes(timestampA) &&
|
||||||
|
!receipts2.timestamps.includes(timestampB)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'receipts2: Failed to find both timestampA and timestampB'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { drop } from '../../util/drop';
|
import { drop } from '../../util/drop';
|
||||||
import type { LoggerType } from '../../types/Logging';
|
import type { LoggerType } from '../../types/Logging';
|
||||||
|
|
||||||
|
import type { JOB_STATUS } from '../../jobs/JobQueue';
|
||||||
import { JobQueue } from '../../jobs/JobQueue';
|
import { JobQueue } from '../../jobs/JobQueue';
|
||||||
import type { ParsedJob, StoredJob, JobQueueStore } from '../../jobs/types';
|
import type { ParsedJob, StoredJob, JobQueueStore } from '../../jobs/types';
|
||||||
import { sleep } from '../../util/sleep';
|
import { sleep } from '../../util/sleep';
|
||||||
|
@ -38,8 +39,13 @@ describe('JobQueue', () => {
|
||||||
return testJobSchema.parse(data);
|
return testJobSchema.parse(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run({ data }: ParsedJob<TestJobData>): Promise<void> {
|
async run({
|
||||||
|
data,
|
||||||
|
}: ParsedJob<TestJobData>): Promise<
|
||||||
|
typeof JOB_STATUS.NEEDS_RETRY | undefined
|
||||||
|
> {
|
||||||
results.add(data.a + data.b);
|
results.add(data.a + data.b);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,13 +89,15 @@ describe('JobQueue', () => {
|
||||||
return z.number().parse(data);
|
return z.number().parse(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
try {
|
try {
|
||||||
updateActiveJobCount(1);
|
updateActiveJobCount(1);
|
||||||
await sleep(1);
|
await sleep(1);
|
||||||
} finally {
|
} finally {
|
||||||
updateActiveJobCount(-1);
|
updateActiveJobCount(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,8 +150,8 @@ describe('JobQueue', () => {
|
||||||
return testQueue;
|
return testQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
run(): Promise<void> {
|
run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,8 +183,8 @@ describe('JobQueue', () => {
|
||||||
return z.string().parse(data);
|
return z.string().parse(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,8 +251,8 @@ describe('JobQueue', () => {
|
||||||
return z.string().parse(data);
|
return z.string().parse(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,7 +299,11 @@ describe('JobQueue', () => {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run({ data }: ParsedJob<TestJobData>): Promise<void> {
|
async run({
|
||||||
|
data,
|
||||||
|
}: ParsedJob<TestJobData>): Promise<
|
||||||
|
typeof JOB_STATUS.NEEDS_RETRY | undefined
|
||||||
|
> {
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case 'foo':
|
case 'foo':
|
||||||
fooAttempts += 1;
|
fooAttempts += 1;
|
||||||
|
@ -308,6 +320,7 @@ describe('JobQueue', () => {
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(data);
|
throw missingCaseError(data);
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,7 +373,7 @@ describe('JobQueue', () => {
|
||||||
async run(
|
async run(
|
||||||
_: unknown,
|
_: unknown,
|
||||||
{ attempt }: Readonly<{ attempt: number }>
|
{ attempt }: Readonly<{ attempt: number }>
|
||||||
): Promise<void> {
|
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
attempts.push(attempt);
|
attempts.push(attempt);
|
||||||
throw new Error('this job always fails');
|
throw new Error('this job always fails');
|
||||||
}
|
}
|
||||||
|
@ -405,10 +418,12 @@ describe('JobQueue', () => {
|
||||||
async run(
|
async run(
|
||||||
_: unknown,
|
_: unknown,
|
||||||
{ log }: Readonly<{ log: LoggerType }>
|
{ log }: Readonly<{ log: LoggerType }>
|
||||||
): Promise<void> {
|
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
log.info(uniqueString);
|
log.info(uniqueString);
|
||||||
log.warn(uniqueString);
|
log.warn(uniqueString);
|
||||||
log.error(uniqueString);
|
log.error(uniqueString);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -450,8 +465,8 @@ describe('JobQueue', () => {
|
||||||
throw new Error('uh oh');
|
throw new Error('uh oh');
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -494,7 +509,9 @@ describe('JobQueue', () => {
|
||||||
throw new Error('invalid data!');
|
throw new Error('invalid data!');
|
||||||
}
|
}
|
||||||
|
|
||||||
run(job: { data: string }): Promise<void> {
|
run(job: {
|
||||||
|
data: string;
|
||||||
|
}): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return run(job);
|
return run(job);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -528,8 +545,8 @@ describe('JobQueue', () => {
|
||||||
throw new Error('invalid data!');
|
throw new Error('invalid data!');
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,8 +579,8 @@ describe('JobQueue', () => {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -596,8 +613,9 @@ describe('JobQueue', () => {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
events.push('running');
|
events.push('running');
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -629,8 +647,8 @@ describe('JobQueue', () => {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -670,7 +688,7 @@ describe('JobQueue', () => {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
events.push('running');
|
events.push('running');
|
||||||
throw new Error('uh oh');
|
throw new Error('uh oh');
|
||||||
}
|
}
|
||||||
|
@ -751,8 +769,13 @@ describe('JobQueue', () => {
|
||||||
return z.number().parse(data);
|
return z.number().parse(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run({ data }: Readonly<{ data: number }>): Promise<void> {
|
async run({
|
||||||
|
data,
|
||||||
|
}: Readonly<{ data: number }>): Promise<
|
||||||
|
typeof JOB_STATUS.NEEDS_RETRY | undefined
|
||||||
|
> {
|
||||||
eventEmitter.emit('run', data);
|
eventEmitter.emit('run', data);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -794,8 +817,8 @@ describe('JobQueue', () => {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -827,8 +850,8 @@ describe('JobQueue', () => {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||||
return Promise.resolve();
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue