Skip to content

feat: PR-3 add Discord API layer with rate limiting and pagination#3

Merged
mynameistito merged 8 commits into
mainfrom
init/03-discord-api
Mar 26, 2026
Merged

feat: PR-3 add Discord API layer with rate limiting and pagination#3
mynameistito merged 8 commits into
mainfrom
init/03-discord-api

Conversation

@mynameistito
Copy link
Copy Markdown
Owner

@mynameistito mynameistito commented Mar 25, 2026

Summary

  • Add Zod schemas for Discord API contracts (users, messages, embeds, search)
  • Add HTTP client with automatic rate limit handling, exponential backoff, and retry logic
  • Add paginated search with snowflake-based partitioning for large datasets

Stacked PR Chain

This is PR 3 of 7 — merge in order.
main ← PR1 ← PR2 ← PR3 ← PR4 ← PR5 ← PR6 ← PR7

Test plan

  • bun run typecheck passes
  • bun run check passes linting
  • Rate limit headers are properly tracked
  • 429/202 responses trigger appropriate retry logic

Summary by cubic

Adds a typed Discord API layer with rate limiting, retries, and paginated search. Enables reliable large-scale message fetching with automatic backoff and schema validation.

  • New Features
    • Strong zod schemas for users, messages, embeds, search responses, and params.
    • discordFetch client with rate-limit tracking, 429 retry with backoff, and 202 “index not ready” polling.
    • searchMessages and searchAllMessages with snowflake-based partitioning, offset/maxId support, and optional progress/page callbacks; caps page size to 25.
    • Result-based error handling via better-result with typed errors: DiscordApiError, RateLimitExhaustedError, IndexNotReadyError, ValidationError.

Written for commit 528f771. Summary will update on new commits.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added Discord REST client with rate-limit and retry handling, Zod schemas for Discord payloads and search params, and paginated guild message search orchestration with progress/page callbacks; also added *.local.json to .gitignore.

Changes

Cohort / File(s) Summary
Configuration
\.gitignore
Added *.local.json to ignore locally-scoped JSON configuration files.
Discord REST client
src/discord/client.ts
New discordFetch<T> exported. Implements authenticated fetch with abort timeout, process-wide per-(path,token) rate-limit state, waitForRateLimit(), 429 exponential backoff with jitter, 202 polling, JSON parse + Zod validation, and structured error union (DiscordApiError, RateLimitExhaustedError, IndexNotReadyError, ValidationError).
Discord schemas
src/discord/schemas.ts
New Zod schemas and exported TS types for embeds, users, attachments, messages, threads, members, search responses, index-not-ready responses, and search parameters; exports MAX_OFFSET and MAX_PAGE_SIZE.
Search orchestration
src/discord/search.ts
New searchMessages and searchAllMessages exported. Validates SearchParamsSchema, builds query strings with repeated params, performs paginated/partitioned retrieval using discordFetch, invokes optional onPage/onProgress callbacks, and aggregates/terminates per total/empty/partition-progression rules.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SearchAll as searchAllMessages
    participant SearchOne as searchMessages
    participant RateLimit as RateLimitMgr
    participant DiscordFetch as discordFetch
    participant DiscordAPI as Discord API

    Client->>SearchAll: start(params, token, callbacks)
    SearchAll->>SearchOne: request page(offset, maxId)
    SearchOne->>RateLimit: waitForRateLimit(path, token)
    RateLimit-->>SearchOne: permit / delay
    SearchOne->>DiscordFetch: call(path, schema, token)
    DiscordFetch->>DiscordAPI: HTTP request (Authorization + timeout)
    DiscordAPI-->>DiscordFetch: response (status, headers, body)
    alt 429 Too Many Requests
        DiscordFetch->>RateLimit: update bucket from headers
        DiscordFetch->>DiscordFetch: backoff + jitter, retry up to maxRetries429
    else 202 Accepted
        DiscordFetch->>DiscordFetch: parse retry_after, wait, retry up to maxRetries202
    else 2xx OK
        DiscordFetch->>DiscordFetch: parse JSON & validate with Zod
        DiscordFetch-->>SearchOne: Ok(SearchResponse)
    else other error
        DiscordFetch-->>SearchOne: Err(DiscordFetchError)
    end
    SearchOne-->>SearchAll: page result
    SearchAll->>Client: onPage / onProgress (progress updates)
    loop pages/partitions
        SearchAll->>SearchOne: next page (advance offset/maxId)
    end
    SearchAll-->>Client: accumulated messages or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through headers, retries, and queues,
