Skip to content

Add bounded batch PPS endpoint - #457

Open
w84april wants to merge 1 commit into
mainfrom
feat/batch-pps
Open

Add bounded batch PPS endpoint#457
w84april wants to merge 1 commit into
mainfrom
feat/batch-pps

Conversation

@w84april

@w84april w84april commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add a Redis-backed endpoint for fetching bounded PPS histories across multiple chains. This lets consumers request one shared date range for several vaults without downloading each vault’s complete timeline.

  • Validate shared date ranges and chain-qualified vault addresses
  • Preserve recent-over-historical precedence and predecessor anchors
  • Bound request and response sizes and document the contract

How to review

Review the implementation in this order:

  1. contract.ts — request validation, address normalization, and limits
  2. service.ts — Redis reads, historical/recent merging, and range selection
  3. route.ts — HTTP handling, CORS, and payload-size protection
  4. docs/rest.md — public request and response contract

The existing REST timeseries endpoint, GraphQL API, ingestion flow, and database schema are intentionally unchanged.

For a local smoke test:

curl -s http://localhost:3001/api/rest/timeseries/pps/v2 \
  -H 'Content-Type: application/json' \
  -d '{
    "start": 1785628800,
    "finish": 1785715200,
    "addresses": [
      "1:0x0000000000000000000000000000000000000000",
      "137:0x0123000000000000000000000000000000000123"
    ]
  }' | jq

Add a Redis-backed endpoint for fetching bounded PPS histories across multiple chains.

- Validate shared date ranges and chain-qualified vault addresses
- Preserve recent-over-historical precedence and predecessor anchors
- Bound request and response sizes and document the contract
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
kong Ready Ready Preview Aug 6, 2026 5:33pm

Request Review

@w84april
w84april marked this pull request as ready for review August 6, 2026 17:50
@w84april
w84april requested a review from matheus1lva August 6, 2026 17:50
!Number.isSafeInteger(chainId) ||
chainId <= 0 ||
chainId > MAX_CHAIN_ID ||
!ADDRESS_PATTERN.test(address)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}

export function parsePpsBatchRequest(input: unknown): ParsePpsBatchRequestResult {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt of using zod here? will make your life easier!

Comment on lines +27 to +30
const contentLength = Number(request.headers.get('content-length'))
if (Number.isFinite(contentLength) && contentLength > MAX_REQUEST_BYTES) {
return errorResponse('Request body is too large', 413)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe next already handles that for us? I dont remember if does.

@matheus1lva matheus1lva left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No linked issue; graded against the PR description. All described behaviors (validation, recent-over-historical precedence with predecessor anchor, size bounds, docs) are implemented.

Issues

1. One malformed cached row 500s the whole batchservice.ts:46
toPpsCandidate/toPpsPoint throw on a bad row inside mergeAndSlicePps; route.ts catches and returns 500 for all up-to-50 requested series. v1 tolerates the same data by filtering. If per-series blast radius matters, catch per address in readPpsBatch and return an empty array for the corrupt series; keep precedence and anchor semantics unchanged. Done when a batch with one malformed cached row still returns 200 with the valid series intact — or confirm fail-whole-batch is the intended contract (docs do document 500 on cache read failure).

2. 16KB request cap bypassable without content-lengthroute.ts:27
Number(request.headers.get('content-length')) is 0 for chunked/absent header, so the guard passes; request.text() then buffers the full body before the byte check. Stream with an incremental cap (abort past MAX_REQUEST_BYTES) or reject requests lacking a valid content-length. Done when an oversized chunked POST gets 413 without full buffering; valid <=16KB bodies still return 200.

3. Second Redis client instead of the shared oneservice.ts:12
Module-level createKeyv(process.env.REST_CACHE_REDIS_URL || ...) duplicates cache.ts's getKeyvClient() config; every other REST route reuses the shared client. Reuse it — but v2 relies on { throwOnErrors: true } for its 500 path, which the shared client doesn't set, so export a throwing variant deliberately rather than silently dropping error propagation. Keep the injectable cacheGet seam. Done when no createKeyv call remains under pps/v2 and Redis read failures still 500.

4. No tests despite explicit test seamsservice.ts:66
mergeAndSlicePps is exported and readPpsBatch takes injectable cacheGet, yet the PR ships zero specs; the package has vitest and the .spec.ts convention next door (cache.spec.ts). Add service.spec.ts + contract.spec.ts covering anchor at/before start, recent-over-historical tie-break, empty series, address dedup/normalization, range validation, and key ordering via stubbed cacheGet. Done when bun --filter web test exercises all three and passes.

Suggestions (non-blocking)

  • service.ts:26 — cached-row validation is split between toPpsCandidate and toPpsPoint; consolidating it in one place would make the throw paths obvious.
  • Coverage this review did not reach: the producer-to-consumer cache contract (whether refresh.ts/refresh-historical.ts writer output actually satisfies the new strict validators — pg numeric strings, day alignment); the cache-control: no-store divergence from v1's 900s public caching; the 4MB response cap firing only after the full Redis read + stringify; and docs/rest.md accuracy vs implementation (405 behavior, address dedup, exact-start anchor, no-store are undocumented).

How This Was Reviewed

Reviewed with the review-pr-workflow skill
5 review lenses, each finding independently verified by claude. 1 candidate finding was refuted and dropped.

@murderteeth murderteeth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking this one for now. As designed it creates a griefing vector. Lets discuss on telegram

Valid request parameters are effectively unlimited and cost the same regardless of range, making it possible to grief redis

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.

3 participants