mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
chore(deps): upgrade all dependencies, toolchains and images (#2825)
This commit is contained in:
1 parent
56e04e7b53
commit
3b552e00ef
510 files changed
+23614
-37473
No files matched your search
@@ -4,22 +4,22 @@ import {randomUUID} from 'node:crypto';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import type {WorkerLaneDefinition} from '@app/api/worker/WorkerLaneConfig';
|
||||
import {WorkerQueueOverflowError} from '@app/api/worker/WorkerQueueOverflowError';
|
||||
import type {JetStreamConnectionManager} from '@pkgs/nats/src/JetStreamConnectionManager';
|
||||
import type {WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes';
|
||||
import {
|
||||
AckPolicy,
|
||||
type ConsumerInfo,
|
||||
type ConsumerUpdateConfig,
|
||||
DeliverPolicy,
|
||||
DiscardPolicy,
|
||||
JetStreamApiError,
|
||||
type JetStreamManager,
|
||||
NatsError,
|
||||
nanos,
|
||||
ReplayPolicy,
|
||||
RetentionPolicy,
|
||||
StorageType,
|
||||
type StreamConfig,
|
||||
} from 'nats';
|
||||
} from '@nats-io/jetstream';
|
||||
import {nanos} from '@nats-io/transport-node';
|
||||
import type {JetStreamConnectionManager} from '@pkgs/nats/src/JetStreamConnectionManager';
|
||||
import type {WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes';
|
||||
|
||||
const STREAM_NAME = 'JOBS';
|
||||
const SUBJECT_PREFIX = 'jobs.';
|
||||
@@ -55,12 +55,6 @@ interface WorkerStreamDefinition {
|
||||
discardNewPerSubject: boolean;
|
||||
}
|
||||
|
||||
interface WorkerStreamConfiguration extends StreamConfig {
|
||||
persist_mode?: string;
|
||||
allow_msg_ttl?: boolean;
|
||||
allow_msg_counter?: boolean;
|
||||
}
|
||||
|
||||
const JOBS_STREAM: WorkerStreamDefinition = {
|
||||
name: STREAM_NAME,
|
||||
subject: `${SUBJECT_PREFIX}>`,
|
||||
@@ -95,21 +89,14 @@ export interface WorkerDeadLetterMetadata {
|
||||
}
|
||||
|
||||
function jsErrorCode(error: unknown): number | null {
|
||||
if (!(error instanceof NatsError)) {
|
||||
return null;
|
||||
}
|
||||
return error.jsError()?.err_code ?? null;
|
||||
return error instanceof JetStreamApiError ? error.code : null;
|
||||
}
|
||||
|
||||
function describeStreamRejection(error: unknown): string | null {
|
||||
if (!(error instanceof NatsError)) {
|
||||
if (!(error instanceof JetStreamApiError) || !STREAM_FULL_ERR_CODES.has(error.code)) {
|
||||
return null;
|
||||
}
|
||||
const apiError = error.jsError();
|
||||
if (apiError === null || !STREAM_FULL_ERR_CODES.has(apiError.err_code ?? 0)) {
|
||||
return null;
|
||||
}
|
||||
return apiError.description ?? 'stream rejected the publish';
|
||||
return error.apiError().description || 'stream rejected the publish';
|
||||
}
|
||||
|
||||
export class JetStreamWorkerQueue {
|
||||
@@ -155,7 +142,7 @@ export class JetStreamWorkerQueue {
|
||||
}
|
||||
}
|
||||
|
||||
private async applyStreamLimits(jsm: JetStreamManager, existing: WorkerStreamConfiguration): Promise<void> {
|
||||
private async applyStreamLimits(jsm: JetStreamManager, existing: StreamConfig): Promise<void> {
|
||||
const unmanaged = this.unmanagedStreamSettings(existing, JOBS_STREAM);
|
||||
if (unmanaged.length > 0) {
|
||||
Logger.warn(
|
||||
@@ -205,7 +192,7 @@ export class JetStreamWorkerQueue {
|
||||
}
|
||||
}
|
||||
|
||||
private assertStreamIdentity(config: WorkerStreamConfiguration, stream: WorkerStreamDefinition): void {
|
||||
private assertStreamIdentity(config: StreamConfig, stream: WorkerStreamDefinition): void {
|
||||
const incompatible: Array<string> = [];
|
||||
if (config.name !== stream.name) incompatible.push('name');
|
||||
if (config.subjects?.length !== 1 || config.subjects[0] !== stream.subject) incompatible.push('subjects');
|
||||
@@ -222,7 +209,7 @@ export class JetStreamWorkerQueue {
|
||||
}
|
||||
}
|
||||
|
||||
private diffStreamLimits(config: WorkerStreamConfiguration, stream: WorkerStreamDefinition): Array<string> {
|
||||
private diffStreamLimits(config: StreamConfig, stream: WorkerStreamDefinition): Array<string> {
|
||||
const drifted: Array<string> = [];
|
||||
if (!Number.isSafeInteger(config.max_bytes) || config.max_bytes < stream.minBytes) drifted.push('max_bytes');
|
||||
if (config.max_msgs !== stream.maxMessages) drifted.push('max_msgs');
|
||||
@@ -234,7 +221,7 @@ export class JetStreamWorkerQueue {
|
||||
return drifted;
|
||||
}
|
||||
|
||||
private unmanagedStreamSettings(config: WorkerStreamConfiguration, stream: WorkerStreamDefinition): Array<string> {
|
||||
private unmanagedStreamSettings(config: StreamConfig, stream: WorkerStreamDefinition): Array<string> {
|
||||
const unmanaged: Array<string> = [];
|
||||
if (config.max_age !== nanos(stream.maxAgeMs)) unmanaged.push('max_age');
|
||||
if (config.duplicate_window !== nanos(DUPLICATE_WINDOW_MS)) unmanaged.push('duplicate_window');
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
type WorkerHeartbeat,
|
||||
type WorkerHeartbeatSignal,
|
||||
} from '@app/api/worker/WorkerHeartbeat';
|
||||
import type {ConsumerMessages, FetchOptions, JsMsg} from '@nats-io/jetstream';
|
||||
import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService';
|
||||
import {JobCancelledError, type WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask';
|
||||
import type {ConsumerMessages, FetchOptions, JsMsg} from 'nats';
|
||||
|
||||
const MAX_DLQ_PUBLISH_ATTEMPTS = 3;
|
||||
const DLQ_RETRY_DELAY_MS = 250;
|
||||
|
||||
@@ -428,7 +428,7 @@ const extractEmbeds: WorkerTaskHandler = async (payload, helpers) => {
|
||||
messageId,
|
||||
validated.expectedContentHash,
|
||||
);
|
||||
if (!message || !message.content) {
|
||||
if (!message?.content) {
|
||||
Logger.info({messageId: messageId.toString()}, 'Skipping extractEmbeds: message not found or no content');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ const userProcessPendingDeletions: WorkerTaskHandler = async (_payload, helpers)
|
||||
try {
|
||||
const userId = createUserID(deletion.userId);
|
||||
const user = await userRepository.findUnique(userId);
|
||||
if (!user || !user.pendingDeletionAt) {
|
||||
if (!user?.pendingDeletionAt) {
|
||||
Logger.warn({userId}, 'User not found or not pending deletion in Cassandra, removing from KV');
|
||||
await deletionQueueService.removeFromQueue(userId);
|
||||
continue;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import {JetStreamWorkerQueue} from '@app/api/worker/JetStreamWorkerQueue';
|
||||
import {WORKER_LANES} from '@app/api/worker/WorkerLaneConfig';
|
||||
import {WorkerQueueOverflowError} from '@app/api/worker/WorkerQueueOverflowError';
|
||||
import {DiscardPolicy, JetStreamApiError, RetentionPolicy, StorageType, type StreamConfig} from '@nats-io/jetstream';
|
||||
import type {JetStreamConnectionManager} from '@pkgs/nats/src/JetStreamConnectionManager';
|
||||
import {DiscardPolicy, NatsError, RetentionPolicy, StorageType, type StreamConfig} from 'nats';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const GIB = 1024 * 1024 * 1024;
|
||||
@@ -51,29 +51,24 @@ function withStreamDefaults(config: Partial<StreamConfig>): Partial<StreamConfig
|
||||
};
|
||||
}
|
||||
|
||||
function missingResourceError(resource: 'stream' | 'consumer'): NatsError {
|
||||
const description = `${resource} not found`;
|
||||
const error = new NatsError(description, '404');
|
||||
error.api_error = {code: 404, err_code: resource === 'stream' ? 10059 : 10014, description};
|
||||
return error;
|
||||
function missingResourceError(resource: 'stream' | 'consumer'): JetStreamApiError {
|
||||
return new JetStreamApiError({
|
||||
code: 404,
|
||||
err_code: resource === 'stream' ? 10059 : 10014,
|
||||
description: `${resource} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
function streamLimitError(description: string): NatsError {
|
||||
const error = new NatsError('503', '503');
|
||||
error.api_error = {code: 503, err_code: 10077, description};
|
||||
return error;
|
||||
function streamLimitError(description: string): JetStreamApiError {
|
||||
return new JetStreamApiError({code: 503, err_code: 10077, description});
|
||||
}
|
||||
|
||||
function serverResourceError(): NatsError {
|
||||
const error = new NatsError('503', '503');
|
||||
error.api_error = {code: 503, err_code: 10023, description: 'insufficient resources'};
|
||||
return error;
|
||||
function serverResourceError(): JetStreamApiError {
|
||||
return new JetStreamApiError({code: 503, err_code: 10023, description: 'insufficient resources'});
|
||||
}
|
||||
|
||||
function noStorageError(): NatsError {
|
||||
const error = new NatsError('503', '503');
|
||||
error.api_error = {code: 503, err_code: 10047, description: 'insufficient storage resources available'};
|
||||
return error;
|
||||
function noStorageError(): JetStreamApiError {
|
||||
return new JetStreamApiError({code: 503, err_code: 10047, description: 'insufficient storage resources available'});
|
||||
}
|
||||
|
||||
function storageBudget(budget: number): (config: Partial<StreamConfig>) => Error | null {
|
||||
@@ -205,7 +200,7 @@ describe('jobs stream limits', () => {
|
||||
|
||||
it('fails the boot when even the smallest jobs stream does not fit', async () => {
|
||||
const {queue, added} = createQueue({existing: null, reject: storageBudget(0)});
|
||||
await expect(queue.ensureStream()).rejects.toBeInstanceOf(NatsError);
|
||||
await expect(queue.ensureStream()).rejects.toBeInstanceOf(JetStreamApiError);
|
||||
expect(added).toHaveLength(8);
|
||||
expect(added[7]?.max_bytes).toBe(64 * MIB);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {UserRepository} from '@app/api/user/repositories/UserRepository';
|
||||
import {sendSystemDm} from '@app/api/worker/tasks/SendSystemDm';
|
||||
import {clearWorkerDependencies, setWorkerDependenciesForTest} from '@app/api/worker/WorkerContext';
|
||||
import {WorkerRunner} from '@app/api/worker/WorkerRunner';
|
||||
import type {JsMsg} from 'nats';
|
||||
import type {JsMsg} from '@nats-io/jetstream';
|
||||
import {afterEach, beforeAll, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const TASK_TYPE = 'sendSystemDm';
|
||||
|
||||
@@ -4,8 +4,8 @@ import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository';
|
||||
import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry';
|
||||
import {NoopWorkerService} from '@app/api/test/NoopWorkerService';
|
||||
import {WorkerRunner} from '@app/api/worker/WorkerRunner';
|
||||
import type {JsMsg} from '@nats-io/jetstream';
|
||||
import {JobCancelledError} from '@pkgs/worker/src/contracts/WorkerTask';
|
||||
import type {JsMsg} from 'nats';
|
||||
import {afterEach, beforeAll, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const LANE_ACK_WAIT_MS = 120000;
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
import {WorkerRunner} from '@app/api/worker/WorkerRunner';
|
||||
import type {WorkerService} from '@app/api/worker/WorkerService';
|
||||
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
|
||||
import type {ConsumerMessages, JsMsg} from '@nats-io/jetstream';
|
||||
import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider';
|
||||
import type {ConsumerMessages, JsMsg} from 'nats';
|
||||
import {afterEach, beforeAll, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const HEARTBEAT_PATH = '/tmp/fluxer-worker-heartbeat-test';
|
||||
|
||||
@@ -66,17 +66,17 @@ describe('Worker process error handler', () => {
|
||||
expect(exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['uncaughtException' as WorkerProcessErrorSource],
|
||||
['unhandledRejection' as WorkerProcessErrorSource],
|
||||
])('survives a transient error arriving as %s', async (source) => {
|
||||
const {exit, shutdown, handle} = createHarness();
|
||||
it.each([['uncaughtException' as WorkerProcessErrorSource], ['unhandledRejection' as WorkerProcessErrorSource]])(
|
||||
'survives a transient error arriving as %s',
|
||||
async (source) => {
|
||||
const {exit, shutdown, handle} = createHarness();
|
||||
|
||||
await handle(source, adminShutdownError());
|
||||
await handle(source, adminShutdownError());
|
||||
|
||||
expect(shutdown).not.toHaveBeenCalled();
|
||||
expect(exit).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(shutdown).not.toHaveBeenCalled();
|
||||
expect(exit).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('survives a socket error raised by a pooled Postgres client', async () => {
|
||||
const {exit, shutdown, handle} = createHarness();
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository';
|
||||
import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry';
|
||||
import {NoopWorkerService} from '@app/api/test/NoopWorkerService';
|
||||
import {WorkerRunner} from '@app/api/worker/WorkerRunner';
|
||||
import type {JsMsg} from 'nats';
|
||||
import type {JsMsg} from '@nats-io/jetstream';
|
||||
import {afterEach, beforeAll, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const RETIRED_TASK_TYPE = 'sendScheduledMessage';
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository';
|
||||
import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry';
|
||||
import {NoopWorkerService} from '@app/api/test/NoopWorkerService';
|
||||
import {WorkerRunner} from '@app/api/worker/WorkerRunner';
|
||||
import type {ConsumerMessages, JsMsg} from 'nats';
|
||||
import type {ConsumerMessages, JsMsg} from '@nats-io/jetstream';
|
||||
import {afterEach, beforeAll, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const TASK_TYPE = 'processInactivityDeletions';
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository';
|
||||
import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry';
|
||||
import {NoopWorkerService} from '@app/api/test/NoopWorkerService';
|
||||
import {WorkerRunner} from '@app/api/worker/WorkerRunner';
|
||||
import type {ConsumerMessages, JsMsg} from 'nats';
|
||||
import type {ConsumerMessages, JsMsg} from '@nats-io/jetstream';
|
||||
import {beforeAll, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const TASK_TYPE = 'processInactivityDeletions';
|
||||
|
||||
@@ -3,7 +3,7 @@ import {lstat, opendir, readlink} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type {Readable} from 'node:stream';
|
||||
import {finished, pipeline} from 'node:stream/promises';
|
||||
import archiver, {type Archiver, type EntryData} from 'archiver';
|
||||
import {type Archiver, type EntryData, ZipArchive} from 'archiver';
|
||||
|
||||
export interface ArchiveEntryWriter {
|
||||
append(source: Readable | Buffer | string, data: EntryData): Promise<void>;
|
||||
@@ -27,19 +27,18 @@ export async function writeZipArchive(
|
||||
filePath: string,
|
||||
produce: (archive: ArchiveFileWriter) => void | Promise<void>,
|
||||
): Promise<void> {
|
||||
const archive: Archiver = archiver('zip', {zlib: {level: 6}});
|
||||
const archive: Archiver = new ZipArchive({zlib: {level: 6}});
|
||||
const output = createWriteStream(filePath);
|
||||
const outputClosed = new Promise<void>((resolve) => output.once('close', resolve));
|
||||
const archiveClosed = new Promise<void>((resolve) => archive.once('close', resolve));
|
||||
const inputs = new Map<Readable, Promise<void>>();
|
||||
const entries = new Map<EntryData, PendingArchiveEntry>();
|
||||
const entries: Array<PendingArchiveEntry> = [];
|
||||
let failure: {error: unknown} | undefined;
|
||||
|
||||
function fail(error: unknown): void {
|
||||
if (failure) return;
|
||||
failure = {error};
|
||||
for (const entry of entries.values()) entry.reject(error);
|
||||
entries.clear();
|
||||
for (const entry of entries.splice(0)) entry.reject(error);
|
||||
const streamError = error instanceof Error ? error : new Error('Archive creation failed', {cause: error});
|
||||
for (const source of inputs.keys()) source.destroy(streamError);
|
||||
archive.abort();
|
||||
@@ -76,7 +75,7 @@ export async function writeZipArchive(
|
||||
}
|
||||
requireWritable();
|
||||
const entry = {...data};
|
||||
const processed = new Promise<void>((resolve, reject) => entries.set(entry, {resolve, reject}));
|
||||
const processed = new Promise<void>((resolve, reject) => entries.push({resolve, reject}));
|
||||
const completed = Promise.all([processed, completion]);
|
||||
try {
|
||||
archive.append(source, entry);
|
||||
@@ -117,14 +116,13 @@ export async function writeZipArchive(
|
||||
|
||||
archive.on('warning', fail);
|
||||
archive.on('error', fail);
|
||||
archive.on('entry', (entry) => {
|
||||
archive.on('entry', () => {
|
||||
if (failure) return;
|
||||
const pending = entries.get(entry);
|
||||
const pending = entries.shift();
|
||||
if (!pending) {
|
||||
fail(new Error('Archive completed an unknown entry'));
|
||||
return;
|
||||
}
|
||||
entries.delete(entry);
|
||||
pending.resolve();
|
||||
});
|
||||
output.on('error', fail);
|
||||
|
||||
Reference in new issue
Block a user