Merge branch 'develop' into upstream/2025.5.0
This commit is contained in:
commit
3ebf9c4a71
317 changed files with 6144 additions and 2603 deletions
|
|
@ -21,7 +21,7 @@ export class FileWriterStream extends WritableStream<Uint8Array> {
|
|||
write: async (chunk, controller) => {
|
||||
if (file === null) {
|
||||
controller.error();
|
||||
throw new Error();
|
||||
throw new Error('file is null');
|
||||
}
|
||||
|
||||
await file.write(chunk);
|
||||
|
|
|
|||
|
|
@ -308,8 +308,17 @@ export class MemoryKVCache<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries from the cache, but does not dispose it.
|
||||
*/
|
||||
@bindThis
|
||||
public clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.clear();
|
||||
clearInterval(this.gcIntervalHandle);
|
||||
}
|
||||
|
||||
|
|
|
|||
102
packages/backend/src/misc/diff-arrays.ts
Normal file
102
packages/backend/src/misc/diff-arrays.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface DiffResult<T> {
|
||||
added: T[];
|
||||
removed: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the difference between two snapshots of data.
|
||||
* Null, undefined, and empty arrays are supported, and duplicate values are ignored.
|
||||
* Result sets are de-duplicated, and will be empty if no data was added or removed (respectively).
|
||||
* The inputs are treated as un-ordered, so a re-ordering of the same data will NOT be considered a change.
|
||||
* @param dataBefore Array containing data before the change
|
||||
* @param dataAfter Array containing data after the change
|
||||
*/
|
||||
export function diffArrays<T>(dataBefore: T[] | null | undefined, dataAfter: T[] | null | undefined): DiffResult<T> {
|
||||
const before = dataBefore ? new Set(dataBefore) : null;
|
||||
const after = dataAfter ? new Set(dataAfter) : null;
|
||||
|
||||
// data before AND after => changed
|
||||
if (before?.size && after?.size) {
|
||||
const added: T[] = [];
|
||||
const removed: T[] = [];
|
||||
|
||||
for (const host of before) {
|
||||
// before and NOT after => removed
|
||||
// delete operation removes duplicates to speed up the "after" loop
|
||||
if (!after.delete(host)) {
|
||||
removed.push(host);
|
||||
}
|
||||
}
|
||||
|
||||
for (const host of after) {
|
||||
// after and NOT before => added
|
||||
if (!before.has(host)) {
|
||||
added.push(host);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
// data ONLY before => all removed
|
||||
if (before?.size) {
|
||||
return { added: [], removed: Array.from(before) };
|
||||
}
|
||||
|
||||
// data ONLY after => all added
|
||||
if (after?.size) {
|
||||
return { added: Array.from(after), removed: [] };
|
||||
}
|
||||
|
||||
// data NEITHER before nor after => no change
|
||||
return { added: [], removed: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for any difference between two snapshots of data.
|
||||
* Null, undefined, and empty arrays are supported, and duplicate values are ignored.
|
||||
* The inputs are treated as un-ordered, so a re-ordering of the same data will NOT be considered a change.
|
||||
* @param dataBefore Array containing data before the change
|
||||
* @param dataAfter Array containing data after the change
|
||||
*/
|
||||
export function diffArraysSimple<T>(dataBefore: T[] | null | undefined, dataAfter: T[] | null | undefined): boolean {
|
||||
const before = dataBefore ? new Set(dataBefore) : null;
|
||||
const after = dataAfter ? new Set(dataAfter) : null;
|
||||
|
||||
if (before?.size && after?.size) {
|
||||
// different size => changed
|
||||
if (before.size !== after.size) return true;
|
||||
|
||||
// removed => changed
|
||||
for (const host of before) {
|
||||
// delete operation removes duplicates to speed up the "after" loop
|
||||
if (!after.delete(host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// added => changed
|
||||
for (const host of after) {
|
||||
if (!before.has(host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// identical values => no change
|
||||
return false;
|
||||
}
|
||||
|
||||
// before and NOT after => change
|
||||
if (before?.size) return true;
|
||||
|
||||
// after and NOT before => change
|
||||
if (after?.size) return true;
|
||||
|
||||
// NEITHER before nor after => no change
|
||||
return false;
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ export class FastifyReplyError extends Error {
|
|||
public message: string;
|
||||
public statusCode: number;
|
||||
|
||||
constructor(statusCode: number, message: string) {
|
||||
super(message);
|
||||
constructor(statusCode: number, message: string, cause?: unknown) {
|
||||
super(message, cause ? { cause } : undefined);
|
||||
this.message = message;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import * as crypto from 'node:crypto';
|
||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||
import { IdentifiableError } from '../identifiable-error.js';
|
||||
|
||||
export const aidRegExp = /^[0-9a-z]{10}$/;
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ function getNoise(): string {
|
|||
}
|
||||
|
||||
export function genAid(t: number): string {
|
||||
if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date');
|
||||
if (isNaN(t)) throw new IdentifiableError('6b73b7d5-9d2b-48b4-821c-ef955efe80ad', 'Failed to create AID: Invalid Date');
|
||||
counter++;
|
||||
return getTime(t) + getNoise();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||
import { IdentifiableError } from '../identifiable-error.js';
|
||||
|
||||
export const aidxRegExp = /^[0-9a-z]{16}$/;
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ function getNoise(): string {
|
|||
}
|
||||
|
||||
export function genAidx(t: number): string {
|
||||
if (isNaN(t)) throw new Error('Failed to create AIDX: Invalid Date');
|
||||
if (isNaN(t)) throw new IdentifiableError('6b73b7d5-9d2b-48b4-821c-ef955efe80ad', 'Failed to create AIDX: Invalid Date');
|
||||
counter++;
|
||||
return getTime(t) + nodeId + getNoise();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ export class IdentifiableError extends Error {
|
|||
*/
|
||||
public readonly isRetryable: boolean;
|
||||
|
||||
constructor(id: string, message?: string, isRetryable = false) {
|
||||
super(message);
|
||||
constructor(id: string, message?: string, isRetryable = false, cause?: unknown) {
|
||||
super(message, cause ? { cause } : undefined);
|
||||
this.message = message ?? '';
|
||||
this.id = id;
|
||||
this.isRetryable = isRetryable;
|
||||
|
|
|
|||
|
|
@ -3,20 +3,34 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { AbortError } from 'node-fetch';
|
||||
import { AbortError, FetchError } from 'node-fetch';
|
||||
import { UnrecoverableError } from 'bullmq';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { CaptchaError, captchaErrorCodes } from '@/core/CaptchaService.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { ConflictError } from '@/server/SkRateLimiterService.js';
|
||||
|
||||
/**
|
||||
* Returns false if the provided value represents a "permanent" error that cannot be retried.
|
||||
* Returns true if the error is retryable, unknown (as all errors are retryable by default), or not an error object.
|
||||
*/
|
||||
export function isRetryableError(e: unknown): boolean {
|
||||
if (e instanceof AggregateError) return e.errors.every(inner => isRetryableError(inner));
|
||||
if (e instanceof StatusError) return e.isRetryable;
|
||||
if (e instanceof IdentifiableError) return e.isRetryable;
|
||||
if (e instanceof CaptchaError) {
|
||||
if (e.code === captchaErrorCodes.verificationFailed) return false;
|
||||
if (e.code === captchaErrorCodes.invalidParameters) return false;
|
||||
if (e.code === captchaErrorCodes.invalidProvider) return false;
|
||||
return true;
|
||||
}
|
||||
if (e instanceof FastifyReplyError) return false;
|
||||
if (e instanceof ConflictError) return true;
|
||||
if (e instanceof UnrecoverableError) return false;
|
||||
if (e instanceof AbortError) return true;
|
||||
if (e instanceof FetchError) return true;
|
||||
if (e instanceof SyntaxError) return false;
|
||||
if (e instanceof Error) return e.name === 'AbortError';
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
60
packages/backend/src/misc/render-full-error.ts
Normal file
60
packages/backend/src/misc/render-full-error.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Bull from 'bullmq';
|
||||
import { AbortError, FetchError } from 'node-fetch';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||
import { CaptchaError, captchaErrorCodes } from '@/core/CaptchaService.js';
|
||||
|
||||
export function renderFullError(e?: unknown): unknown {
|
||||
if (e === undefined) return 'undefined';
|
||||
if (e === null) return 'null';
|
||||
|
||||
if (e instanceof Error) {
|
||||
if (isSimpleError(e)) {
|
||||
return renderInlineError(e);
|
||||
}
|
||||
|
||||
const data: ErrorData = {};
|
||||
if (e.stack) data.stack = e.stack;
|
||||
if (e.message) data.message = e.message;
|
||||
if (e.name) data.name = e.name;
|
||||
|
||||
// mix "cause" and "errors"
|
||||
if (e instanceof AggregateError && e.errors.length > 0) {
|
||||
const causes = e.errors.map(inner => renderFullError(inner));
|
||||
if (e.cause) {
|
||||
causes.push(renderFullError(e.cause));
|
||||
}
|
||||
data.cause = causes;
|
||||
} else if (e.cause) {
|
||||
data.cause = renderFullError(e.cause);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
function isSimpleError(e: Error): boolean {
|
||||
if (e instanceof Bull.UnrecoverableError) return true;
|
||||
if (e instanceof AbortError || e.name === 'AbortError') return true;
|
||||
if (e instanceof FetchError || e.name === 'FetchError') return true;
|
||||
if (e instanceof StatusError) return true;
|
||||
if (e instanceof IdentifiableError) return true;
|
||||
if (e instanceof FetchError) return true;
|
||||
if (e instanceof CaptchaError && e.code !== captchaErrorCodes.unknown) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
interface ErrorData {
|
||||
stack?: Error['stack'];
|
||||
message?: Error['message'];
|
||||
name?: Error['name'];
|
||||
cause?: Error['cause'] | Error['cause'][];
|
||||
}
|
||||
75
packages/backend/src/misc/render-inline-error.ts
Normal file
75
packages/backend/src/misc/render-inline-error.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { CaptchaError } from '@/core/CaptchaService.js';
|
||||
|
||||
export function renderInlineError(err: unknown): string {
|
||||
const parts: string[] = [];
|
||||
renderTo(err, parts);
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function renderTo(err: unknown, parts: string[]): void {
|
||||
parts.push(printError(err));
|
||||
|
||||
if (err instanceof AggregateError) {
|
||||
for (let i = 0; i < err.errors.length; i++) {
|
||||
parts.push(` [${i + 1}/${err.errors.length}]: `);
|
||||
renderTo(err.errors[i], parts);
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
if (err.cause) {
|
||||
parts.push(' [caused by]: ');
|
||||
renderTo(err.cause, parts);
|
||||
// const cause = renderInlineError(err.cause);
|
||||
// parts.push(' [caused by]: ', cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printError(err: unknown): string {
|
||||
if (err === undefined) return 'undefined';
|
||||
if (err === null) return 'null';
|
||||
|
||||
if (err instanceof IdentifiableError) {
|
||||
if (err.message) {
|
||||
return `${err.name} ${err.id}: ${err.message}`;
|
||||
} else {
|
||||
return `${err.name} ${err.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof StatusError) {
|
||||
if (err.message) {
|
||||
return `${err.name} ${err.statusCode}: ${err.message}`;
|
||||
} else if (err.statusMessage) {
|
||||
return `${err.name} ${err.statusCode}: ${err.statusMessage}`;
|
||||
} else {
|
||||
return `${err.name} ${err.statusCode}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof CaptchaError) {
|
||||
if (err.code.description) {
|
||||
return `${err.name} ${err.code.description}: ${err.message}`;
|
||||
} else {
|
||||
return `${err.name}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
if (err.message) {
|
||||
return `${err.name}: ${err.message}`;
|
||||
} else {
|
||||
return err.name;
|
||||
}
|
||||
}
|
||||
|
||||
return String(err);
|
||||
}
|
||||
|
|
@ -9,8 +9,8 @@ export class StatusError extends Error {
|
|||
public isClientError: boolean;
|
||||
public isRetryable: boolean;
|
||||
|
||||
constructor(message: string, statusCode: number, statusMessage?: string) {
|
||||
super(message);
|
||||
constructor(message: string, statusCode: number, statusMessage?: string, cause?: unknown) {
|
||||
super(message, cause ? { cause } : undefined);
|
||||
this.name = 'StatusError';
|
||||
this.statusCode = statusCode;
|
||||
this.statusMessage = statusMessage;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue