Skip to content

feat: valkey support and database request optimization modes - #118

Merged
scolastico merged 13 commits into
mainfrom
feat/valkey-request-optimization
Jul 30, 2026
Merged

feat: valkey support and database request optimization modes#118
scolastico merged 13 commits into
mainfrom
feat/valkey-request-optimization

Conversation

@scolastico

@scolastico scolastico commented Jul 30, 2026

Copy link
Copy Markdown
Member

Adds optional valkey support (shared cache + write buffer) and a DATABASE_REQUEST_OPTIMIZATION mode (none/light/hard) so a serverless Postgres (e.g. neon.tech) can go to sleep between bursts of traffic.

Implements plans/2026-07-30-valkey-request-optimization.md.

What's new

  • VALKEY_ENABLED=true backs the existing cache-manager cache with valkey, so it is shared between instances and survives restarts. Independent of the optimization mode.
  • DATABASE_REQUEST_OPTIMIZATION:
    • none (default) — unchanged behavior.
    • light — longer cache TTLs, cleanup schedulers throttled to every 15 minutes.
    • hard (requires valkey) — rate limit tracking (requests/tokens/bans) moves fully into valkey, and new notes are buffered in valkey and written to the database in batches (interval or queue-size triggered).
  • New ValkeyConfig section (VALKEY_*) with full JSDoc, so the typedoc config docs pick it up.
  • docker-compose.valkey.yml deployment example + README section.

Default behavior is unchanged: with VALKEY_ENABLED=false and DATABASE_REQUEST_OPTIMIZATION=none nothing about the current code paths changes. All mode branching lives inside DatabaseService — controllers and guards are untouched.

Security & robustness fixes

Closes CodeQL alert #1 (js/type-confusion-through-parameter-tampering, critical) and several crashes of the same class found while smoke testing. All are pre-existing and reproducible on main, unrelated to valkey.

  • Decryption key length limit was bypassable (fetch.controller.ts). A repeated query parameter (?key=<33 chars>&key=y) makes express deliver an array, so key.length counted elements (2) instead of characters and the 32-character guard passed. The key now has to be a string, sent exactly once, or the request is rejected with 400.
  • GET /note/:id/decrypt without a key and POST /note/:id/decrypt without a body returned 500 (dereferencing undefined). Both now return 400.
  • DELETE /note/:id without a body returned 500, even though the body is documented as optional when deleting from the creator IP. Now 204 / 401 as documented.
  • POST /note/text without a body returned 500. Now 400 "The note must not be empty".
  • GET /file/:id?json=a&json=b returned 500 on .toLowerCase() of an array. Now falls back to the redirect.

Checked and left alone: the length, part and stats password parameters all fail closed on array input (Number([...])NaN → 400, and a strict !== against an array rejects), so they are not affected.

Note one intentional behaviour change: POST /note/text with an empty body now returns 400 instead of creating an empty note. The endpoint already documented that 400, but the cost === 0 check could never fire because the LIMITS_MIN_TOKENS_PER_CREATE floor (default 1000) was applied first.

Graceful shutdown

SIGTERM — what docker stop sends — previously called process.exit(0) immediately without closing the app, so in hard mode the buffered notes were not flushed. Both signals now close the nest app (with a 5s force-exit fallback so a hanging shutdown cannot wedge the container).

This also required moving the valkey disconnect from onModuleDestroy to onApplicationShutdown: nest tore the valkey client down before DatabaseService got to flush, so the flush failed with Connection is closed. Verified against a real container — before the fix the buffer survived SIGTERM untouched, after it the note is flushed (Flushed 1 notes), the pending hash drops to 0, and the note is readable from the database after a restart.

Verification

  • pnpm test — 14 suites, 105 tests passing; pnpm lint and pnpm build clean.
  • Smoke tested against a real valkey/valkey:8 container:
    • valkey-backed cache: not3:cache:stats appears after hitting /stats.
    • hard mode: create → buffered in valkey, DB untouched; read served from the buffer; queue-size trigger fires the cron flush → rows land in the DB; read after flush served from the DB; delete after flush queued and applied on the next flush (Flushed 0 notes and 1 deletes); delete before flush removed straight from the buffer, never touching the DB.
    • shutdown: SIGTERM in hard mode flushes and exits 0 in ~20ms; default (no valkey) mode shuts down cleanly with no errors.

@scolastico
scolastico merged commit ef1c0a0 into main Jul 30, 2026
5 checks passed
@scolastico
scolastico deleted the feat/valkey-request-optimization branch July 30, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant