11import fs from 'node:fs' ;
2+ import { randomUUID } from 'node:crypto' ;
23import path from 'node:path' ;
34
45interface ActiveWorkspaceSelectionFile {
@@ -10,6 +11,11 @@ export interface ActiveWorkspaceSelectionStore {
1011 snapshot ( ) : { workspaceId : string | null ; generation : number } ;
1112 set ( workspaceId : string ) : Promise < void > ;
1213 clear ( ) : Promise < void > ;
14+ clearIf ( workspaceId : string ) : Promise < boolean > ;
15+ replaceIf (
16+ expectedWorkspaceId : string | null ,
17+ workspaceId : string ,
18+ ) : Promise < string | null > ;
1319 subscribe ( listener : ( workspaceId : string | null ) => void ) : ( ) => void ;
1420}
1521
@@ -53,6 +59,7 @@ export function createActiveWorkspaceSelectionStore(
5359 const filePath = path . join ( dataDir , 'workspace-selection.json' ) ;
5460 let cached : string | null | undefined ;
5561 let generation = 0 ;
62+ let mutationTail = Promise . resolve ( ) ;
5663 const listeners = new Set < ( workspaceId : string | null ) => void > ( ) ;
5764
5865 const read = ( ) : string | null => {
@@ -79,6 +86,37 @@ export function createActiveWorkspaceSelectionStore(
7986 }
8087 } ;
8188
89+ const enqueueMutation = < T > ( mutation : ( ) => Promise < T > ) : Promise < T > => {
90+ const result = mutationTail . then ( mutation ) ;
91+ mutationTail = result . then (
92+ ( ) => undefined ,
93+ ( ) => undefined ,
94+ ) ;
95+ return result ;
96+ } ;
97+
98+ const persist = async ( workspaceId : string ) => {
99+ await fs . promises . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
100+ const tempPath = `${ filePath } .${ process . pid } .${ randomUUID ( ) } .tmp` ;
101+ try {
102+ await fs . promises . writeFile (
103+ tempPath ,
104+ JSON . stringify ( { workspaceId } , null , 2 ) ,
105+ 'utf8' ,
106+ ) ;
107+ await fs . promises . rename ( tempPath , filePath ) ;
108+ } catch ( error ) {
109+ await fs . promises . rm ( tempPath , { force : true } ) . catch ( ( ) => undefined ) ;
110+ throw error ;
111+ }
112+ } ;
113+
114+ const commit = ( workspaceId : string ) => {
115+ cached = workspaceId ;
116+ generation += 1 ;
117+ notify ( workspaceId ) ;
118+ } ;
119+
82120 return {
83121 get : read ,
84122 snapshot ( ) {
@@ -87,21 +125,47 @@ export function createActiveWorkspaceSelectionStore(
87125 async set ( workspaceId : string ) {
88126 const next = workspaceId . trim ( ) ;
89127 if ( ! next ) throw new Error ( 'workspaceId is required' ) ;
90- cached = next ;
91- generation += 1 ;
92- await fs . promises . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
93- await fs . promises . writeFile (
94- filePath ,
95- JSON . stringify ( { workspaceId : next } , null , 2 ) ,
96- 'utf8' ,
97- ) ;
98- notify ( next ) ;
128+ await enqueueMutation ( async ( ) => {
129+ await persist ( next ) ;
130+ commit ( next ) ;
131+ } ) ;
99132 } ,
100133 async clear ( ) {
101- cached = null ;
102- generation += 1 ;
103- await fs . promises . rm ( filePath , { force : true } ) ;
104- notify ( null ) ;
134+ await enqueueMutation ( async ( ) => {
135+ await fs . promises . rm ( filePath , { force : true } ) ;
136+ cached = null ;
137+ generation += 1 ;
138+ notify ( null ) ;
139+ } ) ;
140+ } ,
141+ async clearIf ( workspaceId : string ) {
142+ const expected = workspaceId . trim ( ) ;
143+ if ( ! expected ) return false ;
144+ return enqueueMutation ( async ( ) => {
145+ if ( read ( ) !== expected ) return false ;
146+ await fs . promises . rm ( filePath , { force : true } ) ;
147+ cached = null ;
148+ generation += 1 ;
149+ notify ( null ) ;
150+ return true ;
151+ } ) ;
152+ } ,
153+ async replaceIf ( expectedWorkspaceId : string | null , workspaceId : string ) {
154+ const expected = expectedWorkspaceId ?. trim ( ) || null ;
155+ const next = workspaceId . trim ( ) ;
156+ if ( ! next ) throw new Error ( 'workspaceId is required' ) ;
157+ await enqueueMutation ( async ( ) => {
158+ if ( read ( ) !== expected ) return ;
159+ await persist ( next ) ;
160+ commit ( next ) ;
161+ } ) ;
162+
163+ // A user switch can queue while the conditional write is in flight.
164+ // Drain mutations that were already queued when this write settled, then
165+ // report the selection that actually won instead of the temporary value.
166+ const queuedThroughCommit = mutationTail ;
167+ await queuedThroughCommit ;
168+ return read ( ) ;
105169 } ,
106170 subscribe ( listener ) {
107171 listeners . add ( listener ) ;
0 commit comments