chore(deps): upgrade all dependencies, toolchains and images (#2825)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-17 21:08:56 +02:00
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');
+1 -1
View File
@@ -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);