@@ -123,16 +123,20 @@ function issueListQuery(executor: DbOrTx) {
123123/** Every open issue, plus the most recently closed ones. */
124124export async function listWorkspaceIssues ( workspaceId : string ) : Promise < IssueListRow [ ] > {
125125 const active = and ( eq ( issue . workspaceId , workspaceId ) , isNull ( issue . deletedAt ) )
126- const [ open , done ] = await Promise . all ( [
127- issueListQuery ( db )
128- . where ( and ( active , ne ( issue . status , 'done' ) ) )
129- . orderBy ( desc ( issue . updatedAt ) ) ,
130- issueListQuery ( db )
131- . where ( and ( active , eq ( issue . status , 'done' ) ) )
132- . orderBy ( desc ( issue . completedAt ) )
133- . limit ( LISTED_DONE_ISSUES ) ,
134- ] )
135- return [ ...open , ...done ]
126+ // One snapshot for both reads, so an issue closing in between is listed exactly once.
127+ return db . transaction (
128+ async ( tx ) => {
129+ const open = await issueListQuery ( tx )
130+ . where ( and ( active , ne ( issue . status , 'done' ) ) )
131+ . orderBy ( desc ( issue . updatedAt ) )
132+ const done = await issueListQuery ( tx )
133+ . where ( and ( active , eq ( issue . status , 'done' ) ) )
134+ . orderBy ( desc ( issue . completedAt ) )
135+ . limit ( LISTED_DONE_ISSUES )
136+ return [ ...open , ...done ]
137+ } ,
138+ { isolationLevel : 'repeatable read' , accessMode : 'read only' }
139+ )
136140}
137141
138142export async function getIssueListRow ( issueId : string ) : Promise < IssueListRow | null > {
@@ -144,6 +148,8 @@ export interface IssueStateGuard {
144148 statuses : readonly IssueStatus [ ]
145149 /** Inbox with a working chat is waiting for review; without one it is new. */
146150 hasWorkingChat ?: boolean
151+ /** The exact working chat the caller read, so a replaced chat's late request changes nothing. */
152+ workingChatId ?: string
147153 /** Fields that must still hold the values the caller read, so an edit never logs a stale "from". */
148154 unchanged ?: { title ?: string ; priority ?: number ; ownerId ?: string | null }
149155}
@@ -172,6 +178,9 @@ export async function updateIssueInTx(
172178 eq ( issue . id , issueId ) ,
173179 inArray ( issue . status , [ ...guard . statuses ] ) ,
174180 chatCondition ,
181+ guard . workingChatId === undefined
182+ ? undefined
183+ : eq ( issue . workingChatId , guard . workingChatId ) ,
175184 guard . unchanged ?. title === undefined ? undefined : eq ( issue . title , guard . unchanged . title ) ,
176185 guard . unchanged ?. priority === undefined
177186 ? undefined
0 commit comments