@zenfs/core
Version:
A filesystem, anywhere
163 lines (162 loc) • 4.63 kB
JavaScript
import { EventEmitter } from 'eventemitter3';
import { UV } from 'kerium';
import { contextOf } from '../internal/contexts.js';
import { isStatsEqual } from '../node/stats.js';
import { statSync } from '../node/sync.js';
import { basename, dirname, join, relative } from '../path.js';
import { normalizePath } from '../utils.js';
/**
* Base class for file system watchers.
* Provides event handling capabilities for watching file system changes.
*
* @template TEvents The type of events emitted by the watcher.
*/
class Watcher extends EventEmitter {
_context;
path;
off(event, fn, context, once) {
return super.off(event, fn, context, once);
}
removeListener(event, fn, context, once) {
return super.removeListener(event, fn, context, once);
}
constructor(
/**
* @internal
*/
_context, path) {
super();
this._context = _context;
this.path = path;
}
setMaxListeners() {
throw UV('ENOSYS', 'Watcher.setMaxListeners');
}
getMaxListeners() {
throw UV('ENOSYS', 'Watcher.getMaxListeners');
}
prependListener() {
throw UV('ENOSYS', 'Watcher.prependListener');
}
prependOnceListener() {
throw UV('ENOSYS', 'Watcher.prependOnceListener');
}
rawListeners() {
throw UV('ENOSYS', 'Watcher.rawListeners');
}
ref() {
return this;
}
unref() {
return this;
}
}
/**
* Watches for changes on the file system.
*
* @template T The type of the filename, either `string` or `Buffer`.
*/
export class FSWatcher extends Watcher {
options;
realpath;
constructor(context, path, options) {
const $ = contextOf(context);
super($, path);
this.options = options;
this.realpath = join($.root, path);
addWatcher(this.realpath, this);
}
close() {
super.emit('close');
removeWatcher(this.realpath, this);
}
[Symbol.dispose]() {
this.close();
}
}
/**
* Watches for changes to a file's stats.
*
* Instances of `StatWatcher` are used by `fs.watchFile()` to monitor changes to a file's statistics.
*/
export class StatWatcher extends Watcher {
options;
intervalId;
previous;
constructor(context, path, options) {
super(context, path);
this.options = options;
this.start();
}
onInterval() {
try {
const current = statSync(this.path);
if (!isStatsEqual(this.previous, current)) {
this.emit('change', current, this.previous);
this.previous = current;
}
}
catch (e) {
this.emit('error', e);
}
}
start() {
const interval = this.options.interval || 5000;
try {
this.previous = statSync(this.path);
}
catch (e) {
this.emit('error', e);
return;
}
this.intervalId = setInterval(this.onInterval.bind(this), interval);
if (!this.options.persistent && typeof this.intervalId == 'object') {
this.intervalId.unref();
}
}
/**
* @internal
*/
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
this.removeAllListeners();
}
}
const watchers = new Map();
export function addWatcher(path, watcher) {
const normalizedPath = normalizePath(path);
if (!watchers.has(normalizedPath)) {
watchers.set(normalizedPath, new Set());
}
watchers.get(normalizedPath).add(watcher);
}
export function removeWatcher(path, watcher) {
const normalizedPath = normalizePath(path);
if (watchers.has(normalizedPath)) {
watchers.get(normalizedPath).delete(watcher);
if (watchers.get(normalizedPath).size === 0) {
watchers.delete(normalizedPath);
}
}
}
/**
* @internal @hidden
*/
export function emitChange(context, eventType, filename) {
const $ = contextOf(context);
if ($)
filename = join($.root ?? '/', filename);
filename = normalizePath(filename);
// Notify watchers, including ones on parent directories if they are watching recursively
for (let path = filename; path != '/'; path = dirname(path)) {
const watchersForPath = watchers.get(path);
if (!watchersForPath)
continue;
for (const watcher of watchersForPath) {
watcher.emit('change', eventType, relative.call(watcher._context, path, filename) || basename(filename));
}
}
}