Nobody has started this yet — be first.
Business impact
A short-lived link (a flash-sale link that expires in a few minutes, a one-time invite link) is, by construction, exactly the kind of link that gets hit repeatedly in a short window right before it expires -- that's what "short-lived and shared" traffic looks like. This bug means that once such a link has under a second left before expiresAt, every single GET /:code for it silently stops populating the Redis cache: no error, no log line, just a cache-aside path that quietly falls back to a Postgres query on every request instead of a Redis hit. For a link getting hammered in its final seconds -- exactly when a flash-sale link gets the most traffic, as people rush before it dies -- this turns what should be near-zero-latency cache hits into a Postgres query storm right at the worst possible moment.
Problem
setCachedLink(shortCode, { longUrl, expiresAt }) in src/lib/cache.js computes how many whole seconds remain until expiresAt and uses that as the Redis EX TTL (clamped to the default TTL). When less than 1000ms remain, that computation now rounds DOWN to 0 -- and the function then takes its "already expired, don't bother caching" early-return path even though the link has NOT expired yet; it just has a fraction of a second left.
Current behavior
Calling setCachedLink for a link expiring 500ms in the future results in getCachedLink returning null immediately afterward -- it was never cached at all.
Expected behavior
Any link with a strictly positive amount of time remaining until expiresAt -- even 200ms -- should still get cached, with a TTL of at least 1 whole second. The code's own comment already explains why: rounding down to 0 for a sub-second-away expiry would mean "don't bother caching" for every soon-to-expire link, defeating the cache for exactly the links a client is most likely to hit again soon.
Steps to reproduce
// From a Node REPL or script with DATABASE_URL / REDIS_URL set: const { setCachedLink, getCachedLink } = require('./src/lib/cache');
await setCachedLink('demoCode', { longUrl: 'https://example.com/almost-gone', expiresAt: new Date(Date.now() + 500), // 500ms from now -- not expired });
console.log(await getCachedLink('demoCode')); // Expected: { longUrl: 'https://example.com/almost-gone', expiresAt: ... } // Actual: null -- it was never cached at all
Why this matters
The comment directly above the secondsUntilExpiry calculation in src/lib/cache.js already describes, in detail, exactly the failure mode this bug reintroduces, and explains which rounding direction avoids it. The code and the comment above it currently disagree with each other: one rounding function keeps the guarantee the comment describes, the other breaks it. That mismatch is the whole bug -- the fix is a single function call, not new logic.
Suggested approach
Find the secondsUntilExpiry line in setCachedLink (src/lib/cache.js). Compare what it currently does against what its own comment says it should do, and correct the one thing that's out of sync.
Acceptance criteria
Verification
npx jest --config practice-tickets/jest.config.js practice-tickets/tests/03-cache-ttl-rounds-down.test.js
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/cache-ttl-rounds-downFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/cache-ttl-rounds-downSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inOr run the whole stack in Docker instead of the steps above -- docker compose up --build (dev, hot-reloaded via a bind-mounted src/) or docker compose -f docker-compose.prod.yml up --build (prod-style multi-stage build, non-root user). Either way the app container runs prisma migrate deploy on startup, so no separate migration step is needed. Host ports are non-default -- app 3003, Postgres 5436, Redis 6380 -- to avoid clashing with sibling projects in this repo.
The project's own real test suite (npm test, Jest + Supertest against real Postgres/Redis) is separate from the practice tickets below -- run it any time to confirm you haven't broken anything already-working.
Each ticket in practice-tickets/tickets/ (01 through 07) names a dedicated test under practice-tickets/tests/, run via its own Jest project (practice-tickets/jest.config.js, excluded from plain npm test):
# a single ticket
npx jest --config practice-tickets/jest.config.js practice-tickets/tests/01-health-check-false-positive.test.js
# all 7, one at a time, with a pass/fail summary
./practice-tickets/run.sh
Fixing tickets 01, 03, 04, and 07 also turns several pre-existing failures in the real suite (tests/health.test.js, tests/redirect.test.js, tests/concurrency.test.js) back to green -- that's expected, not a coincidence, since the injected bugs live in shared code that suite also exercises. tests/concurrency.test.js specifically fails for two unrelated reasons at once (tickets 04 and 07 both touch code paths it exercises), so fixing only one of the two will not turn it green.
Level 1
Fix a bug
Read existing behaviour, correct it.