Skip to content

Heap-based expiry for unauthorized cooldown and IndexedDB rapid user-switch race.#51

Merged
santhoshh-kumar merged 2 commits into
mainfrom
guest-user-polish
May 6, 2026
Merged

Heap-based expiry for unauthorized cooldown and IndexedDB rapid user-switch race.#51
santhoshh-kumar merged 2 commits into
mainfrom
guest-user-polish

Conversation

@santhoshh-kumar
Copy link
Copy Markdown
Collaborator

realtime/src/server.ts — Heap-based expiry for unauthorized cooldown:

Previously, cleanupExpiredUnauthorizedCooldown did a full scan of the entire unauthorizedAccessCooldown map on every run. This was O(n) even if nothing had expired.

The new code maintains a min-heap (unauthorizedCooldownExpirations) sorted by denyUntil. Cleanup now peeks at the heap root and stops immediately if the earliest expiry hasn't passed yet — skipping the full scan. The heap processes at most 512 entries per cleanup run to avoid blocking.

The subtle correctness issue it handles: when a cooldown is renewed (same key, new denyUntil), a second entry is pushed onto the heap rather than updating the existing one. The stale entry from before the renewal will eventually surface during cleanup. The check existingState.denyUntil !== expiredEntry.denyUntil detects and skips those stale entries, preventing a renewed cooldown from being incorrectly evicted.

maybeRebuildUnauthorizedCooldownExpirationHeap handles heap bloat from accumulated stale entries by doing a full rebuild from the source-of-truth map when the heap is more than 4× the map size (and above 1024 entries).

web/services/indexed-db.service.ts — Fix race condition in user switches:

Previously, switching users rapidly could cause a race: setUserId only closed the DB if no close was already in progress (!this.dbClosingPromise). Rapid switches (A → B → C → B) would skip closure steps, and a subsequent getDB call might await a stale dbClosingPromise and then open the wrong database.

The fix replaces the single dbClosingPromise with a chained promise (dbSwitchPromise). Each call to setUserId appends a closeCurrentConnection step onto the chain — so no switch is ever skipped, they're always serialized. awaitStableUserSwitch loops until the promise reference stabilises, handling the case where another switch was queued while you were awaiting.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a min-heap to optimize the cleanup of expired unauthorized access cooldowns in the realtime server and refactors the IndexedDB service in the web client to safely handle rapid user switches. Feedback indicates that triggering heap rebuilds during the connection handshake could introduce latency spikes and potential DoS vulnerabilities; it is recommended to move this logic to the background cleanup process.

Comment thread realtime/src/server.ts
Comment thread realtime/src/server.ts
@santhoshh-kumar
Copy link
Copy Markdown
Collaborator Author

santhoshh-kumar commented May 6, 2026

The DoS argument from above comments doesn't seem to hold up but the suggestion to remove the calls is still correct because they're redundant!

Previously, cleanupExpiredUnauthorizedCooldown scanned
the entire unauthorizedAccessCooldown map on every run.
This was O(n) even if nothing had expired.

Maintain a min-heap (unauthorizedCooldownExpirations)
sorted by denyUntil. Cleanup now peeks at the heap root
and stops if the earliest expiry has not passed, skipping
the full scan. Process at most 512 entries per run to
avoid blocking.

When a cooldown is renewed (same key, new denyUntil), a
second entry is pushed to the heap. Stale entries surface
later; compare existingState.denyUntil with the expired
entry and skip mismatches to avoid incorrect eviction.

maybeRebuildUnauthorizedCooldownExpirationHeap handles
heap bloat from stale entries by rebuilding from the
source map when the heap exceeds 4x the map size and is
above 1024 entries.
Previously, switching users rapidly could cause a race:
setUserId only closed the DB if no close was in progress.
Rapid switches (A → B → C → B) skipped closures, and a
subsequent getDB could open the wrong database.

Replace dbClosingPromise with a chained dbSwitchPromise.
Each setUserId appends closeCurrentConnection to the
chain so switches are serialized and none are skipped.

awaitStableUserSwitch loops until the promise reference
stabilizes, handling switches queued while awaiting.
@santhoshh-kumar santhoshh-kumar merged commit bebdf51 into main May 6, 2026
7 checks passed
@santhoshh-kumar santhoshh-kumar deleted the guest-user-polish branch May 6, 2026 14:31
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.

1 participant