mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 21:02:50 +03:00
* fix(compression): remove isStrictlySerializable gate that rejects valid bodies (#13154) Fixes #13154 isCompressionWorkerEligible used isStrictlySerializable to pre-validate bodies before posting them to a worker thread. The gate is stricter than structuredClone (which postMessage uses natively), rejecting: - undefined values (common in optional config fields) - Date, Map, Set, Uint8Array, RegExp (all structuredClone-compatible) - Shared (non-cyclic) sub-objects (misread as cycles) This caused compression to fall back to inline execution on the main event loop, blocking every concurrent request for the duration of compression passes — the exact failure mode of #10300. The serializability walk is a slower, buggier duplicate of the check postMessage already performs. Removing it: - Eliminates a recursive walk of the entire body on the main thread - Fixes false rejections that prevent worker offload - Allows the catch block at the call site to properly fall through to inline compression instead of silently shipping uncompressed * fix(compression): track only the recursion path, not the whole tree, in isStrictlySerializable (#13154) Restores the cycle-detection gate instead of removing it: the original bug was a single `seen` set shared across the entire recursion tree, never backtracked, so two sibling branches referencing the SAME non-cyclic sub-object were misread as a cycle. Adding to `seen` before descending and removing it after (try/finally) fixes the false positive while a genuine cycle is still rejected before it ever reaches postMessage/the worker. Also reverts the worker-failure catch in runCompressionAsync back to returning the body uncompressed: a worker timeout means the compression was already too heavy for the worker's own budget, so falling through to run that same heavy compression synchronously on the main event loop defeats the point of offloading it to a worker in the first place. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Koosha Pari <koosha@phenotype.ai> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import type { CompressionConfig, CompressionMode, CompressionResult } from "./types.ts";
|
|
import type { StackedCompressionStep } from "./strategySelector.ts";
|
|
import type {
|
|
CompressionStage,
|
|
CompressionWireFormat,
|
|
ImageTransportFidelity,
|
|
} from "./engines/types.ts";
|
|
|
|
export interface CompressionWorkerOptions {
|
|
model?: string;
|
|
supportsVision?: boolean | null;
|
|
providerTransport?: "direct" | "aggregator";
|
|
provider?: string;
|
|
imageTransportFidelity?: ImageTransportFidelity;
|
|
sourceFormat?: CompressionWireFormat;
|
|
targetFormat?: CompressionWireFormat;
|
|
compressionStage?: CompressionStage;
|
|
config?: CompressionConfig;
|
|
}
|
|
export interface CompressionWorkerJob {
|
|
id: number;
|
|
body: Record<string, unknown>;
|
|
mode: CompressionMode;
|
|
options?: CompressionWorkerOptions;
|
|
}
|
|
export type CompressionWorkerMessage =
|
|
| { id: number; type: "step"; step: StackedCompressionStep }
|
|
| { id: number; type: "result"; result: CompressionResult }
|
|
| { id: number; type: "error"; error: string };
|
|
|
|
function isPlainObject(value: object): value is Record<string, unknown> {
|
|
const prototype = Object.getPrototypeOf(value);
|
|
return prototype === Object.prototype || prototype === null;
|
|
}
|
|
|
|
// `seen` tracks only the current recursion PATH (ancestors), not every node ever visited:
|
|
// add before descending, remove after returning. That way a real cycle (a node reachable
|
|
// from itself) is still rejected, but two sibling branches that happen to reference the
|
|
// SAME non-cyclic sub-object (a false positive with a globally-shared `seen` set) are not.
|
|
export function isStrictlySerializable(value: unknown, seen = new Set<object>()): boolean {
|
|
if (
|
|
value === null ||
|
|
typeof value === "string" ||
|
|
typeof value === "boolean" ||
|
|
typeof value === "number"
|
|
) {
|
|
return typeof value !== "number" || Number.isFinite(value);
|
|
}
|
|
if (typeof value !== "object") return false;
|
|
if (seen.has(value)) return false;
|
|
seen.add(value);
|
|
try {
|
|
if (Array.isArray(value)) return value.every((entry) => isStrictlySerializable(entry, seen));
|
|
if (!isPlainObject(value)) return false;
|
|
return Object.values(value).every((entry) => isStrictlySerializable(entry, seen));
|
|
} finally {
|
|
seen.delete(value);
|
|
}
|
|
}
|
|
|
|
const WORKER_STACK_ENGINES = new Set(["caveman", "rtk", "standard"]);
|
|
export function isCompressionWorkerEligible(
|
|
body: Record<string, unknown>,
|
|
mode: CompressionMode,
|
|
options?: CompressionWorkerOptions
|
|
): boolean {
|
|
if (mode !== "standard" && mode !== "rtk" && mode !== "stacked") return false;
|
|
if (mode === "stacked") {
|
|
const pipeline = options?.config?.stackedPipeline;
|
|
if (!Array.isArray(pipeline) || pipeline.length === 0) return false;
|
|
if (
|
|
pipeline.some((step) => {
|
|
const engine = typeof step === "string" ? step : step.engine;
|
|
return !WORKER_STACK_ENGINES.has(engine);
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
return isStrictlySerializable({ body, mode, ...(options ? { options } : {}) });
|
|
}
|