Backoff and jitter in careful news,
Pages gathered, callbacks in sight,
Zod keeps payloads tidy and right,
A rabbit cheered the search's flight.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: adding a Discord API layer with rate limiting and pagination support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description is directly related to the changeset, clearly describing the addition of Discord API schemas, HTTP client with rate limiting, and paginated search functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch init/03-discord-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/discord/search.ts">

<violation number="1" location="src/discord/search.ts:130">
P1: `searchAllMessages` can terminate after one page when `params.offset > 0` because `totalResults` is never initialized for non-zero starting offsets.</violation>
</file>

<file name="src/discord/client.ts">

<violation number="1" location="src/discord/client.ts:150">
P1: Error responses during 202 polling are not handled correctly. If a retry request returns 429 or other error status, it's passed to `parseResponse` which expects a success body, causing a confusing `ValidationError` instead of proper error handling. Check `!retryResponse.ok` before calling `parseResponse`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@mynameistito mynameistito changed the title feat: add Discord API layer with rate limiting and pagination feat: PR-3 add Discord API layer with rate limiting and pagination Mar 25, 2026
@mynameistito mynameistito changed the base branch from init/02-core-types to main March 26, 2026 03:50
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/paths.ts">

<violation number="1" location="src/paths.ts:15">
P2: Handle only ENOENT here and create `.gitignore` atomically; broad catch + default write mode can overwrite unexpectedly.</violation>
</file>

<file name="src/discord/client.ts">

