UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

394 lines (348 loc) 11.3 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/fs/watchers.js import { AbortError, UVException, codes as __codes__, } from "nstdlib/lib/internal/errors"; import { createDeferredPromise, kEmptyObject } from "nstdlib/lib/internal/util"; import { kFsStatsFieldsNumber, StatWatcher as _StatWatcher, } from "nstdlib/stub/binding/fs"; import { FSEvent } from "nstdlib/stub/binding/fs_event_wrap"; import { UV_ENOSPC } from "nstdlib/stub/binding/uv"; import { EventEmitter } from "nstdlib/lib/events"; import { getStatsFromBinding, getValidatedPath, } from "nstdlib/lib/internal/fs/utils"; import { defaultTriggerAsyncIdScope, symbols as __symbols__, } from "nstdlib/lib/internal/async_hooks"; import { toNamespacedPath } from "nstdlib/lib/path"; import { validateAbortSignal, validateBoolean, validateObject, validateUint32, } from "nstdlib/lib/internal/validators"; import { Buffer as __Buffer__ } from "nstdlib/lib/buffer"; import * as assert from "nstdlib/lib/internal/assert"; import * as __hoisted_internal_event_target__ from "nstdlib/lib/internal/event_target"; const { ERR_INVALID_ARG_VALUE } = __codes__; const { owner_symbol } = __symbols__; const { isEncoding } = __Buffer__; const kOldStatus = Symbol("kOldStatus"); const kUseBigint = Symbol("kUseBigint"); const kFSWatchStart = Symbol("kFSWatchStart"); const kFSStatWatcherStart = Symbol("kFSStatWatcherStart"); const KFSStatWatcherRefCount = Symbol("KFSStatWatcherRefCount"); const KFSStatWatcherMaxRefCount = Symbol("KFSStatWatcherMaxRefCount"); const kFSStatWatcherAddOrCleanRef = Symbol("kFSStatWatcherAddOrCleanRef"); function emitStop(self) { self.emit("stop"); } function StatWatcher(bigint) { Function.prototype.call.call(EventEmitter, this); this._handle = null; this[kOldStatus] = -1; this[kUseBigint] = bigint; this[KFSStatWatcherRefCount] = 1; this[KFSStatWatcherMaxRefCount] = 1; } Object.setPrototypeOf(StatWatcher.prototype, EventEmitter.prototype); Object.setPrototypeOf(StatWatcher, EventEmitter); function onchange(newStatus, stats) { const self = this[owner_symbol]; if ( self[kOldStatus] === -1 && newStatus === -1 && stats[2 /* new nlink */] === stats[16 /* old nlink */] ) { return; } self[kOldStatus] = newStatus; self.emit( "change", getStatsFromBinding(stats), getStatsFromBinding(stats, kFsStatsFieldsNumber), ); } // At the moment if filename is undefined, we // 1. Throw an Error if it's the first // time Symbol('kFSStatWatcherStart') is called // 2. Return silently if Symbol('kFSStatWatcherStart') has already been called // on a valid filename and the wrap has been initialized // This method is a noop if the watcher has already been started. StatWatcher.prototype[kFSStatWatcherStart] = function ( filename, persistent, interval, ) { if (this._handle !== null) return; this._handle = new _StatWatcher(this[kUseBigint]); this._handle[owner_symbol] = this; this._handle.onchange = onchange; if (!persistent) this.unref(); // uv_fs_poll is a little more powerful than ev_stat but we curb it for // the sake of backwards compatibility. this[kOldStatus] = -1; filename = getValidatedPath(filename, "filename"); validateUint32(interval, "interval"); const err = this._handle.start(toNamespacedPath(filename), interval); if (err) { const error = new UVException({ errno: err, syscall: "watch", path: filename, }); error.filename = filename; throw error; } }; // To maximize backward-compatibility for the end user, // a no-op stub method has been added instead of // totally removing StatWatcher.prototype.start. // This should not be documented. StatWatcher.prototype.start = () => {}; // FIXME(joyeecheung): this method is not documented while there is // another documented fs.unwatchFile(). The counterpart in // FSWatcher is .close() // This method is a noop if the watcher has not been started. StatWatcher.prototype.stop = function () { if (this._handle === null) return; defaultTriggerAsyncIdScope( this._handle.getAsyncId(), process.nextTick, emitStop, this, ); this._handle.close(); this._handle = null; }; // Clean up or add ref counters. StatWatcher.prototype[kFSStatWatcherAddOrCleanRef] = function (operate) { if (operate === "add") { // Add a Ref this[KFSStatWatcherRefCount]++; this[KFSStatWatcherMaxRefCount]++; } else if (operate === "clean") { // Clean up a single this[KFSStatWatcherMaxRefCount]--; this.unref(); } else if (operate === "cleanAll") { // Clean up all this[KFSStatWatcherMaxRefCount] = 0; this[KFSStatWatcherRefCount] = 0; this._handle?.unref(); } }; StatWatcher.prototype.ref = function () { // Avoid refCount calling ref multiple times causing unref to have no effect. if (this[KFSStatWatcherRefCount] === this[KFSStatWatcherMaxRefCount]) return this; if (this._handle && this[KFSStatWatcherRefCount]++ === 0) this._handle.ref(); return this; }; StatWatcher.prototype.unref = function () { // Avoid refCount calling unref multiple times causing ref to have no effect. if (this[KFSStatWatcherRefCount] === 0) return this; if (this._handle && --this[KFSStatWatcherRefCount] === 0) this._handle.unref(); return this; }; function FSWatcher() { Function.prototype.call.call(EventEmitter, this); this._handle = new FSEvent(); this._handle[owner_symbol] = this; this._handle.onchange = (status, eventType, filename) => { // TODO(joyeecheung): we may check self._handle.initialized here // and return if that is false. This allows us to avoid firing the event // after the handle is closed, and to fire both UV_RENAME and UV_CHANGE // if they are set by libuv at the same time. if (status < 0) { if (this._handle !== null) { // We don't use this.close() here to avoid firing the close event. this._handle.close(); this._handle = null; // Make the handle garbage collectable. } const error = new UVException({ errno: status, syscall: "watch", path: filename, }); error.filename = filename; this.emit("error", error); } else { this.emit("change", eventType, filename); } }; } Object.setPrototypeOf(FSWatcher.prototype, EventEmitter.prototype); Object.setPrototypeOf(FSWatcher, EventEmitter); // At the moment if filename is undefined, we // 1. Throw an Error if it's the first time Symbol('kFSWatchStart') is called // 2. Return silently if Symbol('kFSWatchStart') has already been called // on a valid filename and the wrap has been initialized // 3. Return silently if the watcher has already been closed // This method is a noop if the watcher has already been started. FSWatcher.prototype[kFSWatchStart] = function ( filename, persistent, recursive, encoding, ) { if (this._handle === null) { // closed return; } assert(this._handle instanceof FSEvent, "handle must be a FSEvent"); if (this._handle.initialized) { // already started return; } filename = getValidatedPath(filename, "filename"); const err = this._handle.start( toNamespacedPath(filename), persistent, recursive, encoding, ); if (err) { const error = new UVException({ errno: err, syscall: "watch", path: filename, message: err === UV_ENOSPC ? "System limit for number of file watchers reached" : "", }); error.filename = filename; throw error; } }; // To maximize backward-compatibility for the end user, // a no-op stub method has been added instead of // totally removing FSWatcher.prototype.start. // This should not be documented. FSWatcher.prototype.start = () => {}; // This method is a noop if the watcher has not been started or // has already been closed. FSWatcher.prototype.close = function () { if (this._handle === null) { // closed return; } assert(this._handle instanceof FSEvent, "handle must be a FSEvent"); if (!this._handle.initialized) { // not started return; } this._handle.close(); this._handle = null; // Make the handle garbage collectable. process.nextTick(emitCloseNT, this); }; FSWatcher.prototype.ref = function () { if (this._handle) this._handle.ref(); return this; }; FSWatcher.prototype.unref = function () { if (this._handle) this._handle.unref(); return this; }; function emitCloseNT(self) { self.emit("close"); } // Legacy alias on the C++ wrapper object. This is not public API, so we may // want to runtime-deprecate it at some point. There's no hurry, though. Object.defineProperty(FSEvent.prototype, "owner", { __proto__: null, get() { return this[owner_symbol]; }, set(v) { return (this[owner_symbol] = v); }, }); let kResistStopPropagation; async function* watch(filename, options = kEmptyObject) { const path = toNamespacedPath(getValidatedPath(filename)); validateObject(options, "options"); const { persistent = true, recursive = false, encoding = "utf8", signal, } = options; validateBoolean(persistent, "options.persistent"); validateBoolean(recursive, "options.recursive"); validateAbortSignal(signal, "options.signal"); if (encoding && !isEncoding(encoding)) { const reason = "is invalid encoding"; throw new ERR_INVALID_ARG_VALUE(encoding, "encoding", reason); } if (signal?.aborted) throw new AbortError(undefined, { cause: signal?.reason }); const handle = new FSEvent(); let { promise, resolve, reject } = createDeferredPromise(); const oncancel = () => { handle.close(); reject(new AbortError(undefined, { cause: signal?.reason })); }; try { if (signal) { kResistStopPropagation ??= __hoisted_internal_event_target__.kResistStopPropagation; signal.addEventListener("abort", oncancel, { __proto__: null, once: true, [kResistStopPropagation]: true, }); } handle.onchange = (status, eventType, filename) => { if (status < 0) { const error = new UVException({ errno: status, syscall: "watch", path: filename, }); error.filename = filename; handle.close(); reject(error); return; } resolve({ eventType, filename }); }; const err = handle.start(path, persistent, recursive, encoding); if (err) { const error = new UVException({ errno: err, syscall: "watch", path: filename, message: err === UV_ENOSPC ? "System limit for number of file watchers reached" : "", }); error.filename = filename; handle.close(); throw error; } while (!signal?.aborted) { yield await promise; ({ promise, resolve, reject } = createDeferredPromise()); } throw new AbortError(undefined, { cause: signal?.reason }); } finally { handle.close(); signal?.removeEventListener("abort", oncancel); } } export { FSWatcher }; export { StatWatcher }; export { kFSWatchStart }; export { kFSStatWatcherStart }; export { kFSStatWatcherAddOrCleanRef }; export { watch };