From 5af9b8312bb873a88a1b540b8da460f3c135b3fb Mon Sep 17 00:00:00 2001 From: Ariel Date: Sun, 6 Jul 2025 17:35:58 +0300 Subject: [PATCH 1/4] Add write queue to prevent race conditions in file operations - Add writeQueue and isWriting to constructor - Implement writeAll method with queuing - Add processWriteQueue for sequential writes - Prevents concurrent --- lib/json-file-crud.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/json-file-crud.js b/lib/json-file-crud.js index d2fe1ce..d64bf6f 100644 --- a/lib/json-file-crud.js +++ b/lib/json-file-crud.js @@ -14,6 +14,8 @@ class JsonFileCRUD { } this.filePath = path.resolve(filePath); this.idField = options.idField || "id"; + this.writeQueue = []; + this.isWriting = false; } //#region CREATE @@ -110,6 +112,38 @@ class JsonFileCRUD { } //#endregion DELETE + + /** + * Writes items array to file, replacing all content + * @param {Object[]} items - Array of items to save + * @param {Function} callback - Called with (error) + */ + writeAll(items, callback) { + // Add to queue if already writing + if (this.isWriting) { + this.writeQueue.push({ items, callback }); + return; + } + + this.isWriting = true; + + const content = JSON.stringify(items, null, 2); + fs.writeFile(this.filePath, content, (writeErr) => { + this.isWriting = false; + callback(writeErr); + this.processWriteQueue(); + }); + } + + /** + * Process queued write operations + */ + processWriteQueue() { + if (this.writeQueue.length > 0 && !this.isWriting) { + const { items, callback } = this.writeQueue.shift(); + this.writeAll(items, callback); + } + } } export default JsonFileCRUD; From 97e0522219523eaefb0ce0851adc667cb29510d7 Mon Sep 17 00:00:00 2001 From: Ariel Date: Sun, 6 Jul 2025 18:10:39 +0300 Subject: [PATCH 2/4] Implement write queue for create operations to prevent race conditions --- lib/json-file-crud.js | 50 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/lib/json-file-crud.js b/lib/json-file-crud.js index d64bf6f..280468c 100644 --- a/lib/json-file-crud.js +++ b/lib/json-file-crud.js @@ -20,8 +20,43 @@ class JsonFileCRUD { //#region CREATE - create(data, callback) { - // TODO: implement create + create(item, callback) { + // Add to queue if already processing + if (this.isWriting) { + this.writeQueue.push({ + type: 'create', + item: item, + callback: callback + }); + return; + } + + this.isWriting = true; + + this.readAll((err, items) => { + if (err) { + this.isWriting = false; + this.processWriteQueue(); + return callback(err); + } + + // Add new item to array + items.push(item); + + // Write back to file + const content = JSON.stringify(items, null, 2); + fs.writeFile(this.filePath, content, (writeErr) => { + this.isWriting = false; + + if (writeErr) { + callback(writeErr); + } else { + callback(null, item); + } + + this.processWriteQueue(); + }); + }); } //#endregion CREATE @@ -140,8 +175,15 @@ class JsonFileCRUD { */ processWriteQueue() { if (this.writeQueue.length > 0 && !this.isWriting) { - const { items, callback } = this.writeQueue.shift(); - this.writeAll(items, callback); + const queuedOperation = this.writeQueue.shift(); + + if (queuedOperation.type === 'create') { + // Handle create operation + this.create(queuedOperation.item, queuedOperation.callback); + } else { + // Handle writeAll operation + this.writeAll(queuedOperation.items, queuedOperation.callback); + } } } } From 408119cf8805ff64c94451eea169636aa2d1e8c7 Mon Sep 17 00:00:00 2001 From: Ariel Date: Sun, 6 Jul 2025 18:11:05 +0300 Subject: [PATCH 3/4] Add tests for create operations (single, multiple, and concurrent item creation) --- test/test-create.js | 155 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 test/test-create.js diff --git a/test/test-create.js b/test/test-create.js new file mode 100644 index 0000000..94f9754 --- /dev/null +++ b/test/test-create.js @@ -0,0 +1,155 @@ +import JsonFileCRUD from "../lib/json-file-crud.js"; +import fs from "fs"; + +// Test setup +const testFile = "./test-create-data.json"; +const crud = new JsonFileCRUD(testFile); +let passed = 0; +let total = 0; +let completed = 0; + +function test(description, testFn) { + total++; + testFn((err, success) => { + completed++; + + if (err || !success) { + console.log(`✗ ${description}: ${err?.message || 'failed'}`); + } else { + console.log(`✓ ${description}`); + passed++; + } + + // Check if all async tests are done + if (completed === total) { + console.log(`\n${passed}/${total} tests passed`); + // Clean up test file + if (fs.existsSync(testFile)) { + fs.unlinkSync(testFile); + } + process.exit(passed === total ? 0 : 1); + } + }); +} + +// Clean up and start fresh +if (fs.existsSync(testFile)) { + fs.unlinkSync(testFile); +} + +// Test create single item +test('create single item works', (done) => { + crud.create({ id: 1, name: "Ariel" }, (err, item) => { + if (err) return done(err); + if (item.name === "Ariel") return done(null, true); + done(new Error('wrong item data')); + }); +}); + +// Test create and verify item exists +test('create and verify item exists', (done) => { + crud.create({ id: 2, name: "Yoni" }, (err, item) => { + if (err) return done(err); + + // Verify it was saved + crud.findById(2, (err2, foundItem) => { + if (err2) return done(err2); + if (foundItem && foundItem.name === "Yoni") return done(null, true); + done(new Error('item not found after create')); + }); + }); +}); + +// Test create multiple items +test('create multiple items works', (done) => { + crud.create({ id: 3, name: "Moshe" }, (err, item) => { + if (err) return done(err); + + crud.create({ id: 4, name: "Nissim" }, (err2, item2) => { + if (err2) return done(err2); + + // Verify both items exist + crud.count((err3, count) => { + if (err3) return done(err3); + // Should have 4 items total (from previous tests) + if (count === 4) return done(null, true); + done(new Error(`expected 4 items, got ${count}`)); + }); + }); + }); +}); + +// Test concurrent creates (queue test) +test('concurrent creates work with queue', (done) => { + // Create a separate instance for this test + const testFile2 = "./test-concurrent-data.json"; + const crud2 = new JsonFileCRUD(testFile2); + + // Clean up test file + if (fs.existsSync(testFile2)) { + fs.unlinkSync(testFile2); + } + + let completedCreates = 0; + const totalCreates = 3; + + // Create multiple items at the same time + crud2.create({ id: 10, name: "Concurrent1" }, (err, item) => { + if (err) return done(err); + completedCreates++; + checkCompletion(); + }); + + crud2.create({ id: 11, name: "Concurrent2" }, (err, item) => { + if (err) return done(err); + completedCreates++; + checkCompletion(); + }); + + crud2.create({ id: 12, name: "Concurrent3" }, (err, item) => { + if (err) return done(err); + completedCreates++; + checkCompletion(); + }); + + function checkCompletion() { + if (completedCreates === totalCreates) { + // All creates completed, now verify all items exist + crud2.count((err, count) => { + // Clean up test file + if (fs.existsSync(testFile2)) { + fs.unlinkSync(testFile2); + } + + if (err) return done(err); + if (count === 3) return done(null, true); + done(new Error(`expected 3 items, got ${count}`)); + }); + } + } +}); + +// Test create with different data types +test('create with different data types works', (done) => { + const complexItem = { + id: 5, + name: "Moshe", + age: 30, + active: true, + tags: ["test", "demo"], + profile: { city: "Jerusalem", country: "Israel" } + }; + + crud.create(complexItem, (err, item) => { + if (err) return done(err); + + // Verify complex data was saved correctly + crud.findById(5, (err2, foundItem) => { + if (err2) return done(err2); + if (foundItem && foundItem.name === "Moshe" && foundItem.age === 30 && foundItem.active === true) { + return done(null, true); + } + done(new Error('complex item not saved correctly')); + }); + }); +}); From d6151d20a4cc2e282c21208871dee29be2ef2eeb Mon Sep 17 00:00:00 2001 From: Ariel Date: Sun, 6 Jul 2025 18:11:17 +0300 Subject: [PATCH 4/4] Add test script for create operations in package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f4b28e..cd0e57c 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "type": "module", "scripts": { "start": "node .", - "test": "node test/test-basic.js && node test/test-read.js", + "test": "node test/test-basic.js && node test/test-read.js && node test/test-create.js", "test:basic": "node test/test-basic.js", "test:read": "node test/test-read.js", + "test:create": "node test/test-create.js", "prepublishOnly": "npm test" }, "keywords": [