<violation number="1" location="src/discord/client.ts:169">
P2: 429 retries inside the 202 polling loop never reach the exhaustion branch because a 0-based attempt index is passed to `handle429`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config.ts`:
- Around line 21-33: readSettingsFile currently calls JSON.parse(text) which
will throw on malformed JSON and leak to callers (affecting loadConfig and
saveSettings); update readSettingsFile to catch JSON parse errors around
JSON.parse(text) (or wrap the parse + schema validation in a try/catch) and
return {} when parsing fails so callers like loadConfig and saveSettings get a
consistent empty settings result rather than an exception; reference the
readSettingsFile function and ensure behavior aligns with
SettingsJsonSchema.safeParse usage so malformed JSON yields {} instead of
throwing.

In `@src/discord/client.ts`:
- Around line 154-156: The loop currently reuses the initial retryAfter for
every 202 retry; update the retry loop in the function containing the for (let
attempt = 0; attempt < maxRetries; attempt++) { ... } block to re-fetch and
re-parse the 202 response on each iteration, extract any updated retry_after
value and update the retryAfter used for the next sleep call (and/or call
waitForRateLimit with the new value) before awaiting sleep; also stop retrying
if the re-parsed response is no longer a 202. Reference: retryAfter,
waitForRateLimit, and the for (attempt...) retry loop.
- Around line 166-176: The 429 handling inside the 202 polling loop uses the
loop's `attempt` counter, conflating 202-poll retries with the 429 retry budget;
introduce a dedicated 429 retry counter (e.g., `attempt429` or `retry429`) local
to the 202 polling context and pass that to `handle429` instead of the outer
`attempt`, incrementing/resetting it only when a 429 is encountered and
respecting the same `maxRetries` logic inside this context; update calls and any
branching that relies on the returned `rateLimitResult` from `handle429` so the
outer 202 `attempt` remains unchanged by 429 events.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 399cc65e-5328-419f-8bca-93b89eea0cdb

📥 Commits

Reviewing files that changed from the base of the PR and between c28c60f and 6df786e.

📒 Files selected for processing (8)
  • .gitignore
  • src/config.ts
  • src/discord/client.ts
  • src/discord/schemas.ts
  • src/discord/search.ts
  • src/errors.ts
  • src/paths.ts
  • src/types.ts

Add Zod schemas for Discord API contracts, HTTP client with automatic
rate limit handling and exponential backoff, and paginated search with
snowflake-based partitioning.
Repository owner deleted a comment from cubic-dev-ai Bot Mar 26, 2026
Repository owner deleted a comment from coderabbitai Bot Mar 26, 2026
Repository owner deleted a comment from coderabbitai Bot Mar 26, 2026
Repository owner deleted a comment from cubic-dev-ai Bot Mar 26, 2026
Repository owner deleted a comment from cubic-dev-ai Bot Mar 26, 2026
Repository owner deleted a comment from coderabbitai Bot Mar 26, 2026
Repository owner deleted a comment from cubic-dev-ai Bot Mar 26, 2026
@mynameistito
Copy link
Copy Markdown
Owner Author

@greptile

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 26, 2026

Greptile Summary

This PR introduces a typed Discord API layer with automatic rate limiting, retry logic, and snowflake-partitioned pagination for large-scale message fetching. The architecture is clean and the use of better-result for typed error handling is well-executed — two logic bugs need to be addressed before merging.

Key changes:

  • src/discord/schemas.ts — Zod schemas for users, messages, embeds, search params/responses; no issues.
  • src/discord/client.tsdiscordFetch with module-level rate-limit state, 429 backoff, and 202 polling. Bug: during 202 polling, any non-202 response (including 429 or 5xx) is unconditionally passed to parseResponse rather than checking response.ok first, causing error responses to surface as misleading ValidationErrors.
  • src/discord/search.tssearchMessages / searchAllMessages with snowflake partitioning. Bug: state.totalResults is only initialised when offset === 0 && !maxId; if a caller supplies params.offset > 0, totalResults stays 0, and the outer partitioning loop terminates after the first partition due to allMessages.length >= 0 always being true.
  • .gitignore — adds *.local.json; no issues.

Confidence Score: 3/5

Two P1 logic bugs must be fixed before merge: 202 polling mishandles error responses, and non-zero starting offsets silently break snowflake partitioning.

The overall design is solid and the schemas are correct, but both client.ts and search.ts have correctness bugs that would cause incorrect runtime behavior in plausible real-world usage (rate-limited polling, resumable/offset-started fetches). Neither is a security issue, but they would silently produce wrong results or misleading errors.

src/discord/client.ts (202 polling error handling) and src/discord/search.ts (totalResults initialisation guard)

Important Files Changed

Filename Overview
src/discord/client.ts New HTTP client with rate-limit tracking, 429 retry, and 202 polling — one P1 bug: non-OK responses during 202 polling are passed to parseResponse instead of handleErrorResponse, producing misleading ValidationErrors.
src/discord/schemas.ts Zod schemas for Discord API contracts (users, messages, embeds, search) — well-structured and comprehensive; no issues found.
src/discord/search.ts Paginated search with snowflake-based partitioning — one P1 bug: state.totalResults is only set when offset===0, causing premature loop termination when params.offset>0 is passed to searchAllMessages.
.gitignore Adds *.local.json to .gitignore — no issues.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant discordFetch
    participant handle429
    participant handle202
    participant Discord API

    Caller->>discordFetch: discordFetch(path, schema, token)
    loop attempt 0..maxRetries429
        discordFetch->>discordFetch: waitForRateLimit()
        discordFetch->>Discord API: GET request
        Discord API-->>discordFetch: response

        alt status 429
            discordFetch->>handle429: handle429(response, attempt, max)
            handle429->>handle429: parse retry_after
            alt attempt >= maxRetries
                handle429-->>discordFetch: Err(RateLimitExhaustedError)
                discordFetch-->>Caller: Err(RateLimitExhaustedError)
            else
                handle429->>handle429: sleep(retry_after * 1000)
                handle429-->>discordFetch: Ok(retry)
            end
        else status 202 (index not ready)
            discordFetch->>handle202: handle202(response, url, token, schema, maxRetries202)
            loop attempt 0..maxRetries202
                handle202->>handle202: sleep(retry_after * 1000)
                handle202->>Discord API: GET request (retry)
                Discord API-->>handle202: response
                alt status != 202
                    Note over handle202: Bug: skips ok check, calls parseResponse for 429/5xx too
                    handle202->>handle202: parseResponse(response, schema)
                    handle202-->>discordFetch: Ok(data) or Err(ValidationError)
                end
            end
            handle202-->>discordFetch: Err(IndexNotReadyError)
        else not ok (4xx/5xx)
            discordFetch->>discordFetch: handleErrorResponse(response)
            discordFetch-->>Caller: Err(DiscordApiError)
        else ok (200)
            discordFetch->>discordFetch: parseResponse(response, schema)
            discordFetch-->>Caller: Ok(data)
        end
    end
Loading

Reviews (1): Last reviewed commit: "fix: ZodError issues type and gitignore ..." | Re-trigger Greptile

Comment thread src/discord/client.ts Outdated
Comment thread src/discord/search.ts Outdated
Comment thread src/discord/client.ts
…gination

- Route handle202 retries through full status decision tree (429, error, OK)
  and re-read retry_after from each 202 body instead of reusing initial delay
- Add Zod validation to 429 response body and exponential backoff with jitter
- Extract magic numbers into named constants (retries, delays, page size, offset)
- Add AbortController timeout (30s) to fetchWithAuth for stalled connections
- Constrain limit, offset, and slop to valid integer ranges in SearchParamsSchema
- Define explicit ThreadSchema/MemberSchema replacing z.unknown() in search response
- Replace spread accumulator with Array.prototype.push.apply
- Change ProgressCallback to accept SearchProgress object
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/discord/client.ts`:
- Around line 187-196: The 429 handling inside the 202 polling loop is
incorrectly sharing the loop's `attempt` counter, causing rate-limit retries to
consume the 202 retry budget; change the logic so `handle429` uses its own
independent retry counter (e.g., `rateLimitAttempt` or `retries429`) and its own
limit (e.g., `max429Retries`) instead of `attempt`/`maxRetries`, update the call
site that passes `attempt` to `handle429` to pass the new counter, and ensure
the loop still `continue`s after handling 429 without decrementing or advancing
the 202 `attempt` variable (keep `handle429` return behavior the same).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4508fc45-2dd6-4c86-ac13-a8ac9f2ac8a9

