grepmax 0.26.72 → 0.26.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,6 +58,7 @@ const walker_1 = require("../index/walker");
58
58
  const watcher_1 = require("../index/watcher");
59
59
  const watcher_errors_1 = require("../index/watcher-errors");
60
60
  const watcher_ignore_1 = require("../index/watcher-ignore");
61
+ const watcher_recovery_1 = require("../index/watcher-recovery");
61
62
  const file_utils_1 = require("../utils/file-utils");
62
63
  const logger_1 = require("../utils/logger");
63
64
  const project_registry_1 = require("../utils/project-registry");
@@ -97,6 +98,14 @@ function abortableDelay(ms, signal) {
97
98
  * thin watchProject/unwatchProject delegators over this manager.
98
99
  */
99
100
  class WatcherManager {
101
+ recovery(root) {
102
+ let tracker = this.recoveryStates.get(root);
103
+ if (!tracker) {
104
+ tracker = new watcher_recovery_1.WatcherRecoveryTracker();
105
+ this.recoveryStates.set(root, tracker);
106
+ }
107
+ return tracker;
108
+ }
100
109
  constructor(deps) {
101
110
  this.deps = deps;
102
111
  this.pendingOps = new Set();
@@ -117,14 +126,15 @@ class WatcherManager {
117
126
  this.degradedRoots = new Set();
118
127
  this.terminalFailures = new Map();
119
128
  this.watchLifecycles = new Map();
129
+ this.recoveryStates = new Map();
120
130
  this.subscriptionUpdates = new Map();
121
131
  this.watcherGenerations = new Map();
122
132
  }
123
133
  health(root) {
124
- var _a;
125
- var _b, _c;
134
+ var _a, _b;
135
+ var _c, _d;
126
136
  return {
127
- failedFiles: (_b = (_a = this.terminalFailures.get(root)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0,
137
+ failedFiles: (_c = (_a = this.terminalFailures.get(root)) === null || _a === void 0 ? void 0 : _a.size) !== null && _c !== void 0 ? _c : 0,
128
138
  degraded: this.isRootDegraded(root),
129
139
  watcherMode: this.pollIntervals.has(root)
130
140
  ? "polling"
@@ -135,8 +145,9 @@ class WatcherManager {
135
145
  : "native",
136
146
  catchupRunning: this.catchups.has(root),
137
147
  lastReconciledAt: this.reconciledAt.get(root),
138
- overflowCount: (_c = this.overflowCounts.get(root)) !== null && _c !== void 0 ? _c : 0,
148
+ overflowCount: (_d = this.overflowCounts.get(root)) !== null && _d !== void 0 ? _d : 0,
139
149
  catchupMs: this.catchupDurations.get(root),
150
+ watcherRecovery: (_b = this.recoveryStates.get(root)) === null || _b === void 0 ? void 0 : _b.snapshot(),
140
151
  };
141
152
  }
142
153
  watchProject(root_1) {
@@ -325,8 +336,9 @@ class WatcherManager {
325
336
  }
326
337
  catch (_c) { }
327
338
  this.deps.subscriptions.delete(root);
339
+ this.recovery(root).detached();
328
340
  }
329
- const { subscription: sub } = yield (0, watcher_ignore_1.subscribeWithNativeExclusions)(root, (err, events) => {
341
+ const { subscription: sub, policy } = yield (0, watcher_ignore_1.subscribeWithNativeExclusions)(root, (err, events) => {
330
342
  var _a, _b;
331
343
  var _c, _d;
332
344
  if (this.deps.processors.get(root) !== processor ||
@@ -334,6 +346,7 @@ class WatcherManager {
334
346
  ((_b = (_a = this.deps).getShuttingDown) === null || _b === void 0 ? void 0 : _b.call(_a)))
335
347
  return;
336
348
  if (err) {
349
+ this.recovery(root).error(err);
337
350
  this.overflowCounts.set(root, ((_c = this.overflowCounts.get(root)) !== null && _c !== void 0 ? _c : 0) + 1);
338
351
  const now = Date.now();
339
352
  if (!this.lastWatcherErrorLogMs.has(root) ||
@@ -366,6 +379,7 @@ class WatcherManager {
366
379
  return;
367
380
  }
368
381
  this.deps.subscriptions.set(root, sub);
382
+ this.recovery(root).attached(policy);
369
383
  });
370
384
  }
371
385
  requestOverflowCatchup(root, processor) {
@@ -409,6 +423,7 @@ class WatcherManager {
409
423
  if (sub) {
410
424
  sub.unsubscribe().catch(() => { });
411
425
  this.deps.subscriptions.delete(root);
426
+ this.recovery(root).detached();
412
427
  }
413
428
  if (!this.pollIntervals.has(root)) {
414
429
  console.error(`[daemon:${name}] Native watcher failed after ${fails} terminal errors — switching to poll mode (${POLL_INTERVAL_MS / 60000}min interval)`);
@@ -583,186 +598,199 @@ class WatcherManager {
583
598
  var _a, e_1, _b, _c;
584
599
  var _d;
585
600
  var _e, _f, _g;
586
- const scanStart = Date.now();
587
- const overflowCountAtStart = (_e = this.overflowCounts.get(root)) !== null && _e !== void 0 ? _e : 0;
588
- const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
589
- const metaCache = this.deps.getMetaCache();
590
- const vectorDb = this.deps.getVectorDb();
591
- processor.filePolicy.invalidateIgnoreCache();
592
- const rootPrefix = root.endsWith("/") ? root : `${root}/`;
593
- const cachedPaths = yield metaCache.getKeysWithPrefix(rootPrefix);
594
- const vectorPaths = yield vectorDb.getDistinctPathsForPrefix(rootPrefix);
595
- const knownPaths = new Set([...cachedPaths, ...vectorPaths]);
596
- if (signal.aborted)
597
- return false;
598
- const seenPaths = new Set();
599
- const walkState = (0, walker_1.createWalkState)();
600
- let queued = 0;
601
- let skipped = 0;
602
- let debugSamples = 0;
601
+ const tracker = this.recovery(root);
602
+ const scan = tracker.beginScan();
603
+ let outcome = "failed";
603
604
  try {
604
- for (var _h = true, _j = __asyncValues((0, walker_1.walk)(root, {
605
- policy: processor.filePolicy,
606
- state: walkState,
607
- })), _k; _k = yield _j.next(), _a = _k.done, !_a; _h = true) {
608
- _c = _k.value;
609
- _h = false;
610
- const relPath = _c;
611
- if (signal.aborted)
612
- return false;
613
- const absPath = path.join(root, relPath);
614
- try {
615
- const classification = yield processor.filePolicy.classifyFile(absPath);
616
- if (classification.status === "error") {
617
- walkState.protectedPaths.add(classification.protectedPath);
618
- walkState.errors.push({
619
- path: classification.protectedPath,
620
- error: classification.error,
621
- });
622
- continue;
623
- }
624
- if (classification.status !== "indexable")
625
- continue;
626
- const stats = classification.stat;
627
- seenPaths.add(absPath);
628
- let cached = metaCache.get(absPath);
629
- const reconciliation = (0, cache_coherence_1.reconcileMetaEntry)(absPath, cached, vectorPaths.has(absPath));
630
- if (reconciliation.action === "stamp") {
631
- cached = reconciliation.entry;
632
- metaCache.put(absPath, cached);
633
- }
634
- else if (reconciliation.action === "reprocess") {
635
- processor.handleFileEvent("change", absPath, {
636
- forceReprocess: true,
637
- workKind: "catchup",
638
- });
639
- queued++;
640
- if (queued % 500 === 0) {
641
- (0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
642
- yield abortableDelay(5000, signal);
605
+ const scanStart = Date.now();
606
+ const overflowCountAtStart = (_e = this.overflowCounts.get(root)) !== null && _e !== void 0 ? _e : 0;
607
+ const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
608
+ const metaCache = this.deps.getMetaCache();
609
+ const vectorDb = this.deps.getVectorDb();
610
+ processor.filePolicy.invalidateIgnoreCache();
611
+ const rootPrefix = root.endsWith("/") ? root : `${root}/`;
612
+ const cachedPaths = yield metaCache.getKeysWithPrefix(rootPrefix);
613
+ const vectorPaths = yield vectorDb.getDistinctPathsForPrefix(rootPrefix);
614
+ const knownPaths = new Set([...cachedPaths, ...vectorPaths]);
615
+ if (signal.aborted)
616
+ return false;
617
+ const seenPaths = new Set();
618
+ const walkState = (0, walker_1.createWalkState)();
619
+ let queued = 0;
620
+ let skipped = 0;
621
+ let debugSamples = 0;
622
+ try {
623
+ for (var _h = true, _j = __asyncValues((0, walker_1.walk)(root, {
624
+ policy: processor.filePolicy,
625
+ state: walkState,
626
+ })), _k; _k = yield _j.next(), _a = _k.done, !_a; _h = true) {
627
+ _c = _k.value;
628
+ _h = false;
629
+ const relPath = _c;
630
+ if (signal.aborted)
631
+ return false;
632
+ const absPath = path.join(root, relPath);
633
+ try {
634
+ const classification = yield processor.filePolicy.classifyFile(absPath);
635
+ if (classification.status === "error") {
636
+ walkState.protectedPaths.add(classification.protectedPath);
637
+ walkState.errors.push({
638
+ path: classification.protectedPath,
639
+ error: classification.error,
640
+ });
641
+ continue;
643
642
  }
644
- continue;
645
- }
646
- if (!isFileCached(cached, stats)) {
647
- // Fast path: if only mtime changed but size is identical and we have a hash,
648
- // just verify the hash in-process instead of sending to a worker.
649
- if ((cached === null || cached === void 0 ? void 0 : cached.hash) && cached.size === stats.size) {
650
- const observed = Object.assign({}, cached);
651
- const snapshot = yield (0, file_utils_1.readFileSnapshot)(absPath, {
652
- projectRoot: root,
643
+ if (classification.status !== "indexable")
644
+ continue;
645
+ const stats = classification.stat;
646
+ seenPaths.add(absPath);
647
+ let cached = metaCache.get(absPath);
648
+ const reconciliation = (0, cache_coherence_1.reconcileMetaEntry)(absPath, cached, vectorPaths.has(absPath));
649
+ if (reconciliation.action === "stamp") {
650
+ cached = reconciliation.entry;
651
+ metaCache.put(absPath, cached);
652
+ }
653
+ else if (reconciliation.action === "reprocess") {
654
+ processor.handleFileEvent("change", absPath, {
655
+ forceReprocess: true,
656
+ workKind: "catchup",
653
657
  });
654
- if (snapshot.size !== stats.size ||
655
- snapshot.mtimeMs !== stats.mtimeMs) {
656
- processor.handleFileEvent("change", absPath, {
657
- workKind: "catchup",
658
- });
659
- continue;
658
+ queued++;
659
+ if (queued % 500 === 0) {
660
+ (0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
661
+ yield abortableDelay(5000, signal);
660
662
  }
661
- const hash = (0, file_utils_1.computeContentHash)(snapshot.buffer, absPath);
662
- if (hash === observed.hash) {
663
- // Content unchanged — update mtime in cache and skip worker
664
- if (signal.aborted)
665
- return false;
666
- // A live batch can finish while this read yields. Never replace
667
- // newer metadata or resurrect a cache entry it removed.
668
- const current = metaCache.get(absPath);
669
- if (current &&
670
- current.hash === observed.hash &&
671
- current.mtimeMs === observed.mtimeMs &&
672
- current.size === observed.size &&
673
- current.hashVersion === observed.hashVersion &&
674
- current.hasVectors === observed.hasVectors) {
675
- metaCache.put(absPath, Object.assign(Object.assign({}, observed), { mtimeMs: stats.mtimeMs }));
663
+ continue;
664
+ }
665
+ if (!isFileCached(cached, stats)) {
666
+ // Fast path: if only mtime changed but size is identical and we have a hash,
667
+ // just verify the hash in-process instead of sending to a worker.
668
+ if ((cached === null || cached === void 0 ? void 0 : cached.hash) && cached.size === stats.size) {
669
+ const observed = Object.assign({}, cached);
670
+ const snapshot = yield (0, file_utils_1.readFileSnapshot)(absPath, {
671
+ projectRoot: root,
672
+ });
673
+ if (snapshot.size !== stats.size ||
674
+ snapshot.mtimeMs !== stats.mtimeMs) {
675
+ processor.handleFileEvent("change", absPath, {
676
+ workKind: "catchup",
677
+ });
678
+ continue;
679
+ }
680
+ const hash = (0, file_utils_1.computeContentHash)(snapshot.buffer, absPath);
681
+ if (hash === observed.hash) {
682
+ // Content unchanged — update mtime in cache and skip worker
683
+ if (signal.aborted)
684
+ return false;
685
+ // A live batch can finish while this read yields. Never replace
686
+ // newer metadata or resurrect a cache entry it removed.
687
+ const current = metaCache.get(absPath);
688
+ if (current &&
689
+ current.hash === observed.hash &&
690
+ current.mtimeMs === observed.mtimeMs &&
691
+ current.size === observed.size &&
692
+ current.hashVersion === observed.hashVersion &&
693
+ current.hasVectors === observed.hasVectors) {
694
+ metaCache.put(absPath, Object.assign(Object.assign({}, observed), { mtimeMs: stats.mtimeMs }));
695
+ }
696
+ skipped++;
697
+ continue;
676
698
  }
677
- skipped++;
678
- continue;
699
+ }
700
+ // Debug: log first few misses to diagnose re-queue loops
701
+ if (debugSamples < 5) {
702
+ (0, logger_1.debug)("catchup", `miss ${relPath}: cached=${cached ? `mtime=${Math.trunc(cached.mtimeMs)} size=${cached.size}` : "null"} stat=mtime=${Math.trunc(stats.mtimeMs)} size=${stats.size}`);
703
+ debugSamples++;
704
+ }
705
+ if (signal.aborted)
706
+ return false;
707
+ processor.handleFileEvent("change", absPath, {
708
+ workKind: "catchup",
709
+ });
710
+ queued++;
711
+ // Throttle: pause periodically during large catchup scans to let the
712
+ // batch processor drain and compaction run between bursts.
713
+ if (queued % 500 === 0) {
714
+ (0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
715
+ yield abortableDelay(5000, signal);
679
716
  }
680
717
  }
681
- // Debug: log first few misses to diagnose re-queue loops
682
- if (debugSamples < 5) {
683
- (0, logger_1.debug)("catchup", `miss ${relPath}: cached=${cached ? `mtime=${Math.trunc(cached.mtimeMs)} size=${cached.size}` : "null"} stat=mtime=${Math.trunc(stats.mtimeMs)} size=${stats.size}`);
684
- debugSamples++;
685
- }
686
- if (signal.aborted)
687
- return false;
688
- processor.handleFileEvent("change", absPath, { workKind: "catchup" });
689
- queued++;
690
- // Throttle: pause periodically during large catchup scans to let the
691
- // batch processor drain and compaction run between bursts.
692
- if (queued % 500 === 0) {
693
- (0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
694
- yield abortableDelay(5000, signal);
718
+ else {
719
+ skipped++;
695
720
  }
696
721
  }
697
- else {
698
- skipped++;
722
+ catch (error) {
723
+ walkState.protectedPaths.add(absPath);
724
+ walkState.errors.push({ path: absPath, error });
699
725
  }
700
726
  }
701
- catch (error) {
702
- walkState.protectedPaths.add(absPath);
703
- walkState.errors.push({ path: absPath, error });
727
+ }
728
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
729
+ finally {
730
+ try {
731
+ if (!_h && !_a && (_b = _j.return)) yield _b.call(_j);
704
732
  }
733
+ finally { if (e_1) throw e_1.error; }
705
734
  }
706
- }
707
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
708
- finally {
709
- try {
710
- if (!_h && !_a && (_b = _j.return)) yield _b.call(_j);
735
+ (0, logger_1.debug)("catchup", `${path.basename(root)}: ${queued} queued, ${skipped} skipped (cached ok), ${seenPaths.size} total`);
736
+ // Purge files deleted while daemon was offline
737
+ let purged = 0;
738
+ for (const cachedPath of knownPaths) {
739
+ if (signal.aborted)
740
+ return false;
741
+ if (!seenPaths.has(cachedPath) &&
742
+ !(0, walker_1.isPathProtectedByWalkState)(cachedPath, walkState)) {
743
+ processor.handleFileEvent("unlink", cachedPath, {
744
+ forceDelete: true,
745
+ workKind: "cleanup",
746
+ });
747
+ purged++;
748
+ }
711
749
  }
712
- finally { if (e_1) throw e_1.error; }
713
- }
714
- (0, logger_1.debug)("catchup", `${path.basename(root)}: ${queued} queued, ${skipped} skipped (cached ok), ${seenPaths.size} total`);
715
- // Purge files deleted while daemon was offline
716
- let purged = 0;
717
- for (const cachedPath of knownPaths) {
718
- if (signal.aborted)
719
- return false;
720
- if (!seenPaths.has(cachedPath) &&
721
- !(0, walker_1.isPathProtectedByWalkState)(cachedPath, walkState)) {
722
- processor.handleFileEvent("unlink", cachedPath, {
723
- forceDelete: true,
724
- workKind: "cleanup",
750
+ const complete = walkState.rootComplete && walkState.errors.length === 0;
751
+ if (!complete) {
752
+ this.degradedRoots.add(root);
753
+ (0, watcher_store_1.registerWatcher)({
754
+ pid: process.pid,
755
+ projectRoot: root,
756
+ startTime: Date.now(),
757
+ status: "degraded",
758
+ lastHeartbeat: Date.now(),
759
+ lastError: `${walkState.errors.length} scan path(s) incomplete`,
725
760
  });
726
- purged++;
727
761
  }
762
+ else {
763
+ this.degradedRoots.delete(root);
764
+ const failedFiles = (_f = (_d = this.terminalFailures.get(root)) === null || _d === void 0 ? void 0 : _d.size) !== null && _f !== void 0 ? _f : 0;
765
+ (0, watcher_store_1.registerWatcher)(Object.assign({ pid: process.pid, projectRoot: root, startTime: Date.now(), status: this.isRootDegraded(root) ? "degraded" : "watching", lastHeartbeat: Date.now() }, (failedFiles > 0
766
+ ? {
767
+ lastError: `${failedFiles} file(s) exhausted automatic retries`,
768
+ }
769
+ : {})));
770
+ }
771
+ if (queued > 0 || purged > 0) {
772
+ const parts = [];
773
+ if (queued > 0)
774
+ parts.push(`${queued} changed`);
775
+ if (purged > 0)
776
+ parts.push(`${purged} deleted`);
777
+ console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
778
+ }
779
+ const ended = Date.now();
780
+ this.lastCatchupEndMs.set(root, ended);
781
+ this.catchupDurations.set(root, ended - scanStart);
782
+ if (complete) {
783
+ this.reconciledAt.set(root, ended);
784
+ // A later gap still needs its deferred scan, even if this walk succeeded.
785
+ if (((_g = this.overflowCounts.get(root)) !== null && _g !== void 0 ? _g : 0) === overflowCountAtStart)
786
+ this.overflowPendingRoots.delete(root);
787
+ }
788
+ outcome = complete ? "complete" : "incomplete";
789
+ return complete;
728
790
  }
729
- const complete = walkState.rootComplete && walkState.errors.length === 0;
730
- if (!complete) {
731
- this.degradedRoots.add(root);
732
- (0, watcher_store_1.registerWatcher)({
733
- pid: process.pid,
734
- projectRoot: root,
735
- startTime: Date.now(),
736
- status: "degraded",
737
- lastHeartbeat: Date.now(),
738
- lastError: `${walkState.errors.length} scan path(s) incomplete`,
739
- });
740
- }
741
- else {
742
- this.degradedRoots.delete(root);
743
- const failedFiles = (_f = (_d = this.terminalFailures.get(root)) === null || _d === void 0 ? void 0 : _d.size) !== null && _f !== void 0 ? _f : 0;
744
- (0, watcher_store_1.registerWatcher)(Object.assign({ pid: process.pid, projectRoot: root, startTime: Date.now(), status: this.isRootDegraded(root) ? "degraded" : "watching", lastHeartbeat: Date.now() }, (failedFiles > 0
745
- ? { lastError: `${failedFiles} file(s) exhausted automatic retries` }
746
- : {})));
747
- }
748
- if (queued > 0 || purged > 0) {
749
- const parts = [];
750
- if (queued > 0)
751
- parts.push(`${queued} changed`);
752
- if (purged > 0)
753
- parts.push(`${purged} deleted`);
754
- console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
755
- }
756
- const ended = Date.now();
757
- this.lastCatchupEndMs.set(root, ended);
758
- this.catchupDurations.set(root, ended - scanStart);
759
- if (complete) {
760
- this.reconciledAt.set(root, ended);
761
- // A later gap still needs its deferred scan, even if this walk succeeded.
762
- if (((_g = this.overflowCounts.get(root)) !== null && _g !== void 0 ? _g : 0) === overflowCountAtStart)
763
- this.overflowPendingRoots.delete(root);
791
+ finally {
792
+ tracker.finishScan(scan, signal.aborted ? "aborted" : outcome);
764
793
  }
765
- return complete;
766
794
  });
767
795
  }
768
796
  runCatchup(root, processor) {
@@ -792,9 +820,19 @@ class WatcherManager {
792
820
  }
793
821
  } while (active.dirty && !signal.aborted);
794
822
  });
795
- const run = (this.deps.runProjectOperation
796
- ? this.deps.runProjectOperation(root, "watch-catchup", active.controller.signal, execute)
797
- : execute(active.controller.signal)).finally(() => {
823
+ const tracker = this.recovery(root);
824
+ const scansBeforeAttempt = tracker.snapshot().scanCount;
825
+ const run = (() => __awaiter(this, void 0, void 0, function* () {
826
+ try {
827
+ return yield (this.deps.runProjectOperation
828
+ ? this.deps.runProjectOperation(root, "watch-catchup", active.controller.signal, execute)
829
+ : execute(active.controller.signal));
830
+ }
831
+ catch (error) {
832
+ tracker.attemptFailed(tracker.snapshot().scanCount > scansBeforeAttempt, active.controller.signal.aborted);
833
+ throw error;
834
+ }
835
+ }))().finally(() => {
798
836
  if (this.catchups.get(root) === active)
799
837
  this.catchups.delete(root);
800
838
  });
@@ -855,6 +893,7 @@ class WatcherManager {
855
893
  this.overflowPendingRoots.delete(root);
856
894
  this.degradedRoots.delete(root);
857
895
  this.terminalFailures.delete(root);
896
+ this.recoveryStates.delete(root);
858
897
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
859
898
  console.log(`[daemon] Unwatched ${root}`);
860
899
  });
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WatcherRecoveryTracker = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const watcher_errors_1 = require("./watcher-errors");
6
+ /** Fixed-size, process/lifecycle-local evidence. A completed walk covers only
7
+ * errors already observed when it began; queued ingestion is reported separately. */
8
+ class WatcherRecoveryTracker {
9
+ constructor() {
10
+ this.gapCount = 0;
11
+ this.terminalErrorCount = 0;
12
+ this.coveredGapCount = 0;
13
+ this.coveredTerminalErrorCount = 0;
14
+ this.scanCount = 0;
15
+ this.scanIncomplete = false;
16
+ }
17
+ error(error) {
18
+ if ((0, watcher_errors_1.isFSEventsGap)(error)) {
19
+ this.gapCount++;
20
+ this.lastGapAt = Date.now();
21
+ }
22
+ else {
23
+ this.terminalErrorCount++;
24
+ this.lastTerminalErrorAt = Date.now();
25
+ }
26
+ }
27
+ beginScan() {
28
+ const scan = {
29
+ id: ++this.scanCount,
30
+ startedAt: Date.now(),
31
+ gapCountAtStart: this.gapCount,
32
+ terminalErrorCountAtStart: this.terminalErrorCount,
33
+ outcome: "running",
34
+ };
35
+ this.lastScan = scan;
36
+ return scan;
37
+ }
38
+ finishScan(scan, outcome) {
39
+ scan.finishedAt = Date.now();
40
+ scan.outcome = outcome;
41
+ if (outcome === "complete") {
42
+ this.scanIncomplete = false;
43
+ this.coveredGapCount = Math.max(this.coveredGapCount, scan.gapCountAtStart);
44
+ this.coveredTerminalErrorCount = Math.max(this.coveredTerminalErrorCount, scan.terminalErrorCountAtStart);
45
+ this.lastCompleteScan = Object.assign({}, scan);
46
+ }
47
+ else
48
+ this.scanIncomplete = true;
49
+ }
50
+ attemptFailed(scanStarted, aborted) {
51
+ this.scanIncomplete = true;
52
+ this.lastAttemptFailure = { at: Date.now(), scanStarted, aborted };
53
+ }
54
+ attached(policy) {
55
+ // The subscription helper returns the exact accepted policy, including its
56
+ // fallback. Summarize that value; never rediscover directories for health.
57
+ const patterns = JSON.parse(policy);
58
+ const literalCount = patterns.filter((pattern) => !pattern.startsWith("/") && !pattern.includes("*")).length;
59
+ this.exclusions = {
60
+ attached: true,
61
+ literalCount,
62
+ filterCount: patterns.length - literalCount,
63
+ fingerprint: (0, node_crypto_1.createHash)("sha256").update(policy).digest("hex"),
64
+ };
65
+ }
66
+ detached() {
67
+ if (this.exclusions)
68
+ this.exclusions.attached = false;
69
+ }
70
+ snapshot() {
71
+ var _a;
72
+ return {
73
+ gapCount: this.gapCount,
74
+ terminalErrorCount: this.terminalErrorCount,
75
+ coveredGapCount: this.coveredGapCount,
76
+ outstandingGapCount: this.gapCount - this.coveredGapCount,
77
+ reconciliationNeeded: this.scanIncomplete ||
78
+ ((_a = this.lastScan) === null || _a === void 0 ? void 0 : _a.outcome) === "running" ||
79
+ this.gapCount > this.coveredGapCount ||
80
+ this.terminalErrorCount > this.coveredTerminalErrorCount,
81
+ lastGapAt: this.lastGapAt,
82
+ lastTerminalErrorAt: this.lastTerminalErrorAt,
83
+ scanCount: this.scanCount,
84
+ lastScan: this.lastScan && Object.assign({}, this.lastScan),
85
+ lastCompleteScan: this.lastCompleteScan && Object.assign({}, this.lastCompleteScan),
86
+ lastAttemptFailure: this.lastAttemptFailure && Object.assign({}, this.lastAttemptFailure),
87
+ exclusions: this.exclusions && Object.assign({}, this.exclusions),
88
+ };
89
+ }
90
+ }
91
+ exports.WatcherRecoveryTracker = WatcherRecoveryTracker;