NodeFileSystemObserver.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.NodeFileSystemObserver = void 0;
  4. const fs_fsa_1 = require("@jsonjoy.com/fs-fsa");
  5. const NodeFileSystemDirectoryHandle_1 = require("./NodeFileSystemDirectoryHandle");
  6. const NodeFileSystemFileHandle_1 = require("./NodeFileSystemFileHandle");
  7. const util_1 = require("./util");
  8. /**
  9. * A `FileSystemObserver` implementation backed by the underlying Node.js-like
  10. * `fs.watch`. This is a best-effort profile, per the File System Observer
  11. * proposal's allowance for local file systems: `rename` events are classified
  12. * into `"appeared"`/`"disappeared"` records by stat-ing the path, no
  13. * `"moved"` records are ever produced (pairing renames is unreliable — same
  14. * as Chrome on Windows), and a backend watcher error surfaces as a terminal
  15. * `"errored"` record for that observation.
  16. *
  17. * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemObserver
  18. */
  19. class NodeFileSystemObserver {
  20. constructor(fs, callback) {
  21. this.fs = fs;
  22. this.callback = callback;
  23. this._observations = new Map();
  24. this._records = [];
  25. this._flushScheduled = false;
  26. }
  27. async observe(handle, options) {
  28. const path = handle.__path;
  29. const ctx = (handle.ctx ?? handle._ctx);
  30. if (typeof path !== 'string' || !ctx || (ctx.separator !== '/' && ctx.separator !== '\\'))
  31. throw new TypeError("Failed to execute 'observe' on 'FileSystemObserver': Invalid handle.");
  32. const isDirectory = handle.kind === 'directory';
  33. const last = path[path.length - 1];
  34. const isTrimmableSeparator = path.length > 1 && (last === '/' || last === '\\') && path[path.length - 2] !== ':';
  35. const target = isTrimmableSeparator ? path.slice(0, -1) : path;
  36. try {
  37. await this.fs.promises.stat(target);
  38. }
  39. catch (error) {
  40. if (error && typeof error === 'object') {
  41. switch (error.code) {
  42. case 'ENOENT':
  43. throw (0, util_1.newNotFoundError)();
  44. case 'EACCES':
  45. case 'EPERM':
  46. throw (0, util_1.newNotAllowedError)();
  47. }
  48. }
  49. throw error;
  50. }
  51. const recursive = isDirectory && !!options?.recursive;
  52. const watcher = this.fs.watch(target, { recursive }, (eventType, filename) => {
  53. void this.onEvent(handle, watcher, target, isDirectory, ctx, eventType, filename ? String(filename) : '');
  54. });
  55. watcher.on('error', () => {
  56. if (this._observations.get(handle) !== watcher)
  57. return;
  58. this._observations.delete(handle);
  59. watcher.close();
  60. this._enqueue(new fs_fsa_1.FileSystemChangeRecord(handle, 'errored', null, []));
  61. });
  62. this._observations.get(handle)?.close();
  63. this._observations.set(handle, watcher);
  64. }
  65. unobserve(handle) {
  66. const watcher = this._observations.get(handle);
  67. if (!watcher)
  68. return;
  69. watcher.close();
  70. this._observations.delete(handle);
  71. }
  72. /** Disconnect and stop all observations. */
  73. disconnect() {
  74. for (const watcher of this._observations.values())
  75. watcher.close();
  76. this._observations.clear();
  77. this._records = [];
  78. }
  79. async onEvent(root, watcher, rootPath, isDirectory, ctx, eventType, filename) {
  80. const sep = ctx.separator;
  81. const steps = isDirectory && filename ? filename.split(sep) : [];
  82. const absolute = isDirectory ? (rootPath === sep ? rootPath + filename : rootPath + sep + filename) : rootPath;
  83. let stats = null;
  84. try {
  85. stats = (await this.fs.promises.stat(absolute));
  86. }
  87. catch {
  88. stats = null;
  89. }
  90. if (this._observations.get(root) !== watcher)
  91. return;
  92. if (eventType === 'rename') {
  93. if (stats) {
  94. this._enqueue(new fs_fsa_1.FileSystemChangeRecord(root, 'appeared', this._handle(absolute, stats, ctx), steps));
  95. }
  96. else {
  97. this._enqueue(new fs_fsa_1.FileSystemChangeRecord(root, 'disappeared', null, steps));
  98. }
  99. }
  100. else if (stats) {
  101. this._enqueue(new fs_fsa_1.FileSystemChangeRecord(root, 'modified', this._handle(absolute, stats, ctx), steps));
  102. }
  103. }
  104. _handle(absolute, stats, ctx) {
  105. return stats.isDirectory()
  106. ? new NodeFileSystemDirectoryHandle_1.NodeFileSystemDirectoryHandle(this.fs, absolute, ctx)
  107. : new NodeFileSystemFileHandle_1.NodeFileSystemFileHandle(this.fs, absolute, ctx);
  108. }
  109. _enqueue(record) {
  110. this._records.push(record);
  111. if (this._flushScheduled)
  112. return;
  113. this._flushScheduled = true;
  114. queueMicrotask(() => {
  115. this._flushScheduled = false;
  116. const records = this._records;
  117. this._records = [];
  118. if (records.length)
  119. this.callback(records, this);
  120. });
  121. }
  122. }
  123. exports.NodeFileSystemObserver = NodeFileSystemObserver;
  124. //# sourceMappingURL=NodeFileSystemObserver.js.map