📥 Commits

Reviewing files that changed from the base of the PR and between 6df786e and d545188.

📒 Files selected for processing (4)
  • .gitignore
  • src/discord/client.ts
  • src/discord/schemas.ts
  • src/discord/search.ts

Comment thread src/discord/client.ts
The 429 handler was sharing the 202 loop's attempt counter, causing
rate-limit retries to consume the polling retry budget. Introduced an
independent rateLimitAttempt counter with its own MAX_429_RETRIES limit.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/discord/client.ts">

<violation number="1" location="src/discord/client.ts:195">
P2: 429 handling inside the 202 retry loop now ignores the caller-configured maxRetries429 and always uses MAX_429_RETRIES, so retry limits are inconsistent depending on whether the 429 happens during the initial request or during 202 polling.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/discord/client.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/discord/client.ts`:
- Line 12: Rename the two similarly named 429-retry constants to clearly reflect
their different uses: change DEFAULT_MAX_RETRIES_429 to something like
MAIN_LOOP_429_MAX_RETRIES (or DEFAULT_429_RETRIES_MAIN_LOOP) to indicate it
governs top-level request loop retries, and change MAX_429_RETRIES to something
like POLL_202_429_MAX_RETRIES (or POLLING_429_MAX_RETRIES) to indicate it only
applies during 202 polling; update all usages and any nearby comments in the
module (references: DEFAULT_MAX_RETRIES_429 and MAX_429_RETRIES) so the intent
is explicit and unambiguous.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: df62a0ab-5108-4618-8aff-45dc10831f1e

📥 Commits

Reviewing files that changed from the base of the PR and between d545188 and 7f7f59c.

📒 Files selected for processing (1)
  • src/discord/client.ts

Comment thread src/discord/client.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/discord/client.ts`:
- Around line 161-164: The current return uses parsed.data.retry_after ||
DEFAULT_RETRY_DELAY_SECONDS which treats 0 as falsy; change it to use nullish
coalescing so a server-provided 0 is respected: after
IndexNotReadyResponseSchema.safeParse(bodyResult.value) return parsed.success ?
parsed.data.retry_after ?? DEFAULT_RETRY_DELAY_SECONDS :
DEFAULT_RETRY_DELAY_SECONDS, referencing IndexNotReadyResponseSchema.safeParse,
parsed.success, parsed.data.retry_after and DEFAULT_RETRY_DELAY_SECONDS to
locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac58cadc-afc2-4b0b-8d19-aeab20d3ced4

📥 Commits

Reviewing files that changed from the base of the PR and between 7f7f59c and 9261550.

📒 Files selected for processing (1)
  • src/discord/client.ts

Comment thread src/discord/client.ts
- Enforce strict snowflake validation for Discord IDs (guildId, authorId, channelId, etc.)
- Parse SearchParamsSchema before search entrypoints to catch malformed input
- Safely encode guildId in path segments to avoid API contract violations
- Refactor rate-limit tracking to per-bucket state instead of global
- Use bucket keys based on route family and token for independent throttling
- Add response header bucket tracking for proper rate limit isolation
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/discord/client.ts">

<violation number="1" location="src/discord/client.ts:64">
P1: Bucket remapping logic orphans rate limit state. The locally computed `bucketKey` (`bucket:${hash}`) never matches the Discord-provided `X-RateLimit-Bucket` header (a hex string), so after remapping, subsequent requests can't find the stored state. Each request starts fresh with `remaining: 1`, defeating proactive rate limit tracking.

Consider storing a mapping from the computed key to the Discord bucket ID, or simply using the computed key consistently without remapping.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/discord/client.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/discord/client.ts`:
- Around line 31-40: getBucketKey currently derives keys from full request path
plus token, but updateRateLimitState replaces that key with the raw
X-RateLimit-Bucket header (dropping token isolation) causing recreated buckets
and lost state; fix by standardizing on a composite bucket key that always
includes the canonical bucket id plus token (e.g., composeFrom(bucketId, token))
and update all call sites: change getBucketKey to return the composite form,
change updateRateLimitState to map header X-RateLimit-Bucket into that same
composite key (do not delete and reinsert under the raw header), and ensure
waitForRateLimit uses the identical composite key so state persists across
paginated requests and retries while remaining token-scoped.

In `@src/discord/search.ts`:
- Around line 161-165: The pagination stops early because the code only seeds
state.totalResults when offset === 0; change that condition to compare against
the actual start offset passed into searchAllMessages (queryParams.offset) so
initial pages that start at a non-zero offset still initialize totalResults.
Concretely, in fetchPage (and the other places flagged around lines 197-199 and
269) replace checks like "offset === 0 && !maxId" with "offset ===
queryParams.offset && !maxId" (or otherwise use the stored initialOffset from
searchAllMessages) so state.totalResults is set when the first request of this
run is made rather than only when offset is zero.
- Around line 41-52: The code in searchMessages and buildQueryString is
overwriting validated pagination (params.offset / params.maxId) with unvalidated
defaults/overrides (offset param default 0 and maxId arg), so validate and use
an effectiveOffset/effectiveMaxId derived from SearchParamsSchema-validated
values unless an explicit override is passed; when an override is provided,
validate that override against the same bounds/schema (e.g., offset range and
snowflake format) before calling buildQueryString, then pass the validated
effectiveOffset/effectiveMaxId into buildQueryString (and update the other call
sites that build the query string in the same file to follow the same pattern).
- Line 270: In searchAllMessages the code incorrectly uses
SearchParamsSchema.limit (queryParams.limit) as maxMessages which turns the
per-page Discord page-size into a hard total cap; change the logic so
queryParams.limit is treated only as the page-size (keep it as the per-request
limit used when fetching pages) and remove or replace the assignment
maxMessages: queryParams.limit; if you need a total-message ceiling introduce a
separate option (e.g., totalLimit or maxTotalMessages) and use that to stop the
partition loop, leaving maxMessages undefined or driven by the new totalLimit
instead of overloading queryParams.limit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5f1a8c20-83ad-4d67-b115-c8a5361450c2

📥 Commits

Reviewing files that changed from the base of the PR and between 9261550 and 7f195a5.

📒 Files selected for processing (3)
  • src/discord/client.ts
  • src/discord/schemas.ts
  • src/discord/search.ts

Comment thread src/discord/client.ts Outdated
Comment thread src/discord/search.ts
Comment thread src/discord/search.ts Outdated
Comment thread src/discord/search.ts Outdated
…misuse

- Fix bucket remapping: add bucketKeyMap so subsequent requests resolve
  computed keys to Discord-provided bucket IDs, preserving rate limit state
- Fix totalResults init: use state.totalResults === 0 instead of offset === 0
  so pagination works correctly when starting from non-zero offsets
- Fix limit as maxMessages: searchAllMessages no longer caps total results
  at the per-page limit, allowing it to actually fetch all messages
@mynameistito
Copy link
Copy Markdown
Owner Author

@CodeRabbit Please do a deep analysis on this PR and see if there any issues that need to be resolved. bee as nit picky as possible.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== src/discord/schemas.ts ===" && cat src/discord/schemas.ts
echo ""
echo "=== src/discord/client.ts ===" && cat src/discord/client.ts
echo ""
echo "=== src/discord/search.ts ===" && cat src/discord/search.ts
echo ""
echo "=== .gitignore ===" && cat .gitignore
echo ""
echo "=== package.json ===" && cat package.json 2>/dev/null || echo "not found"
echo ""
echo "=== tsconfig.json ===" && cat tsconfig.json 2>/dev/null || echo "not found"
echo ""
echo "=== bun.lockb or bun.lock or package-lock.json ===" 
ls -la *.lock* 2>/dev/null || echo "no lock files at root"
fd -e ts -e json --max-depth 2 . src/ 2>/dev/null | head -40

Length of output: 27695


🏁 Script executed:

#!/bin/bash
echo "=== src/errors.ts ===" && cat src/errors.ts
echo ""
echo "=== src/types.ts ===" && cat src/types.ts
echo ""
echo "=== src/config.ts ===" && cat src/config.ts
echo ""
echo "=== src/index.ts ===" && cat src/index.ts
echo ""
echo "=== src/paths.ts ===" && cat src/paths.ts

Length of output: 6122


@mynameistito Here is a thorough, nitpicky analysis of the three new files:


🔴 Critical / High

1. Rate-limit bucket key includes full query string (client.ts)

// In discordFetch, path looks like:
// /guilds/123/messages/search?content=hello&limit=25&offset=0&...
const computedKey = computeBucketKey(path, token);

computeBucketKey hashes the entire path including query parameters. Every unique search query produces a different bucket key, so Discord's per-route rate limit is never actually shared across calls. All calls to /guilds/{id}/messages/search should share one bucket. The fix is to strip the query string before hashing:

const computeBucketKey = (path: string, token: string): BucketKey => {
  const routePath = path.split("?")[0]; // strip query string
  const hashInput = `${routePath}:${token}`;
  // ...
};

2. waitForRateLimit never decrements remaining — TOCTOU race (client.ts)

const waitForRateLimit = async (bucketKey: BucketKey): Promise<void> => {
  const state = getBucketState(bucketKey);
  if (state.remaining > 0) {
    return; // ← remaining is NOT decremented here
  }
  // ...
};

With concurrent async calls (e.g., progress callback triggering another search), multiple callers all read remaining > 0 and proceed simultaneously. Remaining should be decremented optimistically before the request is made, and restored if the request fails:

if (state.remaining > 0) {
  state.remaining--; // optimistic decrement
  return;
}

3. parseRetryAfterFrom202 uses || instead of ?? (client.ts)

return parsed.success
  ? parsed.data.retry_after || DEFAULT_RETRY_DELAY_SECONDS  // ← bug
  : DEFAULT_RETRY_DELAY_SECONDS;

If Discord returns retry_after: 0 (retry immediately), the falsy || silently substitutes DEFAULT_RETRY_DELAY_SECONDS (2 s), introducing an unnecessary delay. Use ??:

return parsed.success
  ? parsed.data.retry_after ?? DEFAULT_RETRY_DELAY_SECONDS
  : DEFAULT_RETRY_DELAY_SECONDS;

🟠 Medium

4. updateRateLimitState always remaps on every response (client.ts)

After the first successful remap, bucketKey becomes discord:abc123. On the next response, the header bucket is still abc123, so bucket !== bucketKey"abc123" !== "discord:abc123"true, triggering a redundant delete + set on every single call. The condition should compare against the prefixed form:

if (bucket !== null && `discord:${bucket}` !== bucketKey) {

5. snowflakeArraySchema duplicates the inline schema instead of reusing snowflakeSchema (schemas.ts)

const snowflakeArraySchema = z.array(
  z.string().regex(SNOWFLAKE_REGEX, { ... }).max(20, { ... }) // ← copy-paste
);

Should be:

const snowflakeArraySchema = z.array(snowflakeSchema);

6. searchAllMessages silently overwrites caller's sortBy/sortOrder (search.ts)

searchParams: { ...queryParams, sortBy: "timestamp", sortOrder: "desc" },

A user who passes sortBy: "relevance" gets it silently replaced. At minimum this should throw a ValidationError if the caller passes incompatible sort params, or document the override prominently. For sortOrder: "asc", the snowflake partition direction would also break, so an explicit guard makes sense:

if (queryParams.sortBy && queryParams.sortBy !== "timestamp") {
  return Result.err(new ValidationError({
    message: "searchAllMessages requires sortBy: 'timestamp'",
    issues: [],
  }));
}

7. maxMessages is unreachable from callers (search.ts)

PaginationConfig has maxMessages: undefined hardcoded in searchAllMessages. The entire truncation path in fetchPage is wired up but can never be triggered:

const config: PaginationConfig = {
  ...
  maxMessages: undefined, // ← always undefined; no parameter to set it
};

Either expose it as an optional parameter or remove the dead truncation code.


8. Double-validation on every page in fetchPage (search.ts)

fetchPage calls searchMessages(params, ...) where params is already SearchParams (fully validated). Inside searchMessages, validateSearchParams(params) runs SearchParamsSchema.safeParse again on every page. Consider an internal overload or helper that accepts pre-validated params to avoid this overhead.


9. 202-polling has an independent 429-retry counter (client.ts)

handle202 starts its own rateLimitAttempt = 0 counter. This means a request can consume up to 2 × maxRetries429 total 429 retries (outer loop + inside 202 polling). The attempt--; continue pattern inside the 202 loop is also fragile — if rateLimitAttempt reaches maxRetries429 while attempt is still low, the 429 is surfaced, but the outer caller may try the URL again. Consider sharing or threading the retry counter through.


🟡 Low / Nit

10. RateLimitBodySchema is stranded in client.ts (client.ts)

All other schemas live in schemas.ts. RateLimitBodySchema is a private implementation detail but its asymmetry is jarring. Move it to schemas.ts or at least add a comment explaining the intentional placement.


11. fetchWithAuth is hardcoded to GET (client.ts)

fetch(url, { headers: makeHeaders(token), signal: controller.signal })

No method is specified (defaults to GET). If any future Discord endpoint needs POST, the entire abstraction breaks. Adding method?: string = "GET" now costs nothing.


12. guild_id missing from MessageSchema (schemas.ts)

Discord messages in search results include guild_id as an optional string. Without it, any downstream code that needs to know which guild a message came from must get it from context:

export const MessageSchema = z.object({
  // ...
  guild_id: snowflakeSchema.optional(),
});

13. EmbedImageSchema and EmbedThumbnailSchema are structurally identical (schemas.ts)

Both schemas have the exact same four fields (url, proxy_url, height, width). While they're semantically distinct, they could share a base:

const EmbedMediaSchema = z.object({
  url: z.string(),
  proxy_url: z.string().optional(),
  height: z.number().optional(),
  width: z.number().optional(),
});
export const EmbedImageSchema = EmbedMediaSchema;
export const EmbedThumbnailSchema = EmbedMediaSchema;

14. DEFAULT_PAGE_SIZE is a redundant alias for MAX_PAGE_SIZE (search.ts)

// schemas.ts
export const MAX_PAGE_SIZE = 25;

// search.ts
const DEFAULT_PAGE_SIZE = 25; // ← same value, different name

Just import and use MAX_PAGE_SIZE directly throughout search.ts.


15. computeBucketKey hash can produce negative bucket keys (client.ts)

JavaScript's hash &= hash (bitwise AND) preserves the 32-bit signed integer, so hash can be negative, yielding keys like bucket:-1234567890. This isn't a correctness bug but produces confusing diagnostics. Use hash >>> 0 (unsigned right-shift) to force an unsigned value:

hash = ((hash << 5) - hash + char) >>> 0;

16. No unit tests

The PR description lists a test plan but no test files were added. The happy paths and the three error branches (429, 202, validation failure) all warrant coverage, especially given the subtle retry-counter interaction flagged in issue #9 above.


Summary table

# File Severity Issue
1 client.ts 🔴 Critical Bucket key hashes full URL incl. query string
2 client.ts 🔴 High TOCTOU: remaining never decremented optimistically
3 client.ts 🔴 High || instead of ?? for retry_after: 0
4 client.ts 🟠 Medium Bucket always remapped on every response
5 schemas.ts 🟠 Medium snowflakeArraySchema duplicates inline schema
6 search.ts 🟠 Medium Silent sort override in searchAllMessages
7 search.ts 🟠 Medium maxMessages unreachable from callers
8 search.ts 🟠 Medium Redundant re-validation per page
9 client.ts 🟠 Medium Independent 429-counter inside 202 polling
10 client.ts 🟡 Nit RateLimitBodySchema not in schemas.ts
11 client.ts 🟡 Nit fetchWithAuth hardcoded to GET
12 schemas.ts 🟡 Nit Missing guild_id in MessageSchema
13 schemas.ts 🟡 Nit EmbedImageSchema/EmbedThumbnailSchema identical
14 search.ts 🟡 Nit DEFAULT_PAGE_SIZE is redundant alias of MAX_PAGE_SIZE
15 client.ts 🟡 Nit Hash can produce negative bucket key strings
16 🟡 Nit No unit tests

🧠 Learnings used
Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/discord/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:36.696Z
Learning: Applies to src/discord/**/search.ts : Use Snowflake pagination (partition by snowflake) for results exceeding 10,000 items

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:26:40.141Z
Learning: Applies to src/discord/**/*.{ts,tsx} : Use Rate limiting with exponential backoff for Discord API interactions

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/discord/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:36.696Z
Learning: Applies to src/discord/**/search.ts : Use BigInt for Snowflake parsing (cannot use Number type due to precision limits)

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/discord/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:36.696Z
Learning: Applies to src/discord/**/*.ts : Do not skip Zod validation of API responses

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/discord/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:36.696Z
Learning: Applies to src/discord/**/search.ts : Use offset pagination with 0-9975 range (25 results per page, max 400 pages)

… search

Strip query string from rate limit bucket keys, optimistically decrement
remaining count, use ?? for retry_after, fix redundant bucket remapping,
share 429 retry counter between outer loop and 202 polling, deduplicate
schemas, guard sort param overrides, expose maxMessages, eliminate
double-validation per page, and minor cleanups.
@mynameistito mynameistito merged commit 4ce4877 into main Mar 26, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant