@@ -3,6 +3,7 @@ import { dirname, join, resolve } from "node:path";
33import { homedir } from "node:os" ;
44import { AppError , AppErrorCode } from "../types/errors.js" ;
55import { TaskManagerFile } from "../types/data.js" ;
6+ import * as fs from 'node:fs' ;
67
78export interface InitializedTaskData {
89 data : TaskManagerFile ;
@@ -12,12 +13,11 @@ export interface InitializedTaskData {
1213
1314export class FileSystemService {
1415 private filePath : string ;
15- // Simple in-memory queue to prevent concurrent file operations
16- private operationInProgress : boolean = false ;
17- private operationQueue : ( ( ) => void ) [ ] = [ ] ;
16+ private lockFilePath : string ;
1817
1918 constructor ( filePath : string ) {
2019 this . filePath = filePath ;
20+ this . lockFilePath = `${ filePath } .lock` ;
2121 }
2222
2323 /**
@@ -41,47 +41,56 @@ export class FileSystemService {
4141 }
4242
4343 /**
44- * Queue a file operation to prevent concurrent access
45- * @param operation The operation to perform
46- * @returns Promise that resolves when the operation completes
44+ * Acquires a file system lock
4745 */
48- private async queueOperation < T > ( operation : ( ) => Promise < T > ) : Promise < T > {
49- // If another operation is in progress, wait for it to complete
50- if ( this . operationInProgress ) {
51- return new Promise < T > ( ( resolve , reject ) => {
52- this . operationQueue . push ( ( ) => {
53- this . executeOperation ( operation ) . then ( resolve ) . catch ( reject ) ;
54- } ) ;
55- } ) ;
46+ private async acquireLock ( ) : Promise < void > {
47+ while ( true ) {
48+ try {
49+ // Try to create lock file
50+ const fd = fs . openSync ( this . lockFilePath , 'wx' ) ;
51+ fs . closeSync ( fd ) ;
52+ return ;
53+ } catch ( error : any ) {
54+ if ( error . code === 'EEXIST' ) {
55+ // Lock file exists, wait and retry
56+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
57+ continue ;
58+ }
59+ throw error ;
60+ }
5661 }
62+ }
5763
58- return this . executeOperation ( operation ) ;
64+ /**
65+ * Releases the file system lock
66+ */
67+ private async releaseLock ( ) : Promise < void > {
68+ try {
69+ await fs . promises . unlink ( this . lockFilePath ) ;
70+ } catch ( error : any ) {
71+ if ( error . code !== 'ENOENT' ) {
72+ throw error ;
73+ }
74+ }
5975 }
6076
6177 /**
62- * Execute a file operation with mutex protection
63- * @param operation The operation to perform
64- * @returns Promise that resolves when the operation completes
78+ * Execute a file operation with file system lock
6579 */
6680 private async executeOperation < T > ( operation : ( ) => Promise < T > ) : Promise < T > {
67- this . operationInProgress = true ;
81+ await this . acquireLock ( ) ;
6882 try {
6983 return await operation ( ) ;
7084 } finally {
71- this . operationInProgress = false ;
72- // Process the next operation in the queue, if any
73- const nextOperation = this . operationQueue . shift ( ) ;
74- if ( nextOperation ) {
75- nextOperation ( ) ;
76- }
85+ await this . releaseLock ( ) ;
7786 }
7887 }
7988
8089 /**
8190 * Loads and initializes task data from the JSON file
8291 */
8392 public async loadAndInitializeTasks ( ) : Promise < InitializedTaskData > {
84- return this . queueOperation ( async ( ) => {
93+ return this . executeOperation ( async ( ) => {
8594 const data = await this . loadTasks ( ) ;
8695 const { maxProjectId, maxTaskId } = this . calculateMaxIds ( data ) ;
8796
@@ -95,11 +104,9 @@ export class FileSystemService {
95104
96105 /**
97106 * Explicitly reloads task data from the disk
98- * This is useful when the file may have been changed by another process
99- * @returns The latest task data from disk
100107 */
101108 public async reloadTasks ( ) : Promise < TaskManagerFile > {
102- return this . queueOperation ( async ( ) => {
109+ return this . executeOperation ( async ( ) => {
103110 return this . loadTasks ( ) ;
104111 } ) ;
105112 }
@@ -140,7 +147,8 @@ export class FileSystemService {
140147 } catch ( error ) {
141148 if ( error instanceof Error ) {
142149 if ( error . message . includes ( 'ENOENT' ) ) {
143- throw new AppError ( `Tasks file not found: ${ this . filePath } ` , AppErrorCode . FileReadError , error ) ;
150+ // If file doesn't exist, return empty data
151+ return { projects : [ ] } ;
144152 }
145153 throw new AppError ( `Failed to read tasks file: ${ error . message } ` , AppErrorCode . FileReadError , error ) ;
146154 }
@@ -149,10 +157,10 @@ export class FileSystemService {
149157 }
150158
151159 /**
152- * Saves task data to the JSON file with an in-memory mutex to prevent concurrent writes
160+ * Saves task data to the JSON file with file system lock
153161 */
154162 public async saveTasks ( data : TaskManagerFile ) : Promise < void > {
155- return this . queueOperation ( async ( ) => {
163+ return this . executeOperation ( async ( ) => {
156164 try {
157165 // Ensure directory exists before writing
158166 const dir = dirname ( this . filePath ) ;
0 commit comments