mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
* perf(compression): memory and OOM mitigations for large payload hashing and token estimation * fix(compression): implement getMemoStats observability for result memo (#7847) Adds the missing memo observability layer referenced by tests/unit/compression/oom-memo-memory.test.ts and the monitoring API: - resultMemo.ts: lifetime hit/miss counters + bounded time-ordered ring buffer (10k entries, ~90KB) powering 1m/5m/15m/1h hit-rate windows; getMemoStats() reports size/capacity/hits/misses/hitRate + windows. - memoLookup() tags served results with stats.memoHit = true. - clearMemoStore() also resets counters and the ring. - compression/index.ts re-exports getMemoStats for the monitoring route. - types.ts: optional memoHit field on CompressionStats. - New GET /api/monitoring/compression route exposing the stats snapshot (lightweight, no DB) for operators to track cache-hit efficiency. * fix(compression): align memo contract with upstream #11727 — return caller object, reset lookup counter in clearMemoStore * fix(compression): restore unwrapEventEnvelope in stream payload collector summaries The OOM-mitigation commit accidentally replaced unwrapEventEnvelope(evt.data) with asRecord(evt.data) in the summary builders and live push, breaking translate-mode {event, data} envelope unwrapping (clientPayload type detection) and failing 2 stream-payload-collector tests. Restored upstream semantics; kept the jsonLength OOM optimization as the only delta in this file. * refactor(compression): break down writeValue and writeEncodedString to pass complexity ratchets Refactors jsonSha256 internal helpers (writeValue, writeEncodedString) into small, single-responsibility sub-functions under the complexity threshold (max cyclomatic 15, max cognitive 15). Preserves exact JSON.stringify parity, circular reference guards on both arrays and plain objects, and escape behavior (all 530 relevant tests pass). * test(compression): make oom-memo heap assertion robust without expose-gc The CI unit-test shard runner does not pass --expose-gc, so global.gc is undefined and heapUsed can still momentarily hold GC-pending transients (observed 53.4 MiB after a 3MiB body). Gate the retained-heap assertion on forced collection being available (3 forced cycles for array buffers) instead of skipping it silently, and keep it fully active when --expose-gc is present. * fix(compression): restore worker-pool offload path in runCompressionAsync The OOM-mitigation refactor dropped the isCompressionWorkerEligible / runCompressionInWorker dispatch at the top of runCompressionAsync, silently removing the base's worker-thread offload for eligible large payloads. Restore the block exactly as on release/v3.8.51, ahead of the result-memo path, keeping the memoization and hashing improvements intact. * docs(api): document GET /api/monitoring/compression and log route errors via pino Add the new monitoring endpoint to docs/openapi.yaml following the neighboring System entries, and replace the route's console.error with the repo-standard pino logger. * fix(skills): regenerate omni-resilience and add changelog fragment Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Andrian Balanescu <AndrianBalanescu@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
87 lines
2.3 KiB
TypeScript
87 lines
2.3 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import { jsonSha256 } from "../../open-sse/utils/jsonHash.ts";
|
|
|
|
function sha256hex(text: string): string {
|
|
return crypto.createHash("sha256").update(text).digest("hex");
|
|
}
|
|
|
|
describe("jsonSha256 matches sha256hex(JSON.stringify(x)) for serializable values", () => {
|
|
const cases: Array<unknown> = [
|
|
null,
|
|
0,
|
|
1,
|
|
-1,
|
|
3.14159,
|
|
NaN,
|
|
Infinity,
|
|
-Infinity,
|
|
true,
|
|
false,
|
|
"",
|
|
"plain",
|
|
'with "quotes" and \\backslash',
|
|
"line\nbreak\ttab\rcr\bbs\fform",
|
|
"\u0000\u001f control chars",
|
|
"emoji 🚀 and surrogate \ud83d\ude00",
|
|
"unpaired \ud800 lone",
|
|
"mixed \ud83d\ude00\u0041\uD800X",
|
|
[],
|
|
[1, 2, 3],
|
|
[[1], [2], [3]],
|
|
[undefined, null, 1, "x"],
|
|
{},
|
|
{ a: 1, b: "two", c: [true, false] },
|
|
{ z: 1, a: 2, m: 3 }, // insertion order preserved
|
|
{ nested: { deep: { deeper: [{ ok: 1 }, null] } } },
|
|
{ fn: () => 1, ignored: undefined, kept: "x" }, // omitted keys
|
|
["http://x", { url: "http://y" }],
|
|
{
|
|
model: "gpt-4o",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "hi" },
|
|
{
|
|
type: "image_url",
|
|
image_url: { url: "data:image/png;base64," + "A".repeat(5_400_000) },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
// iBrowse MCP local-image shape with a large raw base64 payload.
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [{ type: "image", data: "A".repeat(5_400_000), mimeType: "image/png" }],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
for (const value of cases) {
|
|
const label =
|
|
typeof value === "string" && value.length > 40
|
|
? `string(${value.length})`
|
|
: JSON.stringify(value)?.slice(0, 50);
|
|
it(`matches for ${label}`, () => {
|
|
const expected = sha256hex(JSON.stringify(value));
|
|
assert.equal(jsonSha256(value), expected);
|
|
});
|
|
}
|
|
|
|
it("throws on BigInt like JSON.stringify", () => {
|
|
assert.throws(() => jsonSha256({ n: 1n }), TypeError);
|
|
});
|
|
|
|
it("throws on circular structures like JSON.stringify", () => {
|
|
const obj: Record<string, unknown> = { a: 1 };
|
|
obj.self = obj;
|
|
assert.throws(() => jsonSha256(obj), TypeError);
|
|
});
|
|
});
|