Files
ihasmail-inbuxa/server/src/compress.test.ts
T
jcoffey-dev 01f721d8d1 Cut what a signed-in tab costs by two thirds
Two changes on the push path, both measured against a real Stalwart 0.16.20
with the container capped at 256 MiB and tabs added in steps of 200 until the
kernel killed it:

                                  tabs held   per tab   of which native
  before                              1,665   133 KiB          81 KiB
  pin upstream calls to STALWART_URL  3,400    58 KiB           8 KiB
  + raw push relay                    4,979    37 KiB          10 KiB

Stalwart advertises absolute https URLs in every session, and the proxy
followed them -- so even with STALWART_URL naming a private plain-HTTP hop on
the same Docker network, every held push stream went out through TLS. That leg
is about 80 KiB of OpenSSL state per tab: native memory Node cannot see, which
is why neither the heap ceiling nor the stream buffer size ever moved the
number. absoluteUpstream() now keeps the path and query from the advertised
URL and the scheme, host and port from the configured one. A setup that must
reach Stalwart at an origin other than the one it was given sets
STALWART_FOLLOW_ADVERTISED_URLS=1.

With the transport out of the way, the fetch()-based relay was the next cost:
an undici Response, a web ReadableStream, a reader and Hono's stream bridge
held alive per tab, about 44 KiB of heap for a session that otherwise costs
4 KiB. relayPushRaw() pipes the upstream socket into the Node response and
tells the adapter the response is already sent. RAW_PUSH_RELAY=0 restores the
fetch path for comparison.

JMAP throughput is unchanged (2,383/s against 2,484/s at 50 users, inside
run-to-run noise); the relay does not touch that path. Verified that a push
stream through the raw relay delivers a StateChange while mail is written.

The install page's advice to set --max-old-space-size was measured in the same
runs and made no difference at all -- 3,400 tabs with it and without -- and
is withdrawn in the docs alongside this change.
2026-09-06 00:42:19 -07:00

85 lines
4.3 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
/*
* A static root of our own, built before the app is imported.
*
* CI runs `npm test` before `npm run build`, so `web/dist` does not exist when
* these run: pointing at it would serve the "web build not found" fallback,
* which is short, plain text and rightly uncompressed. That failure looked
* exactly like compression being broken.
*/
const root = mkdtempSync(join(tmpdir(), "ihasmail-compress-"));
mkdirSync(join(root, "assets"));
const script = `/* ${"x".repeat(40_000)} */\n`;
writeFileSync(join(root, "assets", "app.js"), script);
writeFileSync(join(root, "index.html"), `<!doctype html><title>t</title>${"<p>hello</p>".repeat(400)}`);
process.env.STATIC_DIR = root;
process.env.STALWART_URL = "http://127.0.0.1:1";
const { createApp } = await import("./app.js");
test("an asset is gzipped when the client asks for it", async () => {
const res = await createApp().request("/assets/app.js", { headers: { "accept-encoding": "gzip" } });
assert.equal(res.status, 200);
assert.equal(res.headers.get("content-encoding"), "gzip");
assert.match(res.headers.get("vary") ?? "", /accept-encoding/i);
});
test("a client that does not ask for gzip does not get it", async () => {
const res = await createApp().request("/assets/app.js", { headers: { "accept-encoding": "identity" } });
assert.equal(res.status, 200);
assert.equal(res.headers.get("content-encoding"), null);
});
test("gzip actually makes the asset smaller", async () => {
const plain = await (await createApp().request("/assets/app.js", { headers: { "accept-encoding": "identity" } })).arrayBuffer();
const gz = await (await createApp().request("/assets/app.js", { headers: { "accept-encoding": "gzip" } })).arrayBuffer();
assert.ok(gz.byteLength < plain.byteLength / 2, `${gz.byteLength} should be well under ${plain.byteLength}`);
});
test("a gzipped response decodes to the bytes we would have sent plain", async () => {
const plain = await (await createApp().request("/assets/app.js", { headers: { "accept-encoding": "identity" } })).arrayBuffer();
const res = await createApp().request("/assets/app.js", { headers: { "accept-encoding": "gzip" } });
const decoded = await new Response(res.body!.pipeThrough(new DecompressionStream("gzip"))).arrayBuffer();
assert.deepEqual(Buffer.from(decoded), Buffer.from(plain));
});
test("the app shell is gzipped", async () => {
const res = await createApp().request("/", { headers: { "accept-encoding": "gzip" } });
assert.equal(res.status, 200);
assert.equal(res.headers.get("content-encoding"), "gzip");
});
test("proxy routes that forward upstream bytes are never compressed", async () => {
// Unauthenticated, so these stop at 401 -- enough to prove the middleware
// declines the path, which is what issue #76 was about.
const app = createApp();
for (const path of ["/api/blob/a/b/c.pdf", "/api/image?url=https://example.com/x.png", "/api/ics?url=https://example.com/x.ics"]) {
const res = await app.request(path, { headers: { "accept-encoding": "gzip" } });
assert.equal(res.headers.get("content-encoding"), null, `${path} must not be compressed`);
}
});
test("the push stream is never compressed", async () => {
const res = await createApp().request("/api/events", { headers: { "accept-encoding": "gzip" } });
assert.equal(res.headers.get("content-encoding"), null);
});
test("the liveness probe is not compressed, since gzip would make it bigger", async () => {
const res = await createApp().request("/api/health", { headers: { "accept-encoding": "gzip" } });
assert.equal(res.status, 200);
assert.equal(res.headers.get("content-encoding"), null);
});
test("advertised upstream URLs are pinned to the configured origin", async () => {
const { absoluteUpstream } = await import("./upstream.js");
const pinned = absoluteUpstream("https://mail.public.example/jmap/eventsource/?types=*", "http://stalwart:8080");
assert.equal(pinned, "http://stalwart:8080/jmap/eventsource/?types=*");
// A relative URL still resolves against the base, as before.
assert.equal(absoluteUpstream("/jmap/", "http://stalwart:8080/"), "http://stalwart:8080/jmap/");
});