Skip to content

Comments

fix: add pagination cursor and totalCount to cyber threats response#237

Open
haosenwang1018 wants to merge 3 commits intokoala73:mainfrom
haosenwang1018:fix/cyber-pagination-cursor
Open

fix: add pagination cursor and totalCount to cyber threats response#237
haosenwang1018 wants to merge 3 commits intokoala73:mainfrom
haosenwang1018:fix/cyber-pagination-cursor

Conversation

@haosenwang1018
Copy link
Contributor

Problem

listCyberThreats returns pagination: undefined. Clients cannot implement 'next page' semantics — there's no cursor or total count to know if more results exist.

Fix

  • Parse req.pagination.cursor as an offset into the sorted/filtered results
  • Return totalCount (total filtered results before paging)
  • Return nextCursor (offset for next page, empty string when exhausted)
  • Remove the premature slice(0, pageSize) before filtering — now applied after all filters

Ref: PR #106 re-review (NEW-7)
Fixes #202

The previous 0.5° rounding (~50km radius) merged distinct events in
dense urban areas (e.g. Manhattan vs Brooklyn protests on the same
day). Reducing to 0.1° (~10km) preserves neighborhood-level
granularity while still deduplicating true duplicates.

Fixes koala73#204

Signed-off-by: haosenwang1018 <haosenwang1018@users.noreply.github.com>
Replace Math.random() with a djb2 hash seeded by the threat ID,
so the same threat always appears at the same coordinates. This
prevents 'jumping' markers on the map when the same data is
re-fetched.

Fixes koala73#203

Signed-off-by: haosenwang1018 <haosenwang1018@users.noreply.github.com>
The handler previously returned pagination: undefined, making it
impossible for clients to paginate. Now parses cursor as an offset,
returns totalCount of filtered results, and nextCursor for the
next page (empty string when no more results).

Fixes koala73#202

Signed-off-by: haosenwang1018 <haosenwang1018@users.noreply.github.com>
@vercel
Copy link

vercel bot commented Feb 23, 2026

@haosenwang1018 is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

@koala73
Copy link
Owner

koala73 commented Feb 23, 2026

Thanks again @haosenwang1018 much appreciated! Pls see below, the core pagination is on point - but it introduces a few other issues that you should handle

PR Review: fix: add pagination cursor and totalCount to cyber threats response

PR #237 by haosenwang1018 | 3 commits | +30/-11 across 3 files

Summary

The PR addresses the listCyberThreats endpoint returning undefined pagination data. It:

  1. Adds offset-based pagination (parsing cursor as offset, returning totalCount + nextCursor)
  2. Replaces Math.random() jitter with a deterministic hashJitter function for country centroid fallback
  3. Increases dedup grid precision in unrest module from 0.5 degrees to 0.1 degrees

Issues Found

1. Removing Redis caching is a significant performance regression (High)

The PR removes the entire Redis caching layer (list-cyber-threats.ts lines 8-9, 31-32, 42-44, 114-116 on main). The old code cached responses for 15 minutes, shielding upstream APIs from repeated hits. Now every client request triggers 5 upstream API fetches + GeoIP hydration (up to 250 concurrent IP lookups).

The caching was likely removed because the old cache key didn't account for pagination offsets. The correct fix is to cache the full sorted/filtered result set (keyed by filters only, pre-pagination) and apply offset/pageSize slicing after the cache lookup:

const cacheKey = `${REDIS_CACHE_KEY}:${req.timeRange?.start || 0}:${req.type || ''}:...`;
const cached = await getCachedJson(cacheKey) as CyberThreat[] | null;
if (cached?.length) {
  const paged = cached.slice(offset, offset + pageSize);
  return { threats: paged, pagination: { totalCount: cached.length, nextCursor: ... } };
}
// ... fetch, process, cache full result set, then paginate ...

2. Negative cursor offset not guarded (Medium) — list-cyber-threats.ts:36

const offset = req.pagination?.cursor ? parseInt(req.pagination.cursor, 10) || 0 : 0;

parseInt("-5", 10) || 0 evaluates to -5, and results.slice(-5, ...) reads from the end of the array in JavaScript — producing unexpected results.

Fix: Math.max(0, req.pagination?.cursor ? parseInt(req.pagination.cursor, 10) || 0 : 0)

3. Pagination instability across requests (Low / Design)

Since the entire dataset is re-fetched from upstream on every request (especially now without caching), the data between page 1 and page 2 calls can differ entirely. The offset-based cursor assumes a stable result set. Items may be skipped or duplicated across pages. This is inherent to offset pagination over live data — acceptable for a threat feed, but worth documenting for API consumers.

4. hashJitter JSDoc is inaccurate (Nit) — _shared.ts:196

The comment says "returns a float in [-0.5, 0.5)" but the * 2 on the return line produces [-1.0, 1.0]. The code behavior is correct (matches the old (Math.random() - 0.5) * 2); only the comment is wrong.

5. Unrelated dedup change bundled in PR (Nit) — unrest/v1/_shared.ts:76-77

The change from Math.round(lat * 2) / 2 to Math.round(lat * 10) / 10 changes the dedup grid from ~55km to ~11km — a 5x increase in precision that will produce significantly more unique events. This is a meaningful behavioral change unrelated to cyber pagination and should be in its own PR.

What looks good

  • The pagination logic itself (offset extraction, totalCount, nextCursor calculation, slicing after filtering/sorting) is correct
  • The deterministic hashJitter function is a solid replacement for Math.random() — same threat always maps to the same coordinates now
  • Passing threat.id as seed to getCountryCentroid is the right approach for per-threat determinism
  • Error responses now return a proper pagination structure instead of undefined

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add pagination cursor to cyber threats response

2 participants