mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
* KV-optional: in-process backend when KV_URL unset Make dataroom run WITH and WITHOUT external Hanzo KV, uniform with commerce infra/kv.go. KV_URL unset -> in-process MemoryKV (single-replica correct: cache, rate-limit, tus upload locks, export/download job stores, digest queues all work with no external datastore). KV_URL set -> external Hanzo KV over ioredis (multi-replica HA). Malformed KV_URL fails CLOSED (throws) -- never a silent in-process fallback. - lib/kv/url.ts: resolveKvUrl -- fail-closed parse, kv:// brand scheme -> redis:// wire scheme, password redaction in errors. - lib/kv/memory-kv.ts: in-process ioredis-compatible store (strings+TTL, zset, set, hash, list, pipeline) -- the exact surface dataroom uses. - lib/kv/select.ts: the one backend selector (mirrors NewKVClient/FromURL). - lib/redis.ts: use createKvClient() for redis + lockerRedisClient; drop the redis://localhost:6379 silent default (the WITHOUT-KV break) and vestigial REDIS_URL; Upstash-compat shims unchanged. - Tests (node --test, dep-free): 25 pass proving selection + in-process correctness incl NX lock mutual-exclusion, TTL eviction, zset ordering, pipeline shape, fail-closed URL, cross-instance isolation (multi-replica split-brain contract). * kv: warn once when falling back to in-process (non-silent split-brain) Emit a one-time startup warning when KV_URL is unset so the WITHOUT-KV mode is never silent — a multi-replica deploy with KV_URL unset splits sessions, rate-limit windows and tus locks across replicas (single-replica only). --------- Co-authored-by: Hanzo Dev <[email protected]>
55 lines
2.0 KiB
TypeScript
55 lines
2.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
|
|
/**
|
|
* select.ts statically imports ioredis (a production dependency, parity with the
|
|
* original lib/redis.ts). In a bare checkout without node_modules the import
|
|
* can't resolve, so we dynamic-import and SKIP — the selection logic itself is
|
|
* proven dep-free by url.test.ts (fail-closed + normalization) and memory-kv.test.ts
|
|
* (in-process semantics). Where ioredis is installed (CI, image), these run.
|
|
*/
|
|
let createKvClient: (() => any) | undefined;
|
|
try {
|
|
({ createKvClient } = await import("./select.ts"));
|
|
} catch {
|
|
// ioredis not installed in this checkout — tests below skip.
|
|
}
|
|
const ready = createKvClient !== undefined;
|
|
|
|
function withEnv(value: string | undefined, fn: () => void | Promise<void>) {
|
|
const prev = process.env.KV_URL;
|
|
if (value === undefined) delete process.env.KV_URL;
|
|
else process.env.KV_URL = value;
|
|
const restore = () => {
|
|
if (prev === undefined) delete process.env.KV_URL;
|
|
else process.env.KV_URL = prev;
|
|
};
|
|
return Promise.resolve()
|
|
.then(fn)
|
|
.finally(restore);
|
|
}
|
|
|
|
test("KV_URL unset ⇒ in-process backend, fully functional", { skip: !ready }, async () => {
|
|
await withEnv(undefined, async () => {
|
|
const c = createKvClient!();
|
|
assert.equal(c.status, "ready"); // MemoryKV lifecycle stub — no connection
|
|
assert.equal(await c.set("k", "v"), "OK");
|
|
assert.equal(await c.get("k"), "v");
|
|
});
|
|
});
|
|
|
|
test("KV_URL malformed ⇒ THROWS (fail closed, no silent fallback)", { skip: !ready }, async () => {
|
|
await withEnv("http://:pw@hanzo-kv:6379", () => {
|
|
assert.throws(() => createKvClient!());
|
|
});
|
|
});
|
|
|
|
test("KV_URL set (kv://) ⇒ external ioredis client, lazy (does not connect)", { skip: !ready }, async () => {
|
|
await withEnv("kv://hanzo-kv:6379", () => {
|
|
const c = createKvClient!();
|
|
// ioredis with lazyConnect stays 'wait' until first command; MemoryKV is 'ready'.
|
|
assert.notEqual(c.status, "ready");
|
|
if (typeof c.disconnect === "function") c.disconnect();
|
|
});
|
|
});
|