instrumentality 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/road.ts CHANGED
@@ -4,28 +4,26 @@ import * as fp from "node:fs/promises"
4
4
  import * as ph from "node:path"
5
5
  import * as os from "node:os"
6
6
  import * as cr from "node:crypto"
7
- import * as sp from "node:stream/promises"
8
7
  import { on } from "node:events"
9
8
  import * as bs from "./base.ts"
10
9
 
11
10
 
12
11
 
13
- /**
14
- * Subclass of {@link bs.InsErr} that represents an error thrown from this specific module of the library
15
- */
12
+ /** Subclass of {@link bs.InsErr} that represents an error thrown from this specific module of the library */
16
13
  export class RdErr extends bs.InsErr { override name = "Instrumentality-Road-Error" }
14
+ export { RdErr as RoadError }
17
15
 
18
16
 
19
17
 
20
18
  /**
21
- * Returns the constructor function corresponding to the file mode.
19
+ * Returns the constructor function corresponding to the file mode (statmode).
22
20
  *
23
- * @param statmode The file mode to check.
24
- * @returns The constructor function corresponding to the file mode.
21
+ * @param statmode_ The file mode to check.
22
+ * @returns The constructor function corresponding to the road type (e.g., {@link File}, {@link Folder}, etc.).
25
23
  * @throws If the file mode is unknown, throws a {@link RdErr}.
26
24
  */
27
- export function resolveMode(statmode: number) {
28
- switch (statmode & fsc.S_IFMT) {
25
+ export function resolveMode(statmode_: number): typeof File | typeof Folder | typeof BlockDevice | typeof CharacterDevice | typeof SymbolicLink | typeof Fifo | typeof Socket {
26
+ switch (statmode_ & fsc.S_IFMT) {
29
27
  case fsc.S_IFREG: return File
30
28
  case fsc.S_IFDIR: return Folder
31
29
  case fsc.S_IFBLK: return BlockDevice
@@ -33,50 +31,49 @@ export function resolveMode(statmode: number) {
33
31
  case fsc.S_IFLNK: return SymbolicLink
34
32
  case fsc.S_IFIFO: return Fifo
35
33
  case fsc.S_IFSOCK: return Socket
36
- default: throw new RdErr(`Unknown mode type ${statmode} (statmode is most likely corrupted)`)
34
+ default: throw new RdErr(`Unknown mode type ${statmode_} (statmode is most likely corrupted)`)
37
35
  }
38
36
  }
39
37
 
40
38
 
41
39
 
42
40
  /**
43
- * Creates a new instance of the appropriate subclass of {@link Road} based on the file mode of the specified path.
41
+ * Creates the appropriate subclass of {@link Road} based on the file mode of the specified path.
44
42
  *
45
- * @param lookFor The path to check.
46
- * @returns A new instance of the appropriate subclass of {@link Road}.
47
- * @throws If the path does not exist, throws a fs {@link Error}.
43
+ * @param path_ The path to follow.
44
+ * @returns A new instance of {@link Road}.
45
+ * @throws If {@link fp.lstat}/{@link fs.lstatSync} fails to retrieved the status of {@link path_}.
48
46
  */
49
- export function factorySync(lookFor: string) {
50
- fs.accessSync(lookFor, fsc.F_OK)
51
- return new (resolveMode(fs.lstatSync(lookFor).mode))(lookFor, false)
47
+ export async function factory(path_: string) {
48
+ return new (resolveMode((await fp.lstat(path_)).mode))(path_, false)
52
49
  }
53
- /** Async version of {@link factorySync}. */
54
- export async function factory(lookFor: string) {
55
- await fp.access(lookFor, fsc.F_OK)
56
- return new (resolveMode((await fp.lstat(lookFor)).mode))(lookFor, false)
50
+ /** Sync version of {@link factory}. */
51
+ export function factorySync(path_: string) {
52
+ return new (resolveMode(fs.lstatSync(path_).mode))(path_, false)
57
53
  }
58
54
 
59
55
 
60
56
 
61
57
  /**
62
- * A map that keeps track of locked roads to prevent concurrent modifications. The keys are the absolute paths of the roads, and the values are promises that resolve when the lock is released.
58
+ * A map that keeps track of locked roads to prevent concurrent modifications.
63
59
  *
64
- * @remarks Don't manually modify this map. Use the {@link Road.initChange} and {@link Road.initChangeSync} methods to acquire and release locks on roads.
65
- * For read-only purposes, you should use the {@link lockFor} function to await the optional lock on a road.
66
- */
67
- let lockedRoads: Map<string, Promise<void>> | null = null
68
- /**
69
- * Getter for the locked roads map.
60
+ * @key The absolute path of the road that is currently locked.
61
+ * @value A promise that resolves when the lock on the road is released.
70
62
  *
71
- * @param roadOrPath - A {@link Road} instance or a string representing the absolute path of the road to check.
72
- * @returns The promise associated with the locked road, or `undefined` if the road is not currently locked.
63
+ * @remarks The map is initialized lazily when the first lock is created to minimize import-time side effects.
64
+ * It's generally meant for read-only purposes. It's not advised to modify this map directly if there are built-in mechanisms that do the job for you as well.
73
65
  */
74
- export function lockFor(roadOrPath: Road | string) {
75
- return lockedRoads?.get(roadOrPath.toString())
76
- }
66
+ export let lockedRoads: Map<string, Promise<void>> | null = null
77
67
 
78
68
 
79
69
 
70
+ /**
71
+ * Road is an OOP, pointer-like representation of an entry in the local file system. It wraps around the Node.js fs module and provides a more convenient access to it.
72
+ * It's meant to help the user mentally model an entry and help them reason about it, as well as provide a more convenient API and guardrails for common operations.
73
+ *
74
+ * @remarks This is by no means a one-to-one mapping of the underlying file system (after initialization).
75
+ * It is more like a memory representation, similar to a pointer in low-level programming languages; other processes might mess with the underlying entry. There are methods to check for consistency, but they are not guaranteed to be foolproof.
76
+ */
80
77
  export abstract class Road {
81
78
  /** The absolute path to the file or directory that this Road instance represents.
82
79
  * @remarks Intentionally made protected to prevent external modification, as changing this value could lead to inconsistencies and unexpected behavior. */
@@ -86,141 +83,88 @@ export abstract class Road {
86
83
  mutable: boolean = true
87
84
 
88
85
  // Quick accessors
89
- /** Accessor for the absolute path to the file or directory that this Road instance represents. */
86
+ /** Copy of the absolute path. */
90
87
  get isAt() { return this.pointsTo }
91
- /** Accessor for the name of the file or directory that this Road instance represents. */
88
+ /** Name of the road without the path (including extensions). */
92
89
  get name() { return this.isAt.slice(this.isAt.lastIndexOf(ph.sep) + 1) }
93
- /** Same as {@link isAt} but for compatibility with external libraries that try to convert the object to a string. */
90
+ /** The amount of path segments in the absolute path to the file or directory represented by this Road instance, minus one (i.e., the depth of the path in the file system hierarchy). */
91
+ get depth() { return this.isAt.split(ph.sep).length - 1 }
92
+ /** Same as {@link isAt} but for compatibility with external APIs. */
94
93
  toString() { return this.isAt }
95
- /** Returns the OS file type of the file or directory.
96
- * Return value (OS type) and the type of this instance are not guaranteed to be the same, as the file system may have changed since this instance was created. */
97
- typeSync() { return (resolveMode(fs.lstatSync(this.isAt).mode)) }
98
- /** Async version of {@link typeSync}. */
99
- async type() { return (resolveMode((await fp.lstat(this.isAt)).mode)) }
100
94
 
101
95
  /**
102
- * Constructs a new Road instance representing the file or directory at the specified path.
96
+ * Creates a new instance of the Road class.
103
97
  *
104
- * @param lookFor The path to the file or directory that this Road instance will represent.
105
- * @param typeCheck Whether to check if the type of the file or directory at the specified path matches the type of this instance.
106
- * This can be skipped for performance reasons if the type is known to be correct, but it is recommended to keep it enabled for safety.
107
- * @throws If the specified path does not exist, throws a fs.{@link Error}.
108
- * @throws If the type of the file or directory at the specified path does not match the type of this instance, throws a {@link RdErr}. Useful for subclasses.
109
- */
110
- constructor(lookFor: string, typeCheck: boolean | 1 | 0) {
111
- this.pointsTo = ph.resolve(lookFor)
112
- if (typeCheck && !(this instanceof this.typeSync())) // `this` directly refers to the subclass
113
- throw new RdErr(`Type missmatch: Path '${this.isAt}' is not of constructed type ${this.constructor.name}.`)
114
- }
115
-
116
- /**
117
- * Verifies that the file or directory represented by this Road instance exists, is of the same type as this instance, and (optionally) is writable.
98
+ * @param path_ Any path-like string that can be resolved to an absolute path. It will be resolved to an absolute path using {@link ph.resolve}.
99
+ * @param typeCheck_ If true, the constructor will check if the path corresponds to the expected type of road (e.g., file, folder, etc.) and throw an error if it doesn't. If false, no type checking will be performed.
118
100
  *
119
- * @param expectMode The expected access mode for this Road other than visibility by this process.
120
- * @returns Result of the verification.
101
+ * @throws If {@link typeCheck_} is true and the path does not correspond to the expected type of road, a {@link RdErr} will be thrown.
121
102
  */
122
- async verify(expectMode: number, typeCheck: boolean): Promise<boolean> {
123
- try {
124
- await fp.access(this.isAt, fsc.F_OK | expectMode)
125
- return typeCheck || this instanceof (await this.type())
126
- } catch {
127
- return false
128
- }
129
- }
130
- /** Sync version of {@link verify}. */
131
- verifySync(expectMode: number, typeCheck: boolean): boolean {
132
- try {
133
- fs.accessSync(this.isAt, fsc.F_OK | expectMode)
134
- return typeCheck || this instanceof this.typeSync()
135
- } catch {
136
- return false
137
- }
103
+ constructor(path_: string, typeCheck_: boolean | 1 | 0) {
104
+ this.pointsTo = ph.resolve(path_)
105
+ if (typeCheck_ && !this.checkSync())
106
+ throw new RdErr(`Type mismatch: '${this.isAt}'`)
138
107
  }
139
108
 
140
109
  /**
141
- * Creates a disposable lock for the file or directory represented by this Road instance, preventing concurrent modifications.
110
+ * Aquires a lock for the path represented by this Road instance, preventing concurrent modifications from this and other Road instances pointing to the same path.
111
+ *
112
+ * @param cb_ A callback function that will be called when the lock is released. This is used internally to manage the lock state.
113
+ * @returns An object with a dispose method that releases the lock when called.
114
+ * @throws If the road is immutable, a {@link RdErr} will be thrown.
142
115
  *
143
- * @returns An object with a `dispose` method that releases the lock when called. The lock is automatically released when the object is garbage collected.
144
- * @remarks Please use this method with the `await using` statement to ensure that the lock is properly released after the operation is complete. This method is intended for internal use and shouldn't be called directly in most cases. (that's why it's protected)
116
+ * @remarks This method MUST be used with a `using` statement to ensure that the lock is released properly. Failing to do so may result in deadlocks or other concurrency issues.
117
+ * Non-deterministic release is unfortunately not possible due to the nature of JavaScript's garbage collection, thus the lock must be released deterministically by the user (to which the `using` statement is a convenient way to do so).
145
118
  */
146
- protected async initChange() {
147
- if (!await this.verify(fsc.R_OK | fsc.W_OK, true))
148
- throw new RdErr(`Road to '${this.isAt}' (${this.constructor.name}) isn't the same as during construction, can't modify (OS type: ${fs.existsSync(this.isAt) ? this.typeSync().name : 'nonexistent'})`)
119
+ protected async lock(cb_ = () => {}): Promise<AsyncDisposable & Disposable> {
149
120
  if (!this.mutable)
150
- throw new RdErr(`Attempting to modify road to '${this.isAt}' of type ${this.constructor.name} which's marked as immutable (unrelated to the actual OS file permissions)`)
151
- if (!lockedRoads)
152
- lockedRoads = new Map<string, Promise<void>>()
153
- const lockedPath = this.isAt
154
- let releaseLock = () => {}
155
- await lockFor(lockedPath)
156
- lockedRoads.set(lockedPath, new Promise<void>(res => releaseLock = res))
121
+ throw new RdErr(`Road to '${this.isAt}' is immutable.`)
122
+ lockedRoads ??= new Map<string, Promise<void>>()
123
+ const isAt = this.isAt
124
+ await lockedRoads.get(isAt) // will skip if `undefined` (no lock)
125
+ lockedRoads.set(isAt, new Promise<void>(res => cb_ = res))
157
126
  return {
158
127
  [Symbol.dispose]() {
159
- releaseLock()
160
- releaseLock = () => { throw new RdErr("Lock already released, can't dispose") }
161
- lockedRoads!.delete(lockedPath)
128
+ cb_()
129
+ lockedRoads!.delete(isAt)
162
130
  },
163
131
  async [Symbol.asyncDispose]() {
164
- releaseLock()
165
- releaseLock = () => { throw new RdErr("Lock already released, can't dispose") }
166
- lockedRoads!.delete(lockedPath)
132
+ cb_()
133
+ lockedRoads!.delete(isAt)
167
134
  }
168
135
  }
169
136
  }
170
137
  /**
171
- * Sync version of {@link initChangeSync}.
172
- *
173
- * @remarks This sync version will throw if the road is already locked by another operation.
138
+ * Sync version of {@link lock}.
139
+ * @throws Also throws a {@link RdErr} if the road is currently locked by another operation as it cannot wait for the lock to be released in a synchronous context.
174
140
  */
175
- protected initChangeSync() {
176
- if (!this.verifySync(fsc.R_OK | fsc.W_OK, true))
177
- throw new RdErr(`Road to '${this.isAt}' (${this.constructor.name}) isn't the same as during construction, can't modify (OS type: ${fs.existsSync(this.isAt) ? this.typeSync().name : 'nonexistent'})`)
141
+ protected lockSync(cb_ = () => {}): Disposable & AsyncDisposable {
178
142
  if (!this.mutable)
179
- throw new RdErr(`Attempting to modify road to '${this.isAt}' of type ${this.constructor.name} which's marked as immutable (unrelated to the actual OS file permissions)`)
180
- if (!lockedRoads)
181
- lockedRoads = new Map<string, Promise<void>>()
182
- const lockedPath = this.isAt
183
- if (lockedRoads.has(lockedPath))
184
- throw new RdErr(`Road to '${this.isAt}' is currently locked by another operation, can't modify synchronously`)
185
- let releaseLock = () => {}
186
- lockedRoads.set(lockedPath, new Promise<void>(res => releaseLock = res))
143
+ throw new RdErr(`Road to '${this.isAt}' is immutable.`)
144
+ lockedRoads ??= new Map<string, Promise<void>>()
145
+ const isAt = this.isAt
146
+ if (lockedRoads.has(isAt))
147
+ throw new RdErr(`Road to '${this.isAt}' is currently locked by another operation.`)
148
+ lockedRoads.set(isAt, new Promise<void>(res => cb_ = res))
187
149
  return {
188
150
  [Symbol.dispose]() {
189
- lockedRoads!.delete(lockedPath)
190
- releaseLock()
191
- releaseLock = () => { throw new RdErr("Lock already released, can't dispose") }
151
+ lockedRoads!.delete(isAt)
152
+ cb_()
192
153
  },
193
154
  async [Symbol.asyncDispose]() {
194
- lockedRoads!.delete(lockedPath)
195
- releaseLock()
196
- releaseLock = () => { throw new RdErr("Lock already released, can't dispose") }
155
+ lockedRoads!.delete(isAt)
156
+ cb_()
197
157
  }
198
158
  }
199
159
  }
200
160
 
161
+ /** @returns An instance of {@link Folder} representing the parent directory of the current road. */
162
+ parent(): Folder { return new Folder(ph.dirname(this.isAt), false) }
201
163
  /**
202
- * Checks if the file or directory represented by this Road is both visible and of the same type as expected.
203
- */
204
- async exists() { return this.verify(fsc.F_OK, true) }
205
- existsSync() { return this.verifySync(fsc.F_OK, true) }
206
- /**
207
- * @returns The file system stats for the file or directory.
208
- * @see {@link fs.lstat}
209
- */
210
- async stats() { return fp.lstat(this.isAt) }
211
- /**
212
- * @returns The file system stats for the file or directory.
213
- * @see {@link fs.lstatSync}
214
- */
215
- statsSync() { return fs.lstatSync(this.isAt) }
216
-
217
- /**
218
- * @returns The amount of path segments in the absolute path to the file or directory represented by this Road instance, minus one (i.e., the depth of the file or directory in the file system hierarchy).
219
- * @remarks As subclasses of {@link Road} require all paths to be absolute/normalized and valid, this method is guaranteed to return a non-negative integer.
164
+ * An async generator that yields the ancestors of the current road, starting from its parent and moving up the directory tree until it reaches the root.
165
+ *
166
+ * @yields Each ancestor folder as a {@link Folder} instance.
220
167
  */
221
- depth() { return this.isAt.split(ph.sep).length - 1 }
222
- /** @returns The parent folder of the file or directory represented by this Road instance. */
223
- parent() { return new Folder(ph.dirname(this.isAt), false) }
224
168
  *ancestorsIt() {
225
169
  let current: Folder = this.parent()
226
170
  let parent = current.parent()
@@ -230,361 +174,389 @@ export abstract class Road {
230
174
  parent = current.parent()
231
175
  }
232
176
  }
233
- ancestors() { return [...this.ancestorsIt()] }
177
+ /** @returns An array of {@link Folder} instances representing the ancestors of the current road. */
178
+ ancestors(): Folder[] { return [...this.ancestorsIt()] }
234
179
 
235
- async untilAccessible(mode = fsc.F_OK, abs: AbortSignal, onEachAttempt?: () => unknown) {
180
+ /**
181
+ * Watches the current entry for changes and resolves when the entry becomes accessible (i.e., exists and can be accessed).
182
+ *
183
+ * @param abs An {@link AbortSignal} that stops the watching process when aborted.
184
+ * @param expectMode The expected access mode for the entry, defaults to {@link fsc.F_OK} (existence check).
185
+ * @param cb_ An optional callback function that will be called with any errors encountered while checking for accessibility.
186
+ * @see {@link fp.access} on how the check for accessibility is performed.
187
+ * @see {@link fs.watch} for more information on how the watching process works.
188
+ * @see {@link on} for more information on how the event listener is set up.
189
+ */
190
+ async untilAccessible(abs: AbortSignal, expectMode = fsc.F_OK, cb_?: (err: unknown) => unknown): Promise<void> {
236
191
  const watcher = fs.watch(this.isAt)
237
192
  try {
238
- if (await this.verify(mode, true))
239
- return
240
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
241
- for await (let _ of on(watcher, 'change', { signal: abs }))
242
- if (await this.verify(mode, true))
193
+ for await (let _ of on(watcher, 'change', { signal: abs })) {
194
+ try {
195
+ await fp.access(this.isAt, expectMode)
243
196
  return
244
- else
245
- await onEachAttempt?.()
246
- } finally {
247
- watcher.close()
197
+ }
198
+ catch(err: unknown) { await cb_?.(err) }
199
+ }
248
200
  }
201
+ catch(e) { throw e }
202
+ finally { watcher.close() }
249
203
  }
250
- async onChange<T>(abs: AbortSignal, cb?: () => T) {
204
+ /**
205
+ * Watches the current entry for changes and resolves when the entry is changed.
206
+ *
207
+ * @param abs_ An {@link AbortSignal} that stops the watching process when aborted.
208
+ * @param cb_ An optional callback function that will be called when the entry is changed.
209
+ * @returns The return value of the callback function, or null if no callback is provided.
210
+ */
211
+ async onChange<T>(abs_: AbortSignal, cb_?: () => T) {
251
212
  const watcher = fs.watch(this.isAt)
252
213
  try {
253
- for await (let _ of on(watcher, 'change', { signal: abs }))
254
- return await cb?.() || null
214
+ for await (let _ of on(watcher, 'change', { signal: abs_ }))
215
+ return await cb_?.() ?? null
255
216
  return null
256
217
  }
257
218
  catch(e) { throw e }
258
219
  finally { watcher.close() }
259
220
  }
260
221
 
261
- metaSync(suffixID = "tsInstrumentalityMeta") {
262
- if (os.platform() === "win32")
263
- return fs.readFileSync(`${this.isAt}:${suffixID}`)
264
- else
265
- throw new RdErr("Extended attributes are not supported on this platform")
266
- }
267
- async meta(suffixID = "tsInstrumentalityMeta"): Promise<Record<string, unknown>> {
268
- if (os.platform() === "win32")
269
- return JSON.parse(await fp.readFile(`${this.isAt}:${suffixID}`, 'utf-8'))
270
- else
271
- throw new RdErr("Extended attributes are not supported on this platform")
272
- }
273
- setMetaSync(meta: Record<string, unknown>, suffixID = "tsInstrumentalityMeta") {
274
- using _ = this.initChangeSync()
275
- if (os.platform() === "win32")
276
- fs.writeFileSync(`${this.isAt}:${suffixID}`, JSON.stringify(meta), 'utf-8')
277
- else
278
- throw new RdErr("Extended attributes are not supported on this platform")
279
- }
280
- async setMeta(meta: Record<string, unknown>, suffixID = "tsInstrumentalityMeta") {
281
- using _ = await this.initChange()
282
- if (os.platform() === "win32")
283
- await fp.writeFile(`${this.isAt}:${suffixID}`, JSON.stringify(meta), 'utf-8')
284
- else
285
- throw new RdErr("Extended attributes are not supported on this platform")
286
- }
222
+ /** @returns The result of {@link fp.lstat} for the current entry. */
223
+ lstat() { return fp.lstat(this.isAt) }
224
+ /** @returns The result of {@link fs.lstatSync} for the current entry. */
225
+ lstatSync() { return fs.lstatSync(this.isAt) }
226
+ /** @returns The result of {@link fp.stat} for the current entry. */
227
+ stat() { return fp.stat(this.isAt) }
228
+ /** @returns The result of {@link fs.statSync} for the current entry. */
229
+ statSync() { return fs.statSync(this.isAt) }
287
230
 
288
- abstract deleteSync(): void
231
+ // jsdocs for the abstract methods are in the subclasses
289
232
  abstract delete(): Promise<void>
290
- abstract moveSync(into: Folder): void
291
- abstract move(into: Folder): Promise<void>
292
- abstract copySync(into: Folder): this
293
- abstract copy(into: Folder): Promise<this>
294
- abstract renameSync(to: string): void
295
- abstract rename(to: string): Promise<void>
296
- abstract resurrectSync(): void
297
- abstract resurrect(): Promise<void>
233
+ abstract deleteSync(): void
234
+ abstract moveSync(into_: Folder): void
235
+ abstract move(into_: Folder): Promise<void>
236
+ abstract copySync(into_: Folder): this
237
+ abstract copy(into_: Folder): Promise<this>
238
+ abstract renameSync(to_: string): void
239
+ abstract rename(to_: string): Promise<void>
240
+
241
+ // jsdocs for the abstract methods are in the subclasses
242
+ abstract check(): Promise<boolean>
243
+ abstract checkSync(): boolean
244
+
245
+ /** Type narrowing for {@link File} (similar to `instanceof` without unnecessary runtime checks). */
246
+ isFile(): this is File { return false as const }
247
+ /** Type narrowing for {@link Folder} (similar to `instanceof` without unnecessary runtime checks). */
248
+ isDir(): this is Folder { return false as const }
249
+ /** Type narrowing for {@link Folder} (similar to `instanceof` without unnecessary runtime checks). */
250
+ isFolder(): this is Folder { return false as const }
251
+ /** Type narrowing for {@link Folder} (similar to `instanceof` without unnecessary runtime checks). */
252
+ isDirectory(): this is Folder { return false as const }
253
+ /** Type narrowing for {@link Folder} (similar to `instanceof` without unnecessary runtime checks). */
254
+ isDict(): this is Folder { return false as const }
255
+ /** Type narrowing for {@link Folder} (similar to `instanceof` without unnecessary runtime checks). */
256
+ isDictionary(): this is Folder { return false as const }
257
+ /** Type narrowing for {@link SymbolicLink} (similar to `instanceof` without unnecessary runtime checks). */
258
+ isSymlink(): this is SymbolicLink { return false as const }
259
+ /** Type narrowing for {@link SymbolicLink} (similar to `instanceof` without unnecessary runtime checks). */
260
+ isSymbolicLink(): this is SymbolicLink { return false as const }
261
+ /** Type narrowing for {@link UnusableRoad} (similar to `instanceof` without unnecessary runtime checks). */
262
+ isUnusable(): this is UnusableRoad { return false as const }
263
+ /** Type narrowing for {@link BlockDevice} (similar to `instanceof` without unnecessary runtime checks). */
264
+ isBlockDevice(): this is BlockDevice { return false as const }
265
+ /** Type narrowing for {@link CharacterDevice} (similar to `instanceof` without unnecessary runtime checks). */
266
+ isCharacterDevice(): this is CharacterDevice { return false as const }
267
+ /** Type narrowing for {@link Fifo} (similar to `instanceof` without unnecessary runtime checks). */
268
+ isFifo(): this is Fifo { return false as const }
269
+ /** Type narrowing for {@link Socket} (similar to `instanceof` without unnecessary runtime checks). */
270
+ isSocket(): this is Socket { return false as const }
298
271
  }
272
+ export type road_t = ConstructorParameters<typeof Road>
299
273
 
300
274
 
301
275
 
276
+ /** Subclass of {@link Road} that represents a file. */
302
277
  export class File extends Road {
278
+ static async create(at_: string) {
279
+ try { await fp.access(at_, fsc.W_OK) }
280
+ catch { await fp.writeFile(at_, "") }
281
+ return new File(at_, true)
282
+ }
283
+ static createSync(at_: string) {
284
+ try { fs.accessSync(at_, fs.constants.W_OK) }
285
+ catch { fs.writeFileSync(at_, "") }
286
+ return new File(at_, true)
287
+ }
288
+
303
289
  get ext() { return ph.extname(this.isAt) }
304
290
  get noExt() { return ph.basename(this.isAt, this.ext) }
305
291
 
306
- static createSync(at: string) {
307
- try {
308
- fs.accessSync(at, fsc.W_OK)
309
- } catch {
310
- fs.writeFileSync(at, "")
311
- }
312
- return new File(at, false)
313
- }
314
- static async create(at: string) {
315
- try {
316
- await fp.access(at, fsc.W_OK)
317
- } catch {
318
- await fp.writeFile(at, "")
319
- }
320
- return new File(at, false)
292
+ async read(): Promise<Buffer>
293
+ async read(encoding_: BufferEncoding, flag_?: string): Promise<string>
294
+ async read(encoding_?: BufferEncoding, flag_?: string): Promise<Buffer | string> {
295
+ if (encoding_)
296
+ return fp.readFile(this.isAt, { encoding: encoding_, flag: flag_ })
297
+ else
298
+ return fp.readFile(this.isAt)
321
299
  }
322
-
323
300
  readSync(): Buffer
324
- readSync(encoding: BufferEncoding, flag?: string): string
325
- readSync(encoding?: BufferEncoding, flag?: string): Buffer | string {
326
- if (encoding)
327
- return fs.readFileSync(this.isAt, { encoding: encoding, flag: flag })
301
+ readSync(encoding_: BufferEncoding, flag_?: string): string
302
+ readSync(encoding_?: BufferEncoding, flag_?: string): Buffer | string {
303
+ if (encoding_)
304
+ return fs.readFileSync(this.isAt, { encoding: encoding_, flag: flag_ })
328
305
  else
329
306
  return fs.readFileSync(this.isAt)
330
307
  }
331
- async read(): Promise<Buffer>
332
- async read(encoding: BufferEncoding, flag?: string): Promise<string>
333
- async read(encoding?: BufferEncoding, flag?: string): Promise<Buffer | string> {
334
- if (encoding)
335
- return fp.readFile(this.isAt, { encoding: encoding, flag: flag })
336
- else
337
- return fp.readFile(this.isAt)
338
- }
339
308
 
340
- // Bizarre reading
341
- *itBuffSync(chunkSize: number = 64 * 1024, flags: string | number = 'r', mode?: fs.Mode) {
342
- const fd = fs.openSync(this.isAt, flags, mode)
309
+ async *itBuff(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode) {
310
+ const fd = await fp.open(this.isAt, flags_, mode_)
343
311
  try {
344
- const buffer = Buffer.alloc(chunkSize)
312
+ const buffer = Buffer.alloc(chunkSize_)
345
313
  let bytesRead: number
346
314
  do {
347
- bytesRead = fs.readSync(fd, buffer, 0, chunkSize, null)
315
+ const readResult = await fd.read(buffer, 0, chunkSize_, null)
316
+ bytesRead = readResult.bytesRead
348
317
  if (bytesRead > 0)
349
318
  yield buffer.subarray(0, bytesRead)
350
- } while (bytesRead === chunkSize)
319
+ } while (bytesRead === chunkSize_)
351
320
  } finally {
352
- fs.closeSync(fd)
321
+ await fd.close()
353
322
  }
354
323
  }
355
- async *itBuff(chunkSize: number = 64 * 1024, flags: string | number = 'r', mode?: fs.Mode) {
356
- const fd = await fp.open(this.isAt, flags, mode)
324
+ *itBuffSync(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode) {
325
+ const fd = fs.openSync(this.isAt, flags_, mode_)
357
326
  try {
358
- const buffer = Buffer.alloc(chunkSize)
327
+ const buffer = Buffer.alloc(chunkSize_)
359
328
  let bytesRead: number
360
329
  do {
361
- const readResult = await fd.read(buffer, 0, chunkSize, null)
362
- bytesRead = readResult.bytesRead
330
+ bytesRead = fs.readSync(fd, buffer, 0, chunkSize_, null)
363
331
  if (bytesRead > 0)
364
332
  yield buffer.subarray(0, bytesRead)
365
- } while (bytesRead === chunkSize)
333
+ } while (bytesRead === chunkSize_)
366
334
  } finally {
367
- await fd.close()
335
+ fs.closeSync(fd)
368
336
  }
369
337
  }
370
- async *itLines(options: Parameters<typeof fs.createReadStream>[1] = { encoding: 'utf-8' }) {
371
- const readStream = fs.createReadStream(this.isAt, options)
372
- const rlInterface = rl.createInterface({ input: readStream, crlfDelay: Infinity })
373
- try {
374
- for await (const line of rlInterface)
375
- yield line
376
- } finally {
377
- rlInterface.close()
378
- readStream.destroy()
379
- }
338
+
339
+ async write(data_: Buffer | string, options_?: fs.WriteFileOptions) {
340
+ using _ = await this.lock()
341
+ await fp.writeFile(this.isAt, data_, options_)
380
342
  }
381
- computeHashSync(algorithm?: string, options?: cr.HashOptions): Buffer
382
- computeHashSync(algorithm?: string, options?: cr.HashOptions, encoding?: BufferEncoding): string
383
- computeHashSync(algorithm = "sha256", options?: cr.HashOptions, encoding?: BufferEncoding): Buffer | string {
384
- const hash = cr.createHash(algorithm, options)
385
- for (const chunk of this.itBuffSync())
386
- hash.update(chunk)
387
- return encoding ? hash.digest(encoding) : hash.digest()
388
- }
389
- async computeHash(algorithm?: string, options?: cr.HashOptions): Promise<Buffer>
390
- async computeHash(algorithm?: string, options?: cr.HashOptions, encoding?: BufferEncoding): Promise<string>
391
- async computeHash(algorithm = "sha256", options?: cr.HashOptions, encoding?: BufferEncoding): Promise<Buffer | string> {
392
- const hash = cr.createHash(algorithm, options)
393
- for await (const chunk of this.itBuff())
394
- hash.update(chunk)
395
- return encoding ? hash.digest(encoding) : hash.digest()
396
- }
397
- async streamHash(algorithm = "sha256", options?: cr.HashOptions, encoding?: BufferEncoding): Promise<Buffer | string> {
398
- const hash = cr.createHash(algorithm, options)
399
- await sp.pipeline(fs.createReadStream(this.isAt), hash)
400
- return encoding ? hash.digest(encoding) : hash.digest()
401
- }
402
-
403
- writeSync(data: Buffer | string, options?: fs.WriteFileOptions) {
404
- using _ = this.initChangeSync()
405
- fs.writeFileSync(this.isAt, data, options)
406
- }
407
- async write(data: Buffer | string, options?: fs.WriteFileOptions) {
408
- using _ = await this.initChange()
409
- await fp.writeFile(this.isAt, data, options)
410
- }
411
- appendSync(data: Buffer | string, options?: fs.WriteFileOptions) {
412
- using _ = this.initChangeSync()
413
- fs.appendFileSync(this.isAt, data, options)
414
- }
415
- async append(data: Buffer | string, options?: fs.WriteFileOptions) {
416
- using _ = await this.initChange()
417
- await fp.appendFile(this.isAt, data, options)
418
- }
419
-
420
- async sameAs(other: File) {
421
- if (this.isAt === other.isAt)
422
- return true
423
- else if ((await fp.lstat(this.isAt)).size !== (await fp.lstat(other.isAt)).size)
424
- return false
425
- const thisIter = this.itBuff()
426
- const otherIter = other.itBuff()
427
- while (true) {
428
- const [a, b] = await Promise.all([thisIter.next(), otherIter.next()])
429
- if (a.done && b.done) return true
430
- if (a.done !== b.done) return false
431
- if (!a.value!.equals(b.value!)) return false
432
- }
343
+ writeSync(data_: Buffer | string, options_?: fs.WriteFileOptions) {
344
+ using _ = this.lockSync()
345
+ fs.writeFileSync(this.isAt, data_, options_)
433
346
  }
434
- sameAsSync(other: File) {
435
- if (this.isAt === other.isAt)
436
- return true
437
- else if (fs.statSync(this.isAt).size !== fs.statSync(other.isAt).size)
438
- return false
439
- const thisIter = this.itBuffSync()
440
- const otherIter = other.itBuffSync()
441
- while (true) {
442
- const a = thisIter.next()
443
- const b = otherIter.next()
444
- if (a.done && b.done) return true
445
- if (a.done !== b.done) return false
446
- if (!a.value!.equals(b.value!)) return false
447
- }
347
+ async append(data_: Buffer | string, options_?: fs.WriteFileOptions) {
348
+ using _ = await this.lock()
349
+ await fp.appendFile(this.isAt, data_, options_)
448
350
  }
449
-
450
- deleteSync() {
451
- using _ = this.initChangeSync()
452
- fs.rmSync(this.isAt, { force: true })
351
+ appendSync(data_: Buffer | string, options_?: fs.WriteFileOptions) {
352
+ using _ = this.lockSync()
353
+ fs.appendFileSync(this.isAt, data_, options_)
453
354
  }
355
+
454
356
  async delete() {
455
- using _ = await this.initChange()
357
+ using _ = await this.lock()
456
358
  await fp.rm(this.isAt, { force: true })
457
359
  }
458
- moveSync(into: Folder) {
459
- using _ = this.initChangeSync()
460
- const newPath = into.join(this.name)
461
- fs.renameSync(this.isAt, newPath)
462
- this.pointsTo = newPath
360
+ deleteSync() {
361
+ using _ = this.lockSync()
362
+ fs.rmSync(this.isAt, { force: true })
463
363
  }
464
- async move(into: Folder) {
465
- using _ = await this.initChange()
466
- const newPath = into.join(this.name)
364
+ async move(into_: Folder) {
365
+ using _ = await this.lock()
366
+ const newPath = into_.join(this.name)
467
367
  await fp.rename(this.isAt, newPath)
468
368
  this.pointsTo = newPath
469
369
  }
470
- copySync(into: Folder): this {
471
- const newPath = into.join(this.name)
472
- fs.copyFileSync(this.isAt, newPath)
473
- return new File(newPath, false) as this
370
+ moveSync(into_: Folder) {
371
+ using _ = this.lockSync()
372
+ const newPath = into_.join(this.name)
373
+ fs.renameSync(this.isAt, newPath)
374
+ this.pointsTo = newPath
474
375
  }
475
- async copy(into: Folder): Promise<this> {
476
- const newPath = into.join(this.name)
376
+ async copy(into_: Folder): Promise<this> {
377
+ const newPath = into_.join(this.name)
477
378
  await fp.copyFile(this.isAt, newPath)
478
379
  return new File(newPath, false) as this
479
380
  }
480
- renameSync(to: string) {
481
- using _ = this.initChangeSync()
482
- const newPath = this.parent().join(to)
483
- fs.renameSync(this.isAt, newPath)
484
- this.pointsTo = newPath
381
+ copySync(into_: Folder): this {
382
+ const newPath = into_.join(this.name)
383
+ fs.copyFileSync(this.isAt, newPath)
384
+ return new File(newPath, false) as this
485
385
  }
486
- async rename(to: string) {
487
- using _ = await this.initChange()
488
- const newPath = this.parent().join(to)
386
+ async rename(to_: string) {
387
+ using _ = await this.lock()
388
+ const newPath = this.parent().join(to_)
489
389
  await fp.rename(this.isAt, newPath)
490
390
  this.pointsTo = newPath
491
391
  }
492
- resurrectSync() {
493
- using _ = this.initChangeSync()
494
- fs.writeFileSync(this.isAt, "")
392
+ renameSync(to_: string) {
393
+ using _ = this.lockSync()
394
+ const newPath = this.parent().join(to_)
395
+ fs.renameSync(this.isAt, newPath)
396
+ this.pointsTo = newPath
495
397
  }
496
- async resurrect() {
497
- using _ = await this.initChange()
498
- await fp.writeFile(this.isAt, "")
398
+
399
+ async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isFile() }
400
+ checkSync(): boolean { return fs.lstatSync(this.isAt).isFile() }
401
+
402
+ override isFile(): this is File { return true as const }
403
+ }
404
+
405
+
406
+ // Bizarre functions for file I/O
407
+ export async function computeHash(f_: File, algorithm_?: string, options_?: cr.HashOptions): Promise<Buffer>
408
+ export async function computeHash(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<string>
409
+ export async function computeHash(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<Buffer | string> {
410
+ const hash = cr.createHash(algorithm_, options_)
411
+ for await (const chunk of f_.itBuff())
412
+ hash.update(chunk)
413
+ return encoding_ ? hash.digest(encoding_) : hash.digest()
414
+ }
415
+ export function computeHashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions): Buffer
416
+ export function computeHashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): string
417
+ export function computeHashSync(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Buffer | string {
418
+ const hash = cr.createHash(algorithm_, options_)
419
+ for (const chunk of f_.itBuffSync())
420
+ hash.update(chunk)
421
+ return encoding_ ? hash.digest(encoding_) : hash.digest()
422
+ }
423
+
424
+ export async function streamHash(f_: File, algorithm_?: string, options_?: cr.HashOptions): Promise<Buffer>
425
+ export async function streamHash(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<string>
426
+ export async function streamHash(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<Buffer | string> {
427
+ const hash = cr.createHash(algorithm_, options_)
428
+ for await (const chunk of f_.itBuff())
429
+ hash.update(chunk)
430
+ return encoding_ ? hash.digest(encoding_) : hash.digest()
431
+ }
432
+ export function streamHashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions): Buffer
433
+ export function streamHashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): string
434
+ export function streamHashSync(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Buffer | string {
435
+ const hash = cr.createHash(algorithm_, options_)
436
+ for (const chunk of f_.itBuffSync())
437
+ hash.update(chunk)
438
+ return encoding_ ? hash.digest(encoding_) : hash.digest()
439
+ }
440
+
441
+ export async function* itLines(f_: File, options_: fs.ReadStreamOptions = { encoding: 'utf-8' }) {
442
+ const readStream = fs.createReadStream(f_.isAt, options_)
443
+ const rlInterface = rl.createInterface({ input: readStream, crlfDelay: Infinity })
444
+ try {
445
+ for await (const line of rlInterface)
446
+ yield line
447
+ } finally {
448
+ rlInterface.close()
449
+ readStream.destroy()
499
450
  }
500
451
  }
501
452
 
502
- export function entry() { return new File(process.argv[1]!, false) }
453
+ export async function fileSameAs(f1_: File, f2_: File): Promise<boolean> {
454
+ if (f1_.isAt === f2_.isAt)
455
+ return true
456
+ else if ((await fp.lstat(f1_.isAt)).size !== (await fp.lstat(f2_.isAt)).size)
457
+ return false
458
+ const iter1 = f1_.itBuff()
459
+ const iter2 = f2_.itBuff()
460
+ while (true) {
461
+ const [a, b] = await Promise.all([iter1.next(), iter2.next()])
462
+ if (a.done && b.done) return true
463
+ if (a.done !== b.done) return false
464
+ if (!a.value!.equals(b.value!)) return false
465
+ }
466
+ }
467
+ export function fileSameAsSync(f1_: File, f2_: File): boolean {
468
+ if (f1_.isAt === f2_.isAt)
469
+ return true
470
+ else if (fs.statSync(f1_.isAt).size !== fs.statSync(f2_.isAt).size)
471
+ return false
472
+ const iter1 = f1_.itBuffSync()
473
+ const iter2 = f2_.itBuffSync()
474
+ while (true) {
475
+ const a = iter1.next()
476
+ const b = iter2.next()
477
+ if (a.done && b.done) return true
478
+ if (a.done !== b.done) return false
479
+ if (!a.value!.equals(b.value!)) return false
480
+ }
481
+ }
503
482
 
504
483
 
505
484
 
506
485
  export class Folder extends Road {
507
- static async create(at: string): Promise<Folder> {
508
- try {
509
- await fp.access(at, fs.constants.F_OK)
510
- } catch {
511
- await fp.mkdir(at, { recursive: true })
512
- }
513
- return new Folder(at, false)
486
+ static async create(at_: string) {
487
+ try { await fp.access(at_, fsc.W_OK) }
488
+ catch { await fp.mkdir(at_, { recursive: true }) }
489
+ return new Folder(at_, false)
514
490
  }
515
- static createSync(at: string): Folder {
516
- try {
517
- fs.accessSync(at, fs.constants.F_OK)
518
- } catch {
519
- fs.mkdirSync(at, { recursive: true })
520
- }
521
- return new Folder(at, false)
491
+ static createSync(at_: string) {
492
+ try { fs.accessSync(at_, fsc.W_OK) }
493
+ catch { fs.mkdirSync(at_, { recursive: true }) }
494
+ return new Folder(at_, false)
522
495
  }
523
496
 
524
- join(...paths: string[]) {
525
- return ph.join(this.isAt, ...paths)
497
+ join(...paths_: string[]) {
498
+ return ph.join(this.isAt, ...paths_)
526
499
  }
527
500
 
528
- itSync(): Iterable<Road>
529
- itSync<T extends Road>(expectedType: new () => T): Iterable<T>
530
- *itSync<T extends Road>(expectedType?: new () => T): Iterable<Road> | Iterable<T> {
531
- for (const entry of fs.readdirSync(this.isAt)) {
532
- const road = factorySync(this.join(entry))
533
- if (!expectedType || road instanceof expectedType)
501
+ it(): AsyncIterable<Road>
502
+ it<T extends Road>(expectedType_: new (..._: any[]) => T): AsyncIterable<T>
503
+ async *it<T extends Road>(expectedType_?: new (..._: any[]) => T): AsyncIterable<Road> | AsyncIterable<T> {
504
+ for (const entryName of await fp.readdir(this.isAt)) {
505
+ const road = await factory(this.join(entryName))
506
+ if (!expectedType_ || road instanceof expectedType_)
534
507
  yield road
535
508
  }
536
509
  }
537
- it(): AsyncIterable<Road>
538
- it<T extends Road>(expectedType: new () => T): AsyncIterable<T>
539
- async *it<T extends Road>(expectedType?: new () => T): AsyncIterable<Road> | AsyncIterable<T> {
540
- for (const entry of await fp.readdir(this.isAt)) {
541
- const road = await factory(this.join(entry))
542
- if (!expectedType || road instanceof expectedType)
510
+ itSync(): Iterable<Road>
511
+ itSync<T extends Road>(expectedType_: new (..._: any[]) => T): Iterable<T>
512
+ *itSync<T extends Road>(expectedType_?: new (..._: any[]) => T): Iterable<Road> | Iterable<T> {
513
+ for (const entry of fs.readdirSync(this.isAt)) {
514
+ const road = factorySync(this.join(entry))
515
+ if (!expectedType_ || road instanceof expectedType_)
543
516
  yield road
544
517
  }
545
518
  }
546
- listSync(): Road[]
547
- listSync<T extends Road>(expectedType: new () => T): T[]
548
- listSync<T extends Road>(expectedType?: new () => T): Road[] | T[] {
549
- const entries = fs.readdirSync(this.isAt).map(entry => factorySync(this.join(entry)))
550
- if (!expectedType)
551
- return entries
552
- return entries.filter(entry => entry instanceof expectedType) as unknown as T[]
553
- }
554
519
  async list(): Promise<Road[]>
555
- async list<T extends Road>(_expectedType: new () => T): Promise<T[]>
556
- async list<T extends Road>(_expectedType?: new () => T): Promise<Road[] | T[]> {
520
+ async list<T extends Road>(expectedType_: new (..._: any[]) => T): Promise<T[]>
521
+ async list<T extends Road>(expectedType_?: new (..._: any[]) => T): Promise<Road[] | T[]> {
557
522
  const entries = (await fp.readdir(this.isAt)).map(async entry => factory(this.join(entry)))
558
523
  const resolvedEntries = await Promise.all(entries)
559
- if (!_expectedType)
524
+ if (!expectedType_)
560
525
  return resolvedEntries
561
- return resolvedEntries.filter(entry => entry instanceof _expectedType) as unknown as T[]
526
+ return resolvedEntries.filter(entry => entry instanceof expectedType_) as unknown as T[]
527
+ }
528
+ listSync(): Road[]
529
+ listSync<T extends Road>(expectedType_: new (..._: any[]) => T): T[]
530
+ listSync<T extends Road>(expectedType_?: new (..._: any[]) => T): Road[] | T[] {
531
+ const entries = fs.readdirSync(this.isAt).map(entry => factorySync(this.join(entry)))
532
+ if (!expectedType_)
533
+ return entries
534
+ return entries.filter(entry => entry instanceof expectedType_) as unknown as T[]
562
535
  }
563
536
 
564
- findSync(name: string): Road | null
565
- findSync<T extends Road>(name: string, _expectedType: new () => T): T | null
566
- findSync<T extends Road>(name: string, _expectedType?: new () => T): Road | T | null {
537
+ async find(name_: string): Promise<Road | null>
538
+ async find<T extends Road>(name_: string, expectedType_: new (..._: any[]) => T): Promise<T | null>
539
+ async find<T extends Road>(name_: string, expectedType_?: new (..._: any[]) => T): Promise<Road | T | null> {
567
540
  try {
568
- const found = factorySync(this.join(name))
569
- if (!_expectedType)
541
+ await fp.access(this.join(name_), fs.constants.F_OK)
542
+ const found = await factory(this.join(name_))
543
+ if (!expectedType_)
570
544
  return found
571
- if (found instanceof _expectedType)
545
+ if (found instanceof expectedType_)
572
546
  return found as T
573
547
  return null
574
548
  } catch {
575
549
  return null
576
550
  }
577
551
  }
578
-
579
- async find(name: string): Promise<Road | null>
580
- async find<T extends Road>(name: string, _expectedType: new () => T): Promise<T | null>
581
- async find<T extends Road>(name: string, _expectedType?: new () => T): Promise<Road | T | null> {
552
+ findSync(name_: string): Road | null
553
+ findSync<T extends Road>(name_: string, expectedType_: new (..._: any[]) => T): T | null
554
+ findSync<T extends Road>(name_: string, expectedType_?: new (..._: any[]) => T): Road | T | null {
582
555
  try {
583
- await fp.access(this.join(name), fs.constants.F_OK)
584
- const found = await factory(this.join(name))
585
- if (!_expectedType)
556
+ const found = factorySync(this.join(name_))
557
+ if (!expectedType_)
586
558
  return found
587
- if (found instanceof _expectedType)
559
+ if (found instanceof expectedType_)
588
560
  return found as T
589
561
  return null
590
562
  } catch {
@@ -592,69 +564,71 @@ export class Folder extends Road {
592
564
  }
593
565
  }
594
566
 
595
- addSync<T extends Road>(name: string, createable: { createSync: (at: string) => T }): T {
596
- const newPath = this.join(name)
597
- createable.createSync(newPath)
598
- return factorySync(newPath) as unknown as T
567
+ async add<T extends Road>(name_: string, createable_: { create: (at: string) => Promise<T> }): Promise<T> {
568
+ const newPath = this.join(name_)
569
+ await createable_.create(newPath)
570
+ return (await factory(newPath)) as unknown as T
599
571
  }
600
- async add<T extends Road>(name: string, createable: { create: (at: string) => Promise<T> }): Promise<T> {
601
- const newPath = this.join(name)
602
- await createable.create(newPath)
603
- return factory(newPath) as unknown as Promise<T>
572
+ addSync<T extends Road>(name_: string, createable_: { createSync: (at: string) => T }): T {
573
+ const newPath = this.join(name_)
574
+ createable_.createSync(newPath)
575
+ return factorySync(newPath) as unknown as T
604
576
  }
605
577
 
606
- deleteSync(options: fs.RmOptions = { recursive: true }) {
607
- using _ = this.initChangeSync()
608
- fs.rmSync(this.isAt, options)
609
- }
610
- async delete(options: fs.RmOptions = { recursive: true }) {
611
- using _ = await this.initChange()
612
- await fp.rm(this.isAt, options)
578
+ async delete(options_: fs.RmOptions = { recursive: true }) {
579
+ using _ = await this.lock()
580
+ await fp.rm(this.isAt, options_)
613
581
  }
614
- moveSync(into: Folder) {
615
- using _ = this.initChangeSync()
616
- const newPath = into.join(this.name)
617
- fs.renameSync(this.isAt, newPath)
618
- this.pointsTo = newPath
582
+ deleteSync(options_: fs.RmOptions = { recursive: true }) {
583
+ using _ = this.lockSync()
584
+ fs.rmSync(this.isAt, options_)
619
585
  }
620
- async move(into: Folder) {
621
- using _ = await this.initChange()
622
- const newPath = into.join(this.name)
586
+ async move(into_: Folder) {
587
+ using _ = await this.lock()
588
+ const newPath = into_.join(this.name)
623
589
  await fp.rename(this.isAt, newPath)
624
590
  this.pointsTo = newPath
625
591
  }
626
- copySync(into: Folder): this {
627
- const newPath = into.join(this.name)
628
- fs.cpSync(this.isAt, newPath, { recursive: true })
629
- return new Folder(newPath, false) as this
592
+ moveSync(into_: Folder) {
593
+ using _ = this.lockSync()
594
+ const newPath = into_.join(this.name)
595
+ fs.renameSync(this.isAt, newPath)
596
+ this.pointsTo = newPath
630
597
  }
631
- async copy(into: Folder): Promise<this> {
632
- const newPath = into.join(this.name)
598
+ async copy(into_: Folder): Promise<this> {
599
+ const newPath = into_.join(this.name)
633
600
  await fp.cp(this.isAt, newPath, { recursive: true })
634
601
  return new Folder(newPath, false) as this
635
602
  }
636
- renameSync(to: string) {
637
- using _ = this.initChangeSync()
638
- const newPath = this.parent().join(to)
639
- fs.renameSync(this.isAt, newPath)
640
- this.pointsTo = newPath
603
+ copySync(into_: Folder): this {
604
+ const newPath = into_.join(this.name)
605
+ fs.cpSync(this.isAt, newPath, { recursive: true })
606
+ return new Folder(newPath, false) as this
641
607
  }
642
- async rename(to: string) {
643
- using _ = await this.initChange()
644
- const newPath = this.parent().join(to)
608
+ async rename(to_: string) {
609
+ using _ = await this.lock()
610
+ const newPath = this.parent().join(to_)
645
611
  await fp.rename(this.isAt, newPath)
646
612
  this.pointsTo = newPath
647
613
  }
648
- resurrectSync() {
649
- using _ = this.initChangeSync()
650
- fs.mkdirSync(this.isAt, { recursive: true })
651
- }
652
- async resurrect() {
653
- using _ = await this.initChange()
654
- await fp.mkdir(this.isAt, { recursive: true })
614
+ renameSync(to_: string) {
615
+ using _ = this.lockSync()
616
+ const newPath = this.parent().join(to_)
617
+ fs.renameSync(this.isAt, newPath)
618
+ this.pointsTo = newPath
655
619
  }
620
+
621
+ async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isDirectory() }
622
+ checkSync(): boolean { return fs.lstatSync(this.isAt).isDirectory() }
623
+
624
+ override isFolder(): this is Folder { return true as const }
625
+ override isDir(): this is Folder { return true as const }
626
+ override isDirectory(): this is Folder { return true as const }
627
+ override isDict(): this is Folder { return true as const }
628
+ override isDictionary(): this is Folder { return true as const }
656
629
  }
657
630
 
631
+
658
632
  export function sysRoot() { return new Folder(ph.parse(process.cwd()).root, false) }
659
633
  export function home() { return new Folder(os.homedir(), false) }
660
634
  export function tmp() { return new Folder(os.tmpdir(), false) }
@@ -664,133 +638,137 @@ export { Folder as Dir, Folder as Directory, Folder as Dict, Folder as Dictionar
664
638
 
665
639
 
666
640
  export class SymbolicLink extends Road {
667
- static async create(at: string, target: Road) {
668
- try {
669
- await fp.access(at, fs.constants.F_OK)
670
- } catch {
671
- await fp.symlink(target.isAt, at)
672
- }
673
- return new SymbolicLink(at, false)
641
+ static async create(at_: string, target_: string | Road) {
642
+ try { await fp.access(at_, fs.constants.F_OK) }
643
+ catch { await fp.symlink(target_.toString(), at_) }
644
+ return new SymbolicLink(at_, false)
674
645
  }
675
- static createSync(_at: string, _target: Road) {
676
- try {
677
- fs.accessSync(_at, fs.constants.F_OK)
678
- } catch {
679
- fs.symlinkSync(_target.isAt, _at)
680
- }
681
- return new SymbolicLink(_at, false)
646
+ static createSync(at_: string, target_: string | Road) {
647
+ try { fs.accessSync(at_, fs.constants.F_OK) }
648
+ catch { fs.symlinkSync(target_.toString(), at_) }
649
+ return new SymbolicLink(at_, false)
682
650
  }
683
651
 
684
- targetSync() {
685
- return factorySync(ph.resolve(ph.dirname(this.isAt), fs.readlinkSync(this.isAt)))
686
- }
687
652
  async target() {
688
653
  return factory(ph.resolve(ph.dirname(this.isAt), await fp.readlink(this.isAt)))
689
654
  }
690
- retargetSync(_newTarget: Road) {
691
- this.deleteSync()
692
- fs.symlinkSync(_newTarget.isAt, this.isAt)
655
+ targetSync() {
656
+ return factorySync(ph.resolve(ph.dirname(this.isAt), fs.readlinkSync(this.isAt)))
693
657
  }
694
- async retarget(_newTarget: Road) {
658
+ async retarget(to_: Road) {
695
659
  await this.delete()
696
- return fp.symlink(_newTarget.isAt, this.isAt)
660
+ return fp.symlink(to_.isAt, this.isAt)
697
661
  }
698
-
699
- deleteSync() {
700
- using _ = this.initChangeSync()
701
- fs.unlinkSync(this.isAt)
662
+ retargetSync(to_: Road) {
663
+ this.deleteSync()
664
+ fs.symlinkSync(to_.isAt, this.isAt)
702
665
  }
666
+
703
667
  async delete() {
704
- using _ = await this.initChange()
668
+ using _ = await this.lock()
705
669
  await fp.unlink(this.isAt)
706
670
  }
707
- moveSync(_into: Folder) {
708
- using _ = this.initChangeSync()
709
- const newPath = _into.join(this.name)
710
- fs.renameSync(this.isAt, newPath)
711
- this.pointsTo = newPath
671
+ deleteSync() {
672
+ using _ = this.lockSync()
673
+ fs.unlinkSync(this.isAt)
712
674
  }
713
- async move(_into: Folder) {
714
- using _ = await this.initChange()
715
- const newPath = _into.join(this.name)
675
+ async move(into_: Folder) {
676
+ using _ = await this.lock()
677
+ const newPath = into_.join(this.name)
716
678
  await fp.rename(this.isAt, newPath)
717
679
  this.pointsTo = newPath
718
680
  }
719
- copySync(_into: Folder): this {
720
- const newPath = _into.join(this.name)
721
- const target = this.targetSync()
722
- fs.symlinkSync(target.isAt, newPath)
723
- return new SymbolicLink(newPath, false) as this
681
+ moveSync(into_: Folder) {
682
+ using _ = this.lockSync()
683
+ const newPath = into_.join(this.name)
684
+ fs.renameSync(this.isAt, newPath)
685
+ this.pointsTo = newPath
724
686
  }
725
- async copy(_into: Folder): Promise<this> {
726
- const newPath = _into.join(this.name)
687
+ async copy(into_: Folder): Promise<this> {
688
+ const newPath = into_.join(this.name)
727
689
  const target = await this.target()
728
690
  await fp.symlink(target.isAt, newPath)
729
691
  return new SymbolicLink(newPath, false) as this
730
692
  }
731
- renameSync(_to: string) {
732
- using _ = this.initChangeSync()
733
- const newPath = this.parent().join(_to)
734
- fs.renameSync(this.isAt, newPath)
735
- this.pointsTo = newPath
693
+ copySync(into_: Folder): this {
694
+ const newPath = into_.join(this.name)
695
+ const target = this.targetSync()
696
+ fs.symlinkSync(target.isAt, newPath)
697
+ return new SymbolicLink(newPath, false) as this
736
698
  }
737
- async rename(_to: string) {
738
- using _ = await this.initChange()
739
- const newPath = this.parent().join(_to)
699
+ async rename(to_: string) {
700
+ using _ = await this.lock()
701
+ const newPath = this.parent().join(to_)
740
702
  await fp.rename(this.isAt, newPath)
741
703
  this.pointsTo = newPath
742
704
  }
743
- resurrectSync() {
744
- using _ = this.initChangeSync()
745
- const target = this.targetSync()
746
- fs.symlinkSync(target.isAt, this.isAt)
747
- }
748
- async resurrect() {
749
- using _ = await this.initChange()
750
- const target = await this.target()
751
- await fp.symlink(target.isAt, this.isAt)
705
+ renameSync(to_: string) {
706
+ using _ = this.lockSync()
707
+ const newPath = this.parent().join(to_)
708
+ fs.renameSync(this.isAt, newPath)
709
+ this.pointsTo = newPath
752
710
  }
711
+
712
+ async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isSymbolicLink() }
713
+ checkSync(): boolean { return fs.lstatSync(this.isAt).isSymbolicLink() }
714
+
715
+ override isSymlink(): this is SymbolicLink { return true as const }
716
+ override isSymbolicLink(): this is SymbolicLink { return true as const }
753
717
  }
754
718
  export { SymbolicLink as Symlink }
755
719
 
756
720
 
757
721
 
758
722
  export abstract class UnusableRoad extends Road {
759
- override readonly mutable: boolean = false // Modification is most likely to cause system issues (e.g. deleting a device file)
760
- constructor(_at: string, typeCheck: boolean) {
761
- super(_at, typeCheck)
723
+ override readonly mutable: boolean = false // Modification will cause system issues (e.g. deleting a device file)
724
+ constructor(...args_: ConstructorParameters<typeof Road>) {
725
+ super(...args_)
762
726
  Object.freeze(this)
763
727
  }
764
- error(): never { throw new RdErr(`${this.constructor.name} at '${this.isAt}' is a system-level resource thus intentionally made immutable.`) }
765
- override initChangeSync(): never { return this.error() }
766
- override async initChange(): Promise<never> { return this.error() }
767
- override deleteSync(): never { return this.error() }
728
+ error(): never { throw new RdErr(`${this.constructor.name} at '${this.isAt}' is a system-level resource thus not subject to modification.`) }
729
+ override async lock(): Promise<never> { return this.error() }
730
+ override lockSync(): never { return this.error() }
768
731
  override async delete(): Promise<never> { return this.error() }
769
- override moveSync(): never { return this.error() }
732
+ override deleteSync(): never { return this.error() }
770
733
  override async move(): Promise<never> { return this.error() }
771
- override copySync(): never { return this.error() }
734
+ override moveSync(): never { return this.error() }
772
735
  override async copy(): Promise<never> { return this.error() }
773
- override renameSync(): never { return this.error() }
736
+ override copySync(): never { return this.error() }
774
737
  override async rename(): Promise<never> { return this.error() }
775
- override resurrectSync(): never { return this.error() }
776
- override async resurrect(): Promise<never> { return this.error() }
738
+ override renameSync(): never { return this.error() }
739
+
740
+ override isUnusable(): this is UnusableRoad { return true as const }
741
+ }
742
+ export class BlockDevice extends UnusableRoad {
743
+ async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isBlockDevice() }
744
+ checkSync(): boolean { return fs.lstatSync(this.isAt).isBlockDevice() }
745
+ override isBlockDevice(): this is BlockDevice { return true as const }
746
+ }
747
+ export class CharacterDevice extends UnusableRoad {
748
+ async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isCharacterDevice() }
749
+ checkSync(): boolean { return fs.lstatSync(this.isAt).isCharacterDevice() }
750
+ override isCharacterDevice(): this is CharacterDevice { return true as const }
751
+ }
752
+ export class Fifo extends UnusableRoad {
753
+ async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isFIFO() }
754
+ checkSync(): boolean { return fs.lstatSync(this.isAt).isFIFO() }
755
+ override isFifo(): this is Fifo { return true as const }
756
+ }
757
+ export class Socket extends UnusableRoad {
758
+ async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isSocket() }
759
+ checkSync(): boolean { return fs.lstatSync(this.isAt).isSocket() }
760
+ override isSocket(): this is Socket { return true as const }
777
761
  }
778
- export class BlockDevice extends UnusableRoad { }
779
- export class CharacterDevice extends UnusableRoad { }
780
- export class Fifo extends UnusableRoad { }
781
- export class Socket extends UnusableRoad { }
782
762
 
783
763
 
784
764
 
785
- export let finalizer: FinalizationRegistry<string> | null = null
786
- export let toDelete: Set<string> | null = null
787
- let exitHandlerRegistered = false
765
+ let finalizer: FinalizationRegistry<string> | null = null
766
+ let toDelete: Set<string> | null = null
767
+ let exitHandlerRegistered: boolean | null = null
788
768
  /**
789
769
  * Forcefully cleans up all files and folders registered for cleanup on exit.
790
- *
791
- * @remarks This function is not recommended to be called manually, as it will delete all files and folders registered for cleanup on exit, which may lead to data loss if called at the wrong time. This function is intended to be called automatically when the process exits.
792
770
  */
793
- export function forceCleanupToDelete() {
771
+ function forceCleanupToDelete() {
794
772
  for (const path of toDelete ?? [])
795
773
  try { fs.rmSync(path, { force: true, recursive: true }) } catch {}
796
774
  toDelete?.clear()
@@ -800,26 +778,24 @@ export function forceCleanupToDelete() {
800
778
  process.off('exit', forceCleanupToDelete)
801
779
  exitHandlerRegistered = false
802
780
  }
803
- export function registerToCleanup(self: Road) {
804
- if (!finalizer)
805
- finalizer = new FinalizationRegistry<string>(p => { try { fs.rmSync(p, { force: true, recursive: true }) } catch {}; toDelete?.delete(p) })
806
- if (!toDelete)
807
- toDelete = new Set()
781
+ export function registerToCleanup(self_: Road) {
782
+ finalizer ??= new FinalizationRegistry<string>(p => { try { fs.rmSync(p, { force: true, recursive: true }) } catch {}; toDelete?.delete(p) })
783
+ toDelete ??= new Set()
808
784
  if (!exitHandlerRegistered) {
809
785
  process.once('exit', forceCleanupToDelete)
810
786
  exitHandlerRegistered = true
811
787
  }
812
- toDelete.add(self.isAt)
813
- finalizer.register(self, self.isAt, self)
788
+ toDelete.add(self_.isAt)
789
+ finalizer.register(self_, self_.isAt, self_)
814
790
  }
815
791
 
816
792
 
817
- export function Temp<T extends Road>(createable: { createSync: (at: string) => T }, autoCleanup: boolean): T & Disposable & AsyncDisposable {
818
- const t = createable.createSync(tmp().join(`instrumentality@${cr.randomUUID()}`))
819
- if (autoCleanup)
793
+ export function Temp<T extends Road>(createable_: { createSync: (at: string) => T }, autoCleanup_: boolean): T & Disposable & AsyncDisposable {
794
+ let t = createable_.createSync(tmp().join(`instrumentality@${cr.randomUUID()}`))
795
+ if (autoCleanup_)
820
796
  registerToCleanup(t)
821
797
  return Object.freeze(Object.assign(t, {
822
798
  [Symbol.dispose]() { try { t.deleteSync() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) },
823
799
  async [Symbol.asyncDispose]() { try { await t.delete() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) }
824
800
  }))
825
- }
801
+ }11