fix: track pending folder reservations
This commit is contained in:
+13
-2
@@ -30,6 +30,7 @@ class FolderMonitor extends EventEmitter {
|
|||||||
this._settings = null;
|
this._settings = null;
|
||||||
this._seenFiles = new Set();
|
this._seenFiles = new Set();
|
||||||
this._batchBuffer = [];
|
this._batchBuffer = [];
|
||||||
|
this._batchSeenReservations = new Set();
|
||||||
this._batchTimer = null;
|
this._batchTimer = null;
|
||||||
this._initialScopes = new Set();
|
this._initialScopes = new Set();
|
||||||
this._reconcileTimer = null;
|
this._reconcileTimer = null;
|
||||||
@@ -189,8 +190,9 @@ class FolderMonitor extends EventEmitter {
|
|||||||
_invalidateLifecycle(paused) {
|
_invalidateLifecycle(paused) {
|
||||||
this._generation++;
|
this._generation++;
|
||||||
if (paused) {
|
if (paused) {
|
||||||
for (const filePath of this._batchBuffer) this._seenFiles.delete(this._normalizePath(filePath));
|
for (const normalized of this._batchSeenReservations) this._seenFiles.delete(normalized);
|
||||||
}
|
}
|
||||||
|
this._batchSeenReservations.clear();
|
||||||
this._paused = paused;
|
this._paused = paused;
|
||||||
this._scanning = false;
|
this._scanning = false;
|
||||||
this._followUpRequested = false;
|
this._followUpRequested = false;
|
||||||
@@ -213,7 +215,11 @@ class FolderMonitor extends EventEmitter {
|
|||||||
|
|
||||||
_onNewFile(filePath, generation) {
|
_onNewFile(filePath, generation) {
|
||||||
if (!this._acceptCallback(generation)) return;
|
if (!this._acceptCallback(generation)) return;
|
||||||
if (!this._classifyPath(filePath, this._settings).allowed || !this._acceptPath(filePath)) return;
|
if (!this._classifyPath(filePath, this._settings).allowed) return;
|
||||||
|
const normalized = this._normalizePath(filePath);
|
||||||
|
const ownsSeenReservation = !!this._settings?.skipDuplicates && !this._seenFiles.has(normalized);
|
||||||
|
if (!this._acceptPath(filePath)) return;
|
||||||
|
if (ownsSeenReservation) this._batchSeenReservations.add(normalized);
|
||||||
this._batchBuffer.push(filePath);
|
this._batchBuffer.push(filePath);
|
||||||
if (this._batchTimer) this._clearTimeout(this._batchTimer);
|
if (this._batchTimer) this._clearTimeout(this._batchTimer);
|
||||||
this._batchTimer = this._setTimeout(() => {
|
this._batchTimer = this._setTimeout(() => {
|
||||||
@@ -222,6 +228,11 @@ class FolderMonitor extends EventEmitter {
|
|||||||
this._batchTimer = null;
|
this._batchTimer = null;
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
const listenerError = this._emitEvent('new-files', [files], generation);
|
const listenerError = this._emitEvent('new-files', [files], generation);
|
||||||
|
for (const emittedPath of files) {
|
||||||
|
const emittedNormalized = this._normalizePath(emittedPath);
|
||||||
|
if (!this._batchSeenReservations.delete(emittedNormalized)) continue;
|
||||||
|
if (listenerError) this._seenFiles.delete(emittedNormalized);
|
||||||
|
}
|
||||||
if (listenerError && this._acceptCallback(generation)) {
|
if (listenerError && this._acceptCallback(generation)) {
|
||||||
this._lastError = 'Ordnerüberwachung fehlgeschlagen';
|
this._lastError = 'Ordnerüberwachung fehlgeschlagen';
|
||||||
this._emitStatus(generation);
|
this._emitStatus(generation);
|
||||||
|
|||||||
@@ -374,6 +374,42 @@ test('watcher add paused before batch timeout is emitted exactly once by resume
|
|||||||
assert.equal(monitor.status().seenCount, 1);
|
assert.equal(monitor.status().seenCount, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('pause rollback never deletes historical seen state from a dedupe-off batch', async () => {
|
||||||
|
const timers = createManualTimers();
|
||||||
|
const watchers = [];
|
||||||
|
const newFiles = [];
|
||||||
|
const filePath = 'C:\\watch\\historical.mkv';
|
||||||
|
let discoverExisting = false;
|
||||||
|
const monitor = new FolderMonitor({
|
||||||
|
watch: () => {
|
||||||
|
const watcher = new EventEmitter();
|
||||||
|
watcher.close = async () => {};
|
||||||
|
watchers.push(watcher);
|
||||||
|
return watcher;
|
||||||
|
},
|
||||||
|
access: async () => {},
|
||||||
|
walkFolder: async () => discoverExisting ? [{ path: filePath, name: 'historical.mkv', size: 1 }] : [],
|
||||||
|
stat: async () => ({ mtimeMs: 1 }),
|
||||||
|
...timers
|
||||||
|
});
|
||||||
|
monitor.on('new-files', (files) => newFiles.push(files));
|
||||||
|
const dedupeOn = { folderPath: 'C:\\watch', extensions: 'mkv', skipDuplicates: true, reconcileIntervalMinutes: 5 };
|
||||||
|
const dedupeOff = { ...dedupeOn, skipDuplicates: false };
|
||||||
|
monitor.start(dedupeOn);
|
||||||
|
watchers[0].emit('add', filePath);
|
||||||
|
await timers.runTimeouts();
|
||||||
|
assert.deepEqual(newFiles, [[filePath]]);
|
||||||
|
await monitor.pause();
|
||||||
|
await monitor.resume(dedupeOff);
|
||||||
|
watchers[1].emit('add', filePath);
|
||||||
|
assert.deepEqual(newFiles, [[filePath]]);
|
||||||
|
await monitor.pause();
|
||||||
|
discoverExisting = true;
|
||||||
|
await monitor.resume(dedupeOn);
|
||||||
|
assert.deepEqual(newFiles, [[filePath]]);
|
||||||
|
assert.equal(monitor.status().seenCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('dry scan leaves the public status byte-identical and does not consume reconnect state', async () => {
|
test('dry scan leaves the public status byte-identical and does not consume reconnect state', async () => {
|
||||||
let reachable = false;
|
let reachable = false;
|
||||||
const timers = createManualTimers();
|
const timers = createManualTimers();
|
||||||
|
|||||||
Reference in New Issue
Block a user