diff --git a/api/README.md b/api/README.md index 6d5a6a0fdc..0932c5b26c 100644 --- a/api/README.md +++ b/api/README.md @@ -1025,7 +1025,7 @@ Unlinks (removes) file at `path`. | options.signal | AbortSignal? | | true | | | callback | function(Error?) | | false | | -## [`watch(, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L860) +## [`watch(, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L861) Watch for changes at `path` calling `callback` @@ -1033,6 +1033,7 @@ Watch for changes at `path` calling `callback` | :--- | :--- | :---: | :---: | :--- | | (Position 0) | string | | false | | | options | function \| object | | true | | +| options.encoding | string | utf8 | true | | | callback | ?function | | true | | | Return Value | Type | Description | @@ -1330,7 +1331,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw | :--- | :--- | :--- | | Not specified | Promise | | -## [`watch(, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L496) +## [`watch(, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L497) Watch for changes at `path` calling `callback` @@ -1338,6 +1339,7 @@ Watch for changes at `path` calling `callback` | :--- | :--- | :---: | :---: | :--- | | (Position 0) | string | | false | | | options | function \| object | | true | | +| options.encoding | string | utf8 | true | | | options.signal | AbortSignal | | true | | | Return Value | Type | Description | diff --git a/api/fs/index.js b/api/fs/index.js index 262cfd2e42..1b13910118 100644 --- a/api/fs/index.js +++ b/api/fs/index.js @@ -854,6 +854,7 @@ export function writeFile (path, data, options, callback) { * Watch for changes at `path` calling `callback` * @param {string} * @param {function|object=} [options] + * @param {string=} [options.encoding = 'utf8'] * @param {?function} [callback] * @return {Watcher} */ diff --git a/api/fs/promises.js b/api/fs/promises.js index 0c35f234ce..cce96dc2ee 100644 --- a/api/fs/promises.js +++ b/api/fs/promises.js @@ -490,6 +490,7 @@ export async function writeFile (path, data, options) { * Watch for changes at `path` calling `callback` * @param {string} * @param {function|object=} [options] + * @param {string=} [options.encoding = 'utf8'] * @param {AbortSignal=} [options.signal] * @return {Watcher} */ diff --git a/api/fs/watcher.js b/api/fs/watcher.js index 3f2c376425..120d675083 100644 --- a/api/fs/watcher.js +++ b/api/fs/watcher.js @@ -1,9 +1,30 @@ import { EventEmitter } from '../events.js' +import { AbortError } from '../errors.js' import { rand64 } from '../crypto.js' +import { Buffer } from '../buffer.js' import hooks from '../hooks.js' import ipc from '../ipc.js' import gc from '../gc.js' +/** + * Encodes filename based on encoding preference. + * @ignore + * @param {Watcher} watcher + * @param {string} filename + * @return {string|Buffer} + */ +function encodeFilename (watcher, filename) { + if (!watcher.encoding || watcher.encoding === 'utf8') { + return filename.toString() + } + + if (watcher.encoding === 'buffer') { + return Buffer.from(filename.toString()) + } + + return filename +} + /** * Starts the `fs.Watcher` * @ignore @@ -24,7 +45,7 @@ async function start (watcher) { /** * Internal watcher data event listeer. * @ignore - * @param {Watcher} + * @param {Watcher} watcher * @return {function} */ function listen (watcher) { @@ -40,7 +61,7 @@ function listen (watcher) { const { path, events } = data - watcher.emit('change', events[0], path) + watcher.emit('change', events[0], encodeFilename(watcher, path)) }) } @@ -67,6 +88,24 @@ export class Watcher extends EventEmitter { */ closed = false + /** + * `true` if aborted, otherwise `false`. + * @type {boolean} + */ + aborted = false + + /** + * The encoding of the `filename` + * @type {'utf8'|'buffer'} + */ + encoding = 'utf8' + + /** + * A `AbortController` `AbortSignal` for async aborts. + * @type {AbortSignal?} + */ + signal = null + /** * Internal event listener cancellation. * @ignore @@ -79,16 +118,36 @@ export class Watcher extends EventEmitter { * @ignore * @param {string} path * @param {object=} [options] + * @param {AbortSignal=} [options.signal} * @param {string|number|bigint=} [options.id] + * @param {string=} [options.encoding = 'utf8'] */ - constructor (path, options = {}) { + constructor (path, options = null) { super() this.id = options?.id || String(rand64()) this.path = path + this.signal = options?.signal || null + this.aborted = this.signal?.aborted === true + this.encoding = options?.encoding || this.encoding gc.ref(this) + if (this.signal?.aborted) { + throw new AbortError(this.signal) + } + + if (typeof this.signal?.addEventListener === 'function') { + this.signal.addEventListener('abort', async () => { + this.aborted = true + try { + await this.close() + } catch (err) { + console.warn('Failed to close fs.Watcher in AbortSignal:', err.message) + } + }) + } + // internal if (options?.start !== false) { this.start() @@ -155,14 +214,20 @@ export class Watcher extends EventEmitter { * @return {AsyncIterator<{ eventType: string, filename: string }>} */ [Symbol.asyncIterator] () { + let watcher = this return { async next () { - if (this.closed) { + if (watcher?.aborted) { + throw new AbortError(watcher.signal) + } + + if (watcher.closed) { + watcher = null return { done: true, value: null } } const event = await new Promise((resolve) => { - this.once('change', (eventType, filename) => { + watcher.once('change', (eventType, filename) => { resolve({ eventType, filename }) }) }) diff --git a/api/index.d.ts b/api/index.d.ts index afd8865fd7..3b2ef83396 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -2628,7 +2628,9 @@ declare module "socket:fs/watcher" { * @ignore * @param {string} path * @param {object=} [options] + * @param {AbortSignal=} [options.signal} * @param {string|number|bigint=} [options.id] + * @param {string=} [options.encoding = 'utf8'] */ constructor(path: string, options?: object | undefined); /** @@ -2647,6 +2649,21 @@ declare module "socket:fs/watcher" { * @type {boolean} */ closed: boolean; + /** + * `true` if aborted, otherwise `false`. + * @type {boolean} + */ + aborted: boolean; + /** + * The encoding of the `filename` + * @type {'utf8'|'buffer'} + */ + encoding: 'utf8' | 'buffer'; + /** + * A `AbortController` `AbortSignal` for async aborts. + * @type {AbortSignal?} + */ + signal: AbortSignal | null; /** * Internal event listener cancellation. * @ignore @@ -2831,6 +2848,7 @@ declare module "socket:fs/promises" { * Watch for changes at `path` calling `callback` * @param {string} * @param {function|object=} [options] + * @param {string=} [options.encoding = 'utf8'] * @param {AbortSignal=} [options.signal] * @return {Watcher} */ @@ -3060,6 +3078,7 @@ declare module "socket:fs/index" { * Watch for changes at `path` calling `callback` * @param {string} * @param {function|object=} [options] + * @param {string=} [options.encoding = 'utf8'] * @param {?function} [callback] * @return {Watcher} */