-
Notifications
You must be signed in to change notification settings - Fork 2
fix(session): reclaim orphan session codes on create #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { isReclaimableSessionForCode } from "./firestoreAnnotations"; | ||
|
|
||
| describe("isReclaimableSessionForCode", () => { | ||
| it("reclaims missing sessions", () => { | ||
| expect(isReclaimableSessionForCode(null)).toBe(true); | ||
| expect(isReclaimableSessionForCode(undefined)).toBe(true); | ||
| }); | ||
|
|
||
| it("reclaims ended sessions", () => { | ||
| expect(isReclaimableSessionForCode({ status: "ended" })).toBe(true); | ||
| expect( | ||
| isReclaimableSessionForCode({ endedAt: "2026-01-01T00:00:00.000Z" }), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it("does not reclaim live active sessions", () => { | ||
| expect(isReclaimableSessionForCode({ status: "active" })).toBe(false); | ||
| }); | ||
| }); | ||
|
Comment on lines
+4
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift Cover the Firestore lifecycle, not only the predicate. These tests do not exercise the changed As per path instructions, tests should cover changed behavior and edge cases in the Vitest-focused scope. 🤖 Prompt for AI AgentsSource: Path instructions |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { | |
| arrayRemove, | ||
| arrayUnion, | ||
| collection, | ||
| deleteDoc, | ||
| deleteField, | ||
| doc, | ||
| getDoc, | ||
|
|
@@ -66,6 +67,19 @@ function sessionsCollection() { | |
| function sessionCodeDoc(code: string) { | ||
| return doc(getFirestoreDb(), "sessionCodes", code); | ||
| } | ||
|
|
||
| /** True when a sessionCodes doc may be deleted and the code reused. */ | ||
| export function isReclaimableSessionForCode( | ||
| sessionData: Record<string, unknown> | null | undefined, | ||
| ): boolean { | ||
| if (sessionData == null) { | ||
| return true; | ||
| } | ||
|
|
||
| return ( | ||
| sessionData.status === "ended" || typeof sessionData.endedAt === "string" | ||
| ); | ||
| } | ||
| function annotationsCollection(sessionId: string) { | ||
| return collection(getFirestoreDb(), "sessions", sessionId, "annotations"); | ||
| } | ||
|
|
@@ -259,8 +273,14 @@ export async function createRemoteSession( | |
| break; | ||
| } | ||
|
|
||
| code = generateSessionCode(); | ||
| attempts += 1; | ||
| try { | ||
| // Rules allow delete only for host, missing session, or ended session. | ||
| await deleteDoc(sessionCodeDoc(code)); | ||
| break; | ||
| } catch { | ||
| code = generateSessionCode(); | ||
| attempts += 1; | ||
| } | ||
|
Comment on lines
+276
to
+283
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not use the unverified candidate after the retry limit. After the eighth failed deletion, Line 281 generates a new code and the loop exits without checking whether that candidate already exists. The subsequent write can collide with an existing document and fail unexpectedly. Throw when the limit is exhausted or verify the final candidate before writing. As per path instructions, Firestore services must cover “Offline write queuing and idempotency.” 🤖 Prompt for AI AgentsSource: Path instructions |
||
| } | ||
|
|
||
| const sessionRef = doc(sessionsCollection()); | ||
|
|
@@ -744,9 +764,7 @@ export async function endRemoteSession(sessionId: string): Promise<void> { | |
| }); | ||
|
|
||
| if (session?.code) { | ||
| await updateDoc(sessionCodeDoc(session.code), { | ||
| status: "ended", | ||
| }); | ||
| await deleteDoc(sessionCodeDoc(session.code)); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not allow deletion of an active session’s code.
The
resource.data.hostUid == request.auth.uidbranch permits deleting a code while its linked session is still active. SincecreateRemoteSessionattempts this deletion for every collision, a host can remove an active mapping, reuse the code for a new session, and leave the old session pointing at the new mapping.Restrict deletion to missing or ended linked sessions, or make the end update and deletion an atomic, end-specific operation.
As per path instructions, Firestore rules require auth checks on all writes and parity with the TypeScript lifecycle behavior.
🤖 Prompt for AI Agents
Source: Path instructions