From 724bdfb40e0f34f6049bb77fa4621bf8763bf115 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 15:49:18 +0000 Subject: [PATCH 1/2] feat: add isLocked() to Storage and EventStore Agent-Logs-Url: https://github.com/albe/node-event-storage/sessions/3c57c789-88b9-4872-b4d2-35c1723b5b0f Co-authored-by: albe <4259532+albe@users.noreply.github.com> --- src/EventStore.js | 11 ++++++++++ src/Storage/ReadableStorage.js | 12 +++++++++++ src/Storage/WritableStorage.js | 7 +++++-- test/EventStore.spec.js | 38 ++++++++++++++++++++++++++++++++++ test/Storage.spec.js | 37 +++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/EventStore.js b/src/EventStore.js index 5a2ad87..379eecc 100644 --- a/src/EventStore.js +++ b/src/EventStore.js @@ -221,6 +221,17 @@ class EventStore extends events.EventEmitter { this.storage.close(); } + /** + * Returns true if the storage is currently locked by a writer process. + * Useful when this store is opened read-only to check whether a writer holds an exclusive lock. + * + * @api + * @returns {boolean} + */ + isLocked() { + return this.storage.isLocked(); + } + /** * Override EventEmitter.on() to delegate 'preCommit' and 'preRead' event registrations * to the underlying storage, so that `eventstore.on('preCommit', handler)` works naturally. diff --git a/src/Storage/ReadableStorage.js b/src/Storage/ReadableStorage.js index f0a0c2a..24a1feb 100644 --- a/src/Storage/ReadableStorage.js +++ b/src/Storage/ReadableStorage.js @@ -93,6 +93,7 @@ class ReadableStorage extends events.EventEmitter { this.hmac = createHmac(config.hmacSecret); this.dataDirectory = path.resolve(config.dataDirectory); + this.lockFile = path.resolve(this.dataDirectory, this.storageFile + '.lock'); const partitionDefaults = { readBufferSize: DEFAULT_READ_BUFFER_SIZE }; this.partitionConfig = Object.assign(partitionDefaults, config); @@ -157,6 +158,17 @@ class ReadableStorage extends events.EventEmitter { return this.index.length; } + /** + * Returns true if the storage is currently locked by a writer process. + * Useful for read-only clients to check whether a writer holds an exclusive lock. + * + * @api + * @returns {boolean} + */ + isLocked() { + return fs.existsSync(this.lockFile); + } + /** * Scan partitions and secondary index files; emit 'index-created' for each found index. * @param {function} done Called when both scans finish. diff --git a/src/Storage/WritableStorage.js b/src/Storage/WritableStorage.js index 4030c5e..857ae7a 100644 --- a/src/Storage/WritableStorage.js +++ b/src/Storage/WritableStorage.js @@ -61,7 +61,6 @@ class WritableStorage extends ReadableStorage { ensureDirectory(config.dataDirectory); super(storageName, config); - this.lockFile = path.resolve(this.dataDirectory, this.storageFile + '.lock'); this._lockMode = config.lock; this.partitioner = config.partitioner; } @@ -226,14 +225,18 @@ class WritableStorage extends ReadableStorage { if (this.locked) { return false; } + if (this.isLocked()) { + throw new StorageLockedError(`Storage ${this.storageFile} is locked by another process`); + } try { fs.mkdirSync(this.lockFile); this.locked = true; } catch (e) { - /* istanbul ignore if */ + /* istanbul ignore next */ if (e.code !== 'EEXIST') { throw new Error(`Error creating lock for storage ${this.storageFile}: ` + e.message); } + /* istanbul ignore next */ throw new StorageLockedError(`Storage ${this.storageFile} is locked by another process`); } return true; diff --git a/test/EventStore.spec.js b/test/EventStore.spec.js index d70fec3..e451e9d 100644 --- a/test/EventStore.spec.js +++ b/test/EventStore.spec.js @@ -277,6 +277,44 @@ describe('EventStore', function() { }); }); + it('isLocked() returns true when a writer is open', function(done) { + eventstore = new EventStore({ + storageDirectory + }); + + eventstore.on('ready', () => { + const readstore = new EventStore({ + storageDirectory, + readOnly: true + }); + readstore.on('ready', () => { + expect(readstore.isLocked()).to.be(true); + readstore.close(); + done(); + }); + }); + }); + + it('isLocked() returns false when no writer is open', function(done) { + eventstore = new EventStore({ + storageDirectory + }); + + eventstore.on('ready', () => { + eventstore.close(); + const readstore = new EventStore({ + storageDirectory, + readOnly: true + }); + readstore.on('ready', () => { + expect(readstore.isLocked()).to.be(false); + readstore.close(); + eventstore = null; + done(); + }); + }); + }); + describe('commit', function() { it('throws when no stream name specified', function() { diff --git a/test/Storage.spec.js b/test/Storage.spec.js index bee3dfb..9a56c41 100644 --- a/test/Storage.spec.js +++ b/test/Storage.spec.js @@ -1225,6 +1225,43 @@ describe('Storage', function() { }).to.not.throwError(); }); + it('isLocked() returns true when a writer holds the lock', function(){ + storage = createStorage(); + storage.open(); + const reader = createReader(); + reader.open(); + expect(reader.isLocked()).to.be(true); + reader.close(); + }); + + it('isLocked() returns false when no writer holds the lock', function(){ + storage = createStorage(); + storage.open(); + storage.close(); + const reader = createReader(); + reader.open(); + expect(reader.isLocked()).to.be(false); + reader.close(); + }); + + it('isLocked() returns false on a writer before open', function(){ + storage = createStorage(); + expect(storage.isLocked()).to.be(false); + }); + + it('isLocked() returns true on a writer after open', function(){ + storage = createStorage(); + storage.open(); + expect(storage.isLocked()).to.be(true); + }); + + it('isLocked() returns false on a writer after close', function(){ + storage = createStorage(); + storage.open(); + storage.close(); + expect(storage.isLocked()).to.be(false); + }); + it('allows multiple readers for one storage', function () { storage = createStorage(); storage.open(); From 250db53fcdaf81a6dbb80e1306d3eae94d91d28a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 15:50:42 +0000 Subject: [PATCH 2/2] test: use once for one-shot ready event assertions Agent-Logs-Url: https://github.com/albe/node-event-storage/sessions/3c57c789-88b9-4872-b4d2-35c1723b5b0f Co-authored-by: albe <4259532+albe@users.noreply.github.com> --- test/EventStore.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/EventStore.spec.js b/test/EventStore.spec.js index e451e9d..940d4d4 100644 --- a/test/EventStore.spec.js +++ b/test/EventStore.spec.js @@ -282,12 +282,12 @@ describe('EventStore', function() { storageDirectory }); - eventstore.on('ready', () => { + eventstore.once('ready', () => { const readstore = new EventStore({ storageDirectory, readOnly: true }); - readstore.on('ready', () => { + readstore.once('ready', () => { expect(readstore.isLocked()).to.be(true); readstore.close(); done(); @@ -300,13 +300,13 @@ describe('EventStore', function() { storageDirectory }); - eventstore.on('ready', () => { + eventstore.once('ready', () => { eventstore.close(); const readstore = new EventStore({ storageDirectory, readOnly: true }); - readstore.on('ready', () => { + readstore.once('ready', () => { expect(readstore.isLocked()).to.be(false); readstore.close(); eventstore = null;