CustomStatWatcher.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.CustomStatWatcher = exports.assertStatus = exports.Status = exports.Event = void 0;
  4. const tslib_1 = require("tslib");
  5. const events_1 = require("events");
  6. const statUtils = tslib_1.__importStar(require("../../statUtils"));
  7. var Event;
  8. (function (Event) {
  9. Event["Change"] = "change";
  10. Event["Stop"] = "stop";
  11. })(Event = exports.Event || (exports.Event = {}));
  12. var Status;
  13. (function (Status) {
  14. Status["Ready"] = "ready";
  15. Status["Running"] = "running";
  16. Status["Stopped"] = "stopped";
  17. })(Status = exports.Status || (exports.Status = {}));
  18. function assertStatus(current, expected) {
  19. if (current !== expected) {
  20. throw new Error(`Invalid StatWatcher status: expected '${expected}', got '${current}'`);
  21. }
  22. }
  23. exports.assertStatus = assertStatus;
  24. class CustomStatWatcher extends events_1.EventEmitter {
  25. static create(fakeFs, path, opts) {
  26. const statWatcher = new CustomStatWatcher(fakeFs, path, opts);
  27. statWatcher.start();
  28. return statWatcher;
  29. }
  30. constructor(fakeFs, path, { bigint = false } = {}) {
  31. super();
  32. this.status = Status.Ready;
  33. this.changeListeners = new Map();
  34. this.startTimeout = null;
  35. this.fakeFs = fakeFs;
  36. this.path = path;
  37. this.bigint = bigint;
  38. this.lastStats = this.stat();
  39. }
  40. start() {
  41. assertStatus(this.status, Status.Ready);
  42. this.status = Status.Running;
  43. // Node allows other listeners to be registered up to 3 milliseconds
  44. // after the watcher has been started, so that's what we're doing too
  45. this.startTimeout = setTimeout(() => {
  46. this.startTimeout = null;
  47. // Per the Node FS docs:
  48. // "When an fs.watchFile operation results in an ENOENT error,
  49. // it will invoke the listener once, with all the fields zeroed
  50. // (or, for dates, the Unix Epoch)."
  51. if (!this.fakeFs.existsSync(this.path)) {
  52. this.emit(Event.Change, this.lastStats, this.lastStats);
  53. }
  54. }, 3);
  55. }
  56. stop() {
  57. assertStatus(this.status, Status.Running);
  58. this.status = Status.Stopped;
  59. if (this.startTimeout !== null) {
  60. clearTimeout(this.startTimeout);
  61. this.startTimeout = null;
  62. }
  63. this.emit(Event.Stop);
  64. }
  65. stat() {
  66. try {
  67. return this.fakeFs.statSync(this.path, { bigint: this.bigint });
  68. }
  69. catch (error) {
  70. // From observation, all errors seem to be mostly ignored by Node.
  71. // Checked with ENOENT, ENOTDIR, EPERM
  72. const statInstance = this.bigint
  73. ? new statUtils.BigIntStatsEntry()
  74. : new statUtils.StatEntry();
  75. return statUtils.clearStats(statInstance);
  76. }
  77. }
  78. /**
  79. * Creates an interval whose callback compares the current stats with the previous stats and notifies all listeners in case of changes.
  80. *
  81. * @param opts.persistent Decides whether the interval should be immediately unref-ed.
  82. */
  83. makeInterval(opts) {
  84. const interval = setInterval(() => {
  85. const currentStats = this.stat();
  86. const previousStats = this.lastStats;
  87. if (statUtils.areStatsEqual(currentStats, previousStats))
  88. return;
  89. this.lastStats = currentStats;
  90. this.emit(Event.Change, currentStats, previousStats);
  91. }, opts.interval);
  92. return opts.persistent ? interval : interval.unref();
  93. }
  94. /**
  95. * Registers a listener and assigns it an interval.
  96. */
  97. registerChangeListener(listener, opts) {
  98. this.addListener(Event.Change, listener);
  99. this.changeListeners.set(listener, this.makeInterval(opts));
  100. }
  101. /**
  102. * Unregisters the listener and clears the assigned interval.
  103. */
  104. unregisterChangeListener(listener) {
  105. this.removeListener(Event.Change, listener);
  106. const interval = this.changeListeners.get(listener);
  107. if (typeof interval !== `undefined`)
  108. clearInterval(interval);
  109. this.changeListeners.delete(listener);
  110. }
  111. /**
  112. * Unregisters all listeners and clears all assigned intervals.
  113. */
  114. unregisterAllChangeListeners() {
  115. for (const listener of this.changeListeners.keys()) {
  116. this.unregisterChangeListener(listener);
  117. }
  118. }
  119. hasChangeListeners() {
  120. return this.changeListeners.size > 0;
  121. }
  122. /**
  123. * Refs all stored intervals.
  124. */
  125. ref() {
  126. for (const interval of this.changeListeners.values())
  127. interval.ref();
  128. return this;
  129. }
  130. /**
  131. * Unrefs all stored intervals.
  132. */
  133. unref() {
  134. for (const interval of this.changeListeners.values())
  135. interval.unref();
  136. return this;
  137. }
  138. }
  139. exports.CustomStatWatcher = CustomStatWatcher;