UNPKG

@socketsupply/socket

Version:

A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.

260 lines (221 loc) 5.62 kB
import { AsyncResource } from '../async/resource.js' import { EventEmitter } from '../events.js' import { FileHandle } from './handle.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 * @param {Watcher} watcher * @return {Promise} */ async function start (watcher) { // throws if not accessible await FileHandle.access(watcher.path) if (!watcher.id || typeof watcher.id !== 'string') { throw new TypeError('Expectig fs.Watcher to have a id.') } const result = await ipc.request('fs.watch', { path: watcher.path, id: watcher.id }) if (result.err) { throw result.err } } /** * Internal watcher data event listeer. * @ignore * @param {Watcher} watcher * @return {function} */ function listen (watcher, resource) { return hooks.onData((event) => { const { data, source } = event.detail.params if (source !== 'fs.watch') { return } if (BigInt(data.id) !== BigInt(watcher.id)) { return } const { path, events } = data resource.runInAsyncScope(() => { watcher.emit('change', events[0], encodeFilename(watcher, path)) }) }) } /** * A container for a file system path watcher. */ export class Watcher extends EventEmitter { /** * The underlying `fs.Watcher` resource id. * @ignore * @type {string} */ id = null /** * The path the `fs.Watcher` is watching * @type {string} */ path = null /** * `true` if closed, otherwise `false. * @type {boolean} */ 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 * @type {function?} */ stopListening = null #resource = null /** * `Watcher` class constructor. * @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 = 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 this.#resource = new AsyncResource('FileSystemWatcher') this.#resource.handle = this 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() } } /** * Internal starter for watcher. * @ignore */ async start () { try { await start(this) if (typeof this.stopListening === 'function') { this.stopListening() } this.stopListening = listen(this, this.#resource) } catch (err) { this.#resource.runInAsyncScope(() => { this.emit('error', err) }) } } /** * Closes watcher and stops listening for changes. * @return {Promise} */ async close () { if (typeof this.stopListening === 'function') { this.stopListening() this.stopListening = null } this.closed = true const result = await ipc.request('fs.stopWatch', { id: this.id }) if (result.err) { throw result.err } } /** * Implements `gc.finalizer` for gc'd resource cleanup. * @ignore * @return {gc.Finalizer} */ [gc.finalizer] (options) { return { args: [this.id, this.stopListening, options], handle (id, stopListening) { if (typeof stopListening === 'function') { stopListening() } console.warn('Closing fs.Watcher on garbage collection') ipc.request('fs.stopWatch', { id }, options) } } } /** * Implements the `AsyncIterator` (`Symbol.asyncIterator`) iterface. * @ignore * @return {AsyncIterator<{ eventType: string, filename: string }>} */ [Symbol.asyncIterator] () { let watcher = this return { async next () { if (watcher?.aborted) { throw new AbortError(watcher.signal) } if (watcher.closed) { watcher = null return { done: true, value: null } } const event = await new Promise((resolve) => { watcher.once('change', (eventType, filename) => { resolve({ eventType, filename }) }) }) return { done: false, value: event } } } } } export default Watcher