instrumentality 0.0.7 → 0.0.9

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
@@ -1,28 +1,27 @@
1
- import * as rl from "node:readline"
1
+ import * as cr from "node:crypto"
2
2
  import * as fs from "node:fs"; import { constants as fsc } from "node:fs"
3
3
  import * as fp from "node:fs/promises"
4
4
  import * as ph from "node:path"
5
5
  import * as os from "node:os"
6
- import * as cr from "node:crypto"
7
6
  import { on } from "node:events"
8
- import * as bs from "./base.ts"
7
+ import { InsErr } from "./base.ts"
9
8
 
10
9
 
11
10
 
12
- /** Subclass of {@link bs.InsErr} that represents an error thrown from this specific module of the library */
13
- export class RdErr extends bs.InsErr { override name = "Instrumentality-Road-Error" }
14
- export { RdErr as RoadError }
11
+ /** Subclass of {@link InsErr} that represents an error thrown from this specific module of the library */
12
+ export class Err extends InsErr { override name = "Instrumentality-Road-Error" }
13
+ export { Err as RoadError, Err as RdErr }
15
14
 
16
15
 
17
16
 
18
17
  /**
19
18
  * Returns the constructor function corresponding to the file mode (statmode).
20
19
  *
21
- * @param statmode_ The file mode to check.
20
+ * @param statmode_ The file mode or {@link fs.Dirent} to check.
22
21
  * @returns The constructor function corresponding to the road type (e.g., {@link File}, {@link Folder}, etc.).
23
- * @throws If the file mode is unknown, throws a {@link RdErr}.
22
+ * @throws If the file mode is unknown, throws a {@link Err}.
24
23
  */
25
- export function resolveMode(statmode_: number): typeof File | typeof Folder | typeof BlockDevice | typeof CharacterDevice | typeof SymbolicLink | typeof Fifo | typeof Socket {
24
+ export function resolveStat(statmode_: number): typeof File | typeof Folder | typeof BlockDevice | typeof CharacterDevice | typeof SymbolicLink | typeof Fifo | typeof Socket {
26
25
  switch (statmode_ & fsc.S_IFMT) {
27
26
  case fsc.S_IFREG: return File
28
27
  case fsc.S_IFDIR: return Folder
@@ -31,9 +30,30 @@ export function resolveMode(statmode_: number): typeof File | typeof Folder | ty
31
30
  case fsc.S_IFLNK: return SymbolicLink
32
31
  case fsc.S_IFIFO: return Fifo
33
32
  case fsc.S_IFSOCK: return Socket
34
- default: throw new RdErr(`Unknown mode type ${statmode_} (statmode is most likely corrupted)`)
33
+ default: throw new Err(`Unknown mode type ${statmode_} (statmode is most likely corrupted)`)
35
34
  }
36
35
  }
36
+ export { resolveStat as resStat }
37
+
38
+ /**
39
+ * Returns the constructor function corresponding to the type of the given {@link fs.Dirent}.
40
+ *
41
+ * @param dirent The directory entry to check.
42
+ * @returns The constructor function corresponding to the road type (e.g., {@link File}, {@link Folder}, etc.).
43
+ * @throws If the directory entry type is unknown, throws a {@link Err}.
44
+ */
45
+ export function resolveDirent(dirent: fs.Dirent): typeof File | typeof Folder | typeof BlockDevice | typeof CharacterDevice | typeof SymbolicLink | typeof Fifo | typeof Socket {
46
+ // Order by likelihood: files/dicts are most common, followed by symbolic links
47
+ if (dirent.isFile()) return File
48
+ if (dirent.isDirectory()) return Folder
49
+ if (dirent.isSymbolicLink()) return SymbolicLink
50
+ if (dirent.isBlockDevice()) return BlockDevice
51
+ if (dirent.isCharacterDevice()) return CharacterDevice
52
+ if (dirent.isFIFO()) return Fifo
53
+ if (dirent.isSocket()) return Socket
54
+ throw new Err(`Unknown dirent type for ${dirent.name}`)
55
+ }
56
+ export { resolveDirent as resDirent }
37
57
 
38
58
 
39
59
 
@@ -45,19 +65,21 @@ export function resolveMode(statmode_: number): typeof File | typeof Folder | ty
45
65
  * @throws If {@link fp.lstat}/{@link fs.lstatSync} fails to retrieved the status of {@link path_}.
46
66
  */
47
67
  export async function factory(path_: string) {
48
- return new (resolveMode((await fp.lstat(path_)).mode))(path_, false)
68
+ return new (resolveStat((await fp.lstat(path_)).mode))(path_, false)
49
69
  }
70
+ export { factory as fac, factory as mk }
50
71
  /** Sync version of {@link factory}. */
51
72
  export function factorySync(path_: string) {
52
- return new (resolveMode(fs.lstatSync(path_).mode))(path_, false)
73
+ return new (resolveStat(fs.lstatSync(path_).mode))(path_, false)
53
74
  }
75
+ export { factorySync as facSync, factorySync as mkSync }
54
76
 
55
77
 
56
78
 
57
79
  /**
58
80
  * A map that keeps track of locked roads to prevent concurrent modifications.
59
81
  *
60
- * @key The absolute path of the road that is currently locked.
82
+ * @key The **absolute** and **normalized** (**resolved**) path of the road that is currently locked.
61
83
  * @value A promise that resolves when the lock on the road is released.
62
84
  *
63
85
  * @remarks The map is initialized lazily when the first lock is created to minimize import-time side effects.
@@ -90,7 +112,7 @@ export abstract class Road {
90
112
  /** 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
113
  get depth() { return this.isAt.split(ph.sep).length - 1 }
92
114
  /** Same as {@link isAt} but for compatibility with external APIs. */
93
- toString() { return this.isAt }
115
+ toString(): string { return this.isAt }
94
116
 
95
117
  /**
96
118
  * Creates a new instance of the Road class.
@@ -98,64 +120,12 @@ export abstract class Road {
98
120
  * @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
121
  * @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.
100
122
  *
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.
123
+ * @throws If {@link typeCheck_} is true and the path does not correspond to the expected type of road, a {@link Err} will be thrown.
102
124
  */
103
- constructor(path_: string, typeCheck_: boolean | 1 | 0) {
125
+ constructor(path_: string, typeCheck_: boolean) {
104
126
  this.pointsTo = ph.resolve(path_)
105
127
  if (typeCheck_ && !this.checkSync())
106
- throw new RdErr(`Type mismatch: '${this.isAt}'`)
107
- }
108
-
109
- /**
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.
115
- *
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).
118
- */
119
- protected async lock(cb_ = () => {}): Promise<AsyncDisposable & Disposable> {
120
- if (!this.mutable)
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))
126
- return {
127
- [Symbol.dispose]() {
128
- cb_()
129
- lockedRoads!.delete(isAt)
130
- },
131
- async [Symbol.asyncDispose]() {
132
- cb_()
133
- lockedRoads!.delete(isAt)
134
- }
135
- }
136
- }
137
- /**
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.
140
- */
141
- protected lockSync(cb_ = () => {}): Disposable & AsyncDisposable {
142
- if (!this.mutable)
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))
149
- return {
150
- [Symbol.dispose]() {
151
- lockedRoads!.delete(isAt)
152
- cb_()
153
- },
154
- async [Symbol.asyncDispose]() {
155
- lockedRoads!.delete(isAt)
156
- cb_()
157
- }
158
- }
128
+ throw new Err(`Type mismatch: '${this.isAt}'`)
159
129
  }
160
130
 
161
131
  /** @returns An instance of {@link Folder} representing the parent directory of the current road. */
@@ -177,6 +147,34 @@ export abstract class Road {
177
147
  /** @returns An array of {@link Folder} instances representing the ancestors of the current road. */
178
148
  ancestors(): Folder[] { return [...this.ancestorsIt()] }
179
149
 
150
+ protected reserveLock(allowConcurrent: boolean): Disposable & { previous: Promise<void> | undefined } {
151
+ if (!this.mutable)
152
+ throw new Err(`Road to '${this.isAt}' is immutable.`)
153
+ lockedRoads ??= new Map()
154
+ const { promise, resolve } = Promise.withResolvers<void>()
155
+ const isAt = this.isAt
156
+ const previous = lockedRoads.get(this.isAt)
157
+ if (!allowConcurrent && previous)
158
+ throw new Err(`Road to '${this.isAt}' is already locked.`)
159
+ lockedRoads.set(this.isAt, promise)
160
+ return {
161
+ [Symbol.dispose]: () => {
162
+ resolve()
163
+ if (lockedRoads!.get(isAt) === promise)
164
+ lockedRoads!.delete(isAt)
165
+ },
166
+ previous,
167
+ }
168
+ }
169
+ async lock(): Promise<Disposable> {
170
+ const l = this.reserveLock(true)
171
+ try { await l.previous } catch {}
172
+ return l
173
+ }
174
+ lockSync(): Disposable {
175
+ return this.reserveLock(false)
176
+ }
177
+
180
178
  /**
181
179
  * Watches the current entry for changes and resolves when the entry becomes accessible (i.e., exists and can be accessed).
182
180
  *
@@ -223,20 +221,55 @@ export abstract class Road {
223
221
  lstat() { return fp.lstat(this.isAt) }
224
222
  /** @returns The result of {@link fs.lstatSync} for the current entry. */
225
223
  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) }
230
224
 
231
- // jsdocs for the abstract methods are in the subclasses
232
- abstract delete(): 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>
225
+ /** Get the size of the this entry in bytes. */
226
+ abstract size(): Promise<number>
227
+ abstract sizeSync(): number
228
+
229
+ /** Wrapper around {@link fp.rm} with locking. */
230
+ async delete(): Promise<void> {
231
+ using _ = await this.lock()
232
+ await fp.rm(this.isAt, { recursive: true, force: true })
233
+ }
234
+ /** Wrapper around {@link fs.rmSync} with locking. */
235
+ deleteSync(): void {
236
+ using _ = this.lockSync()
237
+ fs.rmSync(this.isAt, { recursive: true, force: true })
238
+ }
239
+ async copy(into_: Folder): Promise<this> {
240
+ const newPath = into_.join(this.name)
241
+ await fp.cp(this.isAt, newPath, { recursive: true, force: true })
242
+ return new (this.constructor as new (path: string, typeCheck: boolean) => this)(newPath, false)
243
+ }
244
+ copySync(into_: Folder): this {
245
+ const newPath = into_.join(this.name)
246
+ fs.cpSync(this.isAt, newPath, { recursive: true, force: true })
247
+ return new (this.constructor as new (path: string, typeCheck: boolean) => this)(newPath, false)
248
+ }
249
+ async move(into_: Folder): Promise<void> {
250
+ using _ = await this.lock()
251
+ const newPath = into_.join(this.name)
252
+ await fp.rename(this.isAt, newPath)
253
+ this.pointsTo = newPath
254
+ }
255
+ moveSync(into_: Folder): void {
256
+ using _ = this.lockSync()
257
+ const newPath = into_.join(this.name)
258
+ fs.renameSync(this.isAt, newPath)
259
+ this.pointsTo = newPath
260
+ }
261
+ async rename(newName_: string): Promise<void> {
262
+ using _ = await this.lock()
263
+ const newPath = this.parent().join(newName_)
264
+ await fp.rename(this.isAt, newPath)
265
+ this.pointsTo = newPath
266
+ }
267
+ renameSync(newName_: string): void {
268
+ using _ = this.lockSync()
269
+ const newPath = this.parent().join(newName_)
270
+ fs.renameSync(this.isAt, newPath)
271
+ this.pointsTo = newPath
272
+ }
240
273
 
241
274
  // jsdocs for the abstract methods are in the subclasses
242
275
  abstract check(): Promise<boolean>
@@ -250,10 +283,6 @@ export abstract class Road {
250
283
  isFolder(): this is Folder { return false as const }
251
284
  /** Type narrowing for {@link Folder} (similar to `instanceof` without unnecessary runtime checks). */
252
285
  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
286
  /** Type narrowing for {@link SymbolicLink} (similar to `instanceof` without unnecessary runtime checks). */
258
287
  isSymlink(): this is SymbolicLink { return false as const }
259
288
  /** Type narrowing for {@link SymbolicLink} (similar to `instanceof` without unnecessary runtime checks). */
@@ -269,61 +298,67 @@ export abstract class Road {
269
298
  /** Type narrowing for {@link Socket} (similar to `instanceof` without unnecessary runtime checks). */
270
299
  isSocket(): this is Socket { return false as const }
271
300
  }
272
- export type road_t = ConstructorParameters<typeof Road>
301
+ /** Constructor type for a subclass of {@link Road}. */
302
+ export type road_t<T extends Road> = new (...args_: ConstructorParameters<typeof Road>) => T
273
303
 
274
304
 
275
305
 
276
306
  /** Subclass of {@link Road} that represents a file. */
277
307
  export class File extends Road {
278
- static async create(at_: string) {
308
+ /**
309
+ * Creates a new file at the specified path if it does not already exist.
310
+ *
311
+ * @param at_ The path at which to create the file.
312
+ * @returns A promise that resolves to the newly created `File` instance.
313
+ * @throws if {@link fp.writeFile} throws.
314
+ */
315
+ static async create(at_: string, data: string | Buffer = "") {
279
316
  try { await fp.access(at_, fsc.W_OK) }
280
- catch { await fp.writeFile(at_, "") }
317
+ catch { await fp.writeFile(at_, data) }
281
318
  return new File(at_, true)
282
319
  }
320
+ /** Alias for {@link File.create}. */
283
321
  static readonly mk: typeof File.create = File.create
284
- static createSync(at_: string) {
322
+ /** Synchronous version of {@link File.create}. */
323
+ static createSync(at_: string, data: string | Buffer = "") {
285
324
  try { fs.accessSync(at_, fs.constants.W_OK) }
286
- catch { fs.writeFileSync(at_, "") }
325
+ catch { fs.writeFileSync(at_, data) }
287
326
  return new File(at_, true)
288
327
  }
328
+ /** Alias for {@link File.createSync}. */
289
329
  static readonly mkSync: typeof File.createSync = File.createSync
290
330
 
331
+ /** The file extension of this file, including the leading dot. */
291
332
  get ext() { return ph.extname(this.isAt) }
333
+ /** The file name without its extension. */
292
334
  get noExt() { return ph.basename(this.isAt, this.ext) }
293
335
 
336
+ /**
337
+ * Reads the contents of the file.
338
+ *
339
+ * @returns A promise that resolves to the contents of the file as a `Buffer` or `string`, depending on the specified encoding.
340
+ * @throws If the file can't be read due to permission issues or other filesystem errors.
341
+ */
294
342
  async read(): Promise<Buffer>
295
- async read(encoding_: BufferEncoding, flag_?: string): Promise<string>
296
- async read(encoding_?: BufferEncoding, flag_?: string): Promise<Buffer | string> {
297
- if (encoding_)
298
- return fp.readFile(this.isAt, { encoding: encoding_, flag: flag_ })
299
- else
300
- return fp.readFile(this.isAt)
343
+ async read(options_: Parameters<typeof fp.readFile>[1]): Promise<string>
344
+ async read(options_?: Parameters<typeof fp.readFile>[1]): Promise<Buffer | string> {
345
+ return fp.readFile(this.isAt, options_!)
301
346
  }
302
347
  readSync(): Buffer
303
- readSync(encoding_: BufferEncoding, flag_?: string): string
304
- readSync(encoding_?: BufferEncoding, flag_?: string): Buffer | string {
305
- if (encoding_)
306
- return fs.readFileSync(this.isAt, { encoding: encoding_, flag: flag_ })
307
- else
308
- return fs.readFileSync(this.isAt)
348
+ readSync(options_: Parameters<typeof fs.readFileSync>[1]): string
349
+ readSync(options_?: Parameters<typeof fs.readFileSync>[1]): Buffer | string {
350
+ return fs.readFileSync(this.isAt, options_!)
309
351
  }
310
352
 
311
- async *itBuff(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode) {
312
- const fd = await fp.open(this.isAt, flags_, mode_)
353
+ async *itBuff(options_?: fs.ReadStreamOptions): AsyncGenerator<Buffer> {
354
+ const stream = fs.createReadStream(this.isAt, { ...options_, encoding: undefined })
313
355
  try {
314
- const buffer = Buffer.alloc(chunkSize_)
315
- let bytesRead: number
316
- do {
317
- const readResult = await fd.read(buffer, 0, chunkSize_, null)
318
- bytesRead = readResult.bytesRead
319
- if (bytesRead > 0)
320
- yield buffer.subarray(0, bytesRead)
321
- } while (bytesRead === chunkSize_)
322
- } finally {
323
- await fd.close()
356
+ for await (const chunk of stream)
357
+ yield chunk
324
358
  }
359
+ finally { if (!stream.destroyed) stream.destroy() }
325
360
  }
326
- *itBuffSync(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode) {
361
+ *itBuffSync(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode): Generator<Buffer> {
327
362
  const fd = fs.openSync(this.isAt, flags_, mode_)
328
363
  try {
329
364
  const buffer = Buffer.alloc(chunkSize_)
@@ -331,158 +366,115 @@ export class File extends Road {
331
366
  do {
332
367
  bytesRead = fs.readSync(fd, buffer, 0, chunkSize_, null)
333
368
  if (bytesRead > 0)
334
- yield buffer.subarray(0, bytesRead)
369
+ yield Buffer.from(buffer.subarray(0, bytesRead))
335
370
  } while (bytesRead === chunkSize_)
336
- } finally {
337
- fs.closeSync(fd)
338
371
  }
372
+ finally { fs.closeSync(fd) }
339
373
  }
340
374
 
341
- async write(data_: Buffer | string, options_?: fs.WriteFileOptions) {
342
- using _ = await this.lock()
343
- await fp.writeFile(this.isAt, data_, options_)
344
- }
345
- writeSync(data_: Buffer | string, options_?: fs.WriteFileOptions) {
346
- using _ = this.lockSync()
347
- fs.writeFileSync(this.isAt, data_, options_)
375
+ async sameAs(other_: File): Promise<boolean> {
376
+ if (this.isAt === other_.isAt) return true
377
+ const [s1, s2] = await Promise.all([this.size(), other_.size()])
378
+ if (s1 !== s2) return false
379
+ if (s1 === 0) return true
380
+ const iter1 = this.itBuff()
381
+ const iter2 = other_.itBuff()
382
+ try {
383
+ while (true) {
384
+ const [a, b] = await Promise.all([iter1.next(), iter2.next()])
385
+ if (a.done && b.done) return true
386
+ if (a.done !== b.done) return false
387
+ if (!a.value.equals(b.value)) return false
388
+ }
389
+ }
390
+ finally { await Promise.all([iter1.return?.(undefined), iter2.return?.(undefined)]) }
391
+ }
392
+ sameAsSync(other_: File): boolean {
393
+ if (this.isAt === other_.isAt) return true
394
+ const [s1, s2] = [this.sizeSync(), other_.sizeSync()]
395
+ if (s1 !== s2) return false
396
+ if (s1 === 0) return true
397
+ const iter1 = this.itBuffSync()
398
+ const iter2 = other_.itBuffSync()
399
+ try {
400
+ while (true) {
401
+ const a = iter1.next()
402
+ const b = iter2.next()
403
+ if (a.done && b.done) return true
404
+ if (a.done !== b.done) return false
405
+ if (!a.value!.equals(b.value!)) return false
406
+ }
407
+ }
408
+ finally {
409
+ try { iter1.return?.(undefined) } catch {}
410
+ try { iter2.return?.(undefined) } catch {}
411
+ }
348
412
  }
349
- async append(data_: Buffer | string, options_?: fs.WriteFileOptions) {
350
- using _ = await this.lock()
351
- await fp.appendFile(this.isAt, data_, options_)
413
+
414
+ async hash(algorithm_?: string, options_?: cr.HashOptions): Promise<Buffer>
415
+ async hash(algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<string>
416
+ async hash(algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<Buffer | string> {
417
+ const hash = cr.createHash(algorithm_, options_)
418
+ for await (const chunk of this.itBuff())
419
+ hash.update(chunk)
420
+ return encoding_ ? hash.digest(encoding_) : hash.digest()
352
421
  }
353
- appendSync(data_: Buffer | string, options_?: fs.WriteFileOptions) {
354
- using _ = this.lockSync()
355
- fs.appendFileSync(this.isAt, data_, options_)
422
+ hashSync(algorithm_?: string, options_?: cr.HashOptions): Buffer
423
+ hashSync(algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): string
424
+ hashSync(algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Buffer | string {
425
+ const hash = cr.createHash(algorithm_, options_)
426
+ for (const chunk of this.itBuffSync())
427
+ hash.update(chunk)
428
+ return encoding_ ? hash.digest(encoding_) : hash.digest()
356
429
  }
357
430
 
358
- async delete() {
431
+ async size(): Promise<number> { return (await this.lstat()).size }
432
+ sizeSync(): number { return this.lstatSync().size }
433
+
434
+ async writeAtomic(data_: Buffer | string, options_?: fs.WriteFileOptions) {
359
435
  using _ = await this.lock()
360
- await fp.rm(this.isAt, { force: true })
436
+ await this.parent().borrow(File, async tmp => {
437
+ using _ = await tmp.lock()
438
+ await fp.writeFile(tmp.isAt, data_, options_)
439
+ await fp.rename(tmp.isAt, this.isAt)
440
+ })
361
441
  }
362
- deleteSync() {
442
+ writeAtomicSync(data_: Buffer | string, options_?: fs.WriteFileOptions) {
363
443
  using _ = this.lockSync()
364
- fs.rmSync(this.isAt, { force: true })
444
+ this.parent().borrowSync(File, tmp => {
445
+ using _ = tmp.lockSync()
446
+ fs.writeFileSync(tmp.isAt, data_, options_)
447
+ fs.renameSync(tmp.isAt, this.isAt)
448
+ })
365
449
  }
366
- async move(into_: Folder) {
450
+
451
+ async write(data_: Buffer | string, options_?: fs.WriteFileOptions) {
367
452
  using _ = await this.lock()
368
- const newPath = into_.join(this.name)
369
- await fp.rename(this.isAt, newPath)
370
- this.pointsTo = newPath
453
+ await fp.writeFile(this.isAt, data_, options_)
371
454
  }
372
- moveSync(into_: Folder) {
455
+ writeSync(data_: Buffer | string, options_?: fs.WriteFileOptions) {
373
456
  using _ = this.lockSync()
374
- const newPath = into_.join(this.name)
375
- fs.renameSync(this.isAt, newPath)
376
- this.pointsTo = newPath
377
- }
378
- async copy(into_: Folder): Promise<this> {
379
- const newPath = into_.join(this.name)
380
- await fp.copyFile(this.isAt, newPath)
381
- return new File(newPath, false) as this
382
- }
383
- copySync(into_: Folder): this {
384
- const newPath = into_.join(this.name)
385
- fs.copyFileSync(this.isAt, newPath)
386
- return new File(newPath, false) as this
457
+ fs.writeFileSync(this.isAt, data_, options_)
387
458
  }
388
- async rename(to_: string) {
459
+ async append(data_: Buffer | string, options_?: fs.WriteFileOptions) {
389
460
  using _ = await this.lock()
390
- const newPath = this.parent().join(to_)
391
- await fp.rename(this.isAt, newPath)
392
- this.pointsTo = newPath
461
+ await fp.appendFile(this.isAt, data_, options_)
393
462
  }
394
- renameSync(to_: string) {
463
+ appendSync(data_: Buffer | string, options_?: fs.WriteFileOptions) {
395
464
  using _ = this.lockSync()
396
- const newPath = this.parent().join(to_)
397
- fs.renameSync(this.isAt, newPath)
398
- this.pointsTo = newPath
465
+ fs.appendFileSync(this.isAt, data_, options_)
399
466
  }
400
467
 
401
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isFile() }
402
- checkSync(): boolean { return fs.lstatSync(this.isAt).isFile() }
468
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isFile() } catch { return false } }
469
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isFile() } catch { return false } }
403
470
 
404
471
  override isFile(): this is File { return true as const }
405
472
  }
406
473
 
407
474
 
408
- // Bizarre functions for file I/O
409
- export async function hash(f_: File, algorithm_?: string, options_?: cr.HashOptions): Promise<Buffer>
410
- export async function hash(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<string>
411
- export async function hash(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<Buffer | string> {
412
- const hash = cr.createHash(algorithm_, options_)
413
- for await (const chunk of f_.itBuff())
414
- hash.update(chunk)
415
- return encoding_ ? hash.digest(encoding_) : hash.digest()
416
- }
417
- export function hashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions): Buffer
418
- export function hashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): string
419
- export function hashSync(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Buffer | string {
420
- const hash = cr.createHash(algorithm_, options_)
421
- for (const chunk of f_.itBuffSync())
422
- hash.update(chunk)
423
- return encoding_ ? hash.digest(encoding_) : hash.digest()
424
- }
425
-
426
- export async function streamHash(f_: File, algorithm_?: string, options_?: cr.HashOptions): Promise<Buffer>
427
- export async function streamHash(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<string>
428
- export async function streamHash(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<Buffer | string> {
429
- const hash = cr.createHash(algorithm_, options_)
430
- for await (const chunk of f_.itBuff())
431
- hash.update(chunk)
432
- return encoding_ ? hash.digest(encoding_) : hash.digest()
433
- }
434
- export function streamHashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions): Buffer
435
- export function streamHashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): string
436
- export function streamHashSync(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Buffer | string {
437
- const hash = cr.createHash(algorithm_, options_)
438
- for (const chunk of f_.itBuffSync())
439
- hash.update(chunk)
440
- return encoding_ ? hash.digest(encoding_) : hash.digest()
441
- }
442
-
443
- export async function* itLines(f_: File, options_: fs.ReadStreamOptions = { encoding: 'utf-8' }) {
444
- const readStream = fs.createReadStream(f_.isAt, options_)
445
- const rlInterface = rl.createInterface({ input: readStream, crlfDelay: Infinity })
446
- try {
447
- for await (const line of rlInterface)
448
- yield line
449
- } finally {
450
- rlInterface.close()
451
- readStream.destroy()
452
- }
453
- }
454
-
455
- export async function fileSameAs(f1_: File, f2_: File): Promise<boolean> {
456
- if (f1_.isAt === f2_.isAt)
457
- return true
458
- else if ((await fp.lstat(f1_.isAt)).size !== (await fp.lstat(f2_.isAt)).size)
459
- return false
460
- const iter1 = f1_.itBuff()
461
- const iter2 = f2_.itBuff()
462
- while (true) {
463
- const [a, b] = await Promise.all([iter1.next(), iter2.next()])
464
- if (a.done && b.done) return true
465
- if (a.done !== b.done) return false
466
- if (!a.value!.equals(b.value!)) return false
467
- }
468
- }
469
- export function fileSameAsSync(f1_: File, f2_: File): boolean {
470
- if (f1_.isAt === f2_.isAt)
471
- return true
472
- else if (fs.statSync(f1_.isAt).size !== fs.statSync(f2_.isAt).size)
473
- return false
474
- const iter1 = f1_.itBuffSync()
475
- const iter2 = f2_.itBuffSync()
476
- while (true) {
477
- const a = iter1.next()
478
- const b = iter2.next()
479
- if (a.done && b.done) return true
480
- if (a.done !== b.done) return false
481
- if (!a.value!.equals(b.value!)) return false
482
- }
483
- }
484
-
485
475
 
476
+ /** Helper type for filtering {@link Road} instances. */
477
+ export type filter_t<T extends Road> = ((road: Road) => road is T)
486
478
 
487
479
  export class Folder extends Road {
488
480
  static async create(at_: string) {
@@ -503,133 +495,130 @@ export class Folder extends Road {
503
495
  }
504
496
 
505
497
  it(): AsyncIterable<Road>
506
- it<T extends Road>(expectedType_: new (..._: any[]) => T): AsyncIterable<T>
507
- async *it<T extends Road>(expectedType_?: new (..._: any[]) => T): AsyncIterable<Road> | AsyncIterable<T> {
508
- for (const entryName of await fp.readdir(this.isAt)) {
509
- const road = await factory(this.join(entryName))
510
- if (!expectedType_ || road instanceof expectedType_)
498
+ it<T extends Road>(filter_: filter_t<T>): AsyncIterable<T>
499
+ async *it<T extends Road>(filter_?: filter_t<T>): AsyncIterable<Road> | AsyncIterable<T> {
500
+ for (const entry of await fp.readdir(this.isAt, { withFileTypes: true })) {
501
+ const road = new (resolveDirent(entry))(this.join(entry.name), false)
502
+ if (!filter_ || filter_(road))
511
503
  yield road
512
504
  }
513
505
  }
514
506
  itSync(): Iterable<Road>
515
- itSync<T extends Road>(expectedType_: new (..._: any[]) => T): Iterable<T>
516
- *itSync<T extends Road>(expectedType_?: new (..._: any[]) => T): Iterable<Road> | Iterable<T> {
517
- for (const entry of fs.readdirSync(this.isAt)) {
518
- const road = factorySync(this.join(entry))
519
- if (!expectedType_ || road instanceof expectedType_)
507
+ itSync<T extends Road>(filter_: filter_t<T>): Iterable<T>
508
+ *itSync<T extends Road>(filter_?: filter_t<T>): Iterable<Road> | Iterable<T> {
509
+ for (const entry of fs.readdirSync(this.isAt, { withFileTypes: true })) {
510
+ const road = new (resolveDirent(entry))(this.join(entry.name), false)
511
+ if (!filter_ || filter_(road))
520
512
  yield road
521
513
  }
522
514
  }
515
+
523
516
  async list(): Promise<Road[]>
524
- async list<T extends Road>(expectedType_: new (..._: any[]) => T): Promise<T[]>
525
- async list<T extends Road>(expectedType_?: new (..._: any[]) => T): Promise<Road[] | T[]> {
526
- const entries = (await fp.readdir(this.isAt)).map(async entry => factory(this.join(entry)))
527
- const resolvedEntries = await Promise.all(entries)
528
- if (!expectedType_)
529
- return resolvedEntries
530
- return resolvedEntries.filter(entry => entry instanceof expectedType_) as unknown as T[]
517
+ async list<T extends Road>(filter_: filter_t<T>): Promise<T[]>
518
+ async list<T extends Road>(filter_?: filter_t<T>): Promise<Road[] | T[]> {
519
+ const entries = (await fp.readdir(this.isAt, { withFileTypes: true })).map(e => new (resolveDirent(e))(this.join(e.name), false))
520
+ return filter_ ? entries.filter(entry => filter_(entry)) : entries
531
521
  }
532
522
  listSync(): Road[]
533
- listSync<T extends Road>(expectedType_: new (..._: any[]) => T): T[]
534
- listSync<T extends Road>(expectedType_?: new (..._: any[]) => T): Road[] | T[] {
535
- const entries = fs.readdirSync(this.isAt).map(entry => factorySync(this.join(entry)))
536
- if (!expectedType_)
537
- return entries
538
- return entries.filter(entry => entry instanceof expectedType_) as unknown as T[]
523
+ listSync<T extends Road>(filter_: filter_t<T>): T[]
524
+ listSync<T extends Road>(filter_?: filter_t<T>): Road[] | T[] {
525
+ const entries = fs.readdirSync(this.isAt, { withFileTypes: true }).map(e => new (resolveDirent(e))(this.join(e.name), false))
526
+ return filter_ ? entries.filter(entry => filter_(entry)) : entries
527
+ }
528
+
529
+ walk(): AsyncIterable<Road>
530
+ walk<T extends Road>(filter_: filter_t<T>): AsyncIterable<T>
531
+ walk(filter_: (r: Road) => boolean): AsyncIterable<Road>
532
+ async *walk<T extends Road>(filter_?: filter_t<T> | ((r: Road) => boolean)): AsyncIterable<T> | AsyncIterable<Road> {
533
+ for await (const entry of this.it()) {
534
+ if (!filter_ || filter_(entry))
535
+ yield entry
536
+
537
+ if (entry.isDir())
538
+ yield* entry.walk(filter_!)
539
+ }
540
+ }
541
+ walkSync(): Iterable<Road>
542
+ walkSync<T extends Road>(filter_: filter_t<T>): Iterable<T>
543
+ walkSync(filter_: (r: Road) => boolean): Iterable<Road>
544
+ *walkSync<T extends Road>(filter_?: filter_t<T> | ((r: Road) => boolean)): Iterable<T> | Iterable<Road> {
545
+ for (const entry of this.itSync()) {
546
+ if (!filter_ || filter_(entry))
547
+ yield entry
548
+
549
+ if (entry.isDir())
550
+ yield* entry.walkSync(filter_!)
551
+ }
539
552
  }
540
553
 
541
554
  async find(name_: string): Promise<Road | null>
542
- async find<T extends Road>(name_: string, expectedType_: new (..._: any[]) => T): Promise<T | null>
543
- async find<T extends Road>(name_: string, expectedType_?: new (..._: any[]) => T): Promise<Road | T | null> {
555
+ async find<T extends Road>(name_: string, expect_: road_t<T>): Promise<T | null>
556
+ async find<T extends Road>(name_: string, expect_?: road_t<T>): Promise<Road | T | null> {
544
557
  try {
545
- await fp.access(this.join(name_), fs.constants.F_OK)
546
558
  const found = await factory(this.join(name_))
547
- if (!expectedType_)
559
+ if (!expect_)
548
560
  return found
549
- if (found instanceof expectedType_)
561
+ if (found instanceof expect_)
550
562
  return found as T
551
563
  return null
552
- } catch {
553
- return null
554
564
  }
565
+ catch { return null }
555
566
  }
556
567
  findSync(name_: string): Road | null
557
- findSync<T extends Road>(name_: string, expectedType_: new (..._: any[]) => T): T | null
558
- findSync<T extends Road>(name_: string, expectedType_?: new (..._: any[]) => T): Road | T | null {
568
+ findSync<T extends Road>(name_: string, expect_: road_t<T>): T | null
569
+ findSync<T extends Road>(name_: string, expect_?: road_t<T>): Road | T | null {
559
570
  try {
560
571
  const found = factorySync(this.join(name_))
561
- if (!expectedType_)
572
+ if (!expect_)
562
573
  return found
563
- if (found instanceof expectedType_)
574
+ if (found instanceof expect_)
564
575
  return found as T
565
576
  return null
566
- } catch {
567
- return null
568
577
  }
578
+ catch(e: unknown) { return null }
569
579
  }
570
580
 
571
- async add<T extends Road>(name_: string, createable_: { create: (at: string) => Promise<T> }): Promise<T> {
581
+ async add<T extends Road>(name_: string, createable_: { mk: (at: string) => Promise<T> }): Promise<T> {
572
582
  const newPath = this.join(name_)
573
- await createable_.create(newPath)
583
+ await createable_.mk(newPath)
574
584
  return (await factory(newPath)) as unknown as T
575
585
  }
576
- addSync<T extends Road>(name_: string, createable_: { createSync: (at: string) => T }): T {
586
+ addSync<T extends Road>(name_: string, createable_: { mkSync: (at: string) => T }): T {
577
587
  const newPath = this.join(name_)
578
- createable_.createSync(newPath)
588
+ createable_.mkSync(newPath)
579
589
  return factorySync(newPath) as unknown as T
580
590
  }
581
591
 
582
- async delete(options_: fs.RmOptions = { recursive: true }) {
583
- using _ = await this.lock()
584
- await fp.rm(this.isAt, options_)
585
- }
586
- deleteSync(options_: fs.RmOptions = { recursive: true }) {
587
- using _ = this.lockSync()
588
- fs.rmSync(this.isAt, options_)
589
- }
590
- async move(into_: Folder) {
591
- using _ = await this.lock()
592
- const newPath = into_.join(this.name)
593
- await fp.rename(this.isAt, newPath)
594
- this.pointsTo = newPath
595
- }
596
- moveSync(into_: Folder) {
597
- using _ = this.lockSync()
598
- const newPath = into_.join(this.name)
599
- fs.renameSync(this.isAt, newPath)
600
- this.pointsTo = newPath
592
+ async borrow<T extends Road>(createable_: { mk: (at: string) => Promise<T> }, cb_: (r: T) => Promise<void> | void): Promise<void> {
593
+ const path = this.join(`instrumentality@${crypto.randomUUID()}`)
594
+ try { await cb_(await createable_.mk(path)) }
595
+ finally { await fp.rm(path, { recursive: true, force: true }) }
601
596
  }
602
- async copy(into_: Folder): Promise<this> {
603
- const newPath = into_.join(this.name)
604
- await fp.cp(this.isAt, newPath, { recursive: true })
605
- return new Folder(newPath, false) as this
597
+ borrowSync<T extends Road>(createable_: { mkSync: (at: string) => T }, cb_: (r: T) => void): void {
598
+ const path = this.join(`instrumentality@${crypto.randomUUID()}`)
599
+ try { cb_(createable_.mkSync(path)) }
600
+ finally { fs.rmSync(path, { recursive: true, force: true }) }
606
601
  }
607
- copySync(into_: Folder): this {
608
- const newPath = into_.join(this.name)
609
- fs.cpSync(this.isAt, newPath, { recursive: true })
610
- return new Folder(newPath, false) as this
611
- }
612
- async rename(to_: string) {
613
- using _ = await this.lock()
614
- const newPath = this.parent().join(to_)
615
- await fp.rename(this.isAt, newPath)
616
- this.pointsTo = newPath
602
+
603
+ async size(): Promise<number> {
604
+ let size = 0
605
+ for await (const entry of this.it())
606
+ size += await entry.size()
607
+ return size
617
608
  }
618
- renameSync(to_: string) {
619
- using _ = this.lockSync()
620
- const newPath = this.parent().join(to_)
621
- fs.renameSync(this.isAt, newPath)
622
- this.pointsTo = newPath
609
+ sizeSync(): number {
610
+ let size = 0
611
+ for (const entry of this.itSync())
612
+ size += entry.sizeSync()
613
+ return size
623
614
  }
624
615
 
625
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isDirectory() }
626
- checkSync(): boolean { return fs.lstatSync(this.isAt).isDirectory() }
616
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isDirectory() } catch { return false } }
617
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isDirectory() } catch { return false } }
627
618
 
628
619
  override isFolder(): this is Folder { return true as const }
629
620
  override isDir(): this is Folder { return true as const }
630
621
  override isDirectory(): this is Folder { return true as const }
631
- override isDict(): this is Folder { return true as const }
632
- override isDictionary(): this is Folder { return true as const }
633
622
  }
634
623
 
635
624
 
@@ -662,61 +651,30 @@ export class SymbolicLink extends Road {
662
651
  return factorySync(ph.resolve(ph.dirname(this.isAt), fs.readlinkSync(this.isAt)))
663
652
  }
664
653
  async retarget(to_: Road) {
665
- await this.delete()
666
- return fp.symlink(to_.isAt, this.isAt)
654
+ using _ = await this.lock()
655
+ await fp.unlink(this.isAt)
656
+ await fp.symlink(to_.isAt, this.isAt)
667
657
  }
668
658
  retargetSync(to_: Road) {
669
- this.deleteSync()
659
+ using _ = this.lockSync()
660
+ fs.unlinkSync(this.isAt)
670
661
  fs.symlinkSync(to_.isAt, this.isAt)
671
662
  }
672
663
 
673
- async delete() {
664
+ async size(): Promise<number> { return (await this.lstat()).size }
665
+ sizeSync(): number { return this.lstatSync().size }
666
+
667
+ override async delete() {
674
668
  using _ = await this.lock()
675
669
  await fp.unlink(this.isAt)
676
670
  }
677
- deleteSync() {
671
+ override deleteSync() {
678
672
  using _ = this.lockSync()
679
673
  fs.unlinkSync(this.isAt)
680
674
  }
681
- async move(into_: Folder) {
682
- using _ = await this.lock()
683
- const newPath = into_.join(this.name)
684
- await fp.rename(this.isAt, newPath)
685
- this.pointsTo = newPath
686
- }
687
- moveSync(into_: Folder) {
688
- using _ = this.lockSync()
689
- const newPath = into_.join(this.name)
690
- fs.renameSync(this.isAt, newPath)
691
- this.pointsTo = newPath
692
- }
693
- async copy(into_: Folder): Promise<this> {
694
- const newPath = into_.join(this.name)
695
- const target = await this.target()
696
- await fp.symlink(target.isAt, newPath)
697
- return new SymbolicLink(newPath, false) as this
698
- }
699
- copySync(into_: Folder): this {
700
- const newPath = into_.join(this.name)
701
- const target = this.targetSync()
702
- fs.symlinkSync(target.isAt, newPath)
703
- return new SymbolicLink(newPath, false) as this
704
- }
705
- async rename(to_: string) {
706
- using _ = await this.lock()
707
- const newPath = this.parent().join(to_)
708
- await fp.rename(this.isAt, newPath)
709
- this.pointsTo = newPath
710
- }
711
- renameSync(to_: string) {
712
- using _ = this.lockSync()
713
- const newPath = this.parent().join(to_)
714
- fs.renameSync(this.isAt, newPath)
715
- this.pointsTo = newPath
716
- }
717
675
 
718
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isSymbolicLink() }
719
- checkSync(): boolean { return fs.lstatSync(this.isAt).isSymbolicLink() }
676
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isSymbolicLink() } catch { return false } }
677
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isSymbolicLink() } catch { return false } }
720
678
 
721
679
  override isSymlink(): this is SymbolicLink { return true as const }
722
680
  override isSymbolicLink(): this is SymbolicLink { return true as const }
@@ -727,81 +685,49 @@ export { SymbolicLink as Symlink }
727
685
 
728
686
  export abstract class UnusableRoad extends Road {
729
687
  override readonly mutable: boolean = false // Modification will cause system issues (e.g. deleting a device file)
730
- constructor(...args_: ConstructorParameters<typeof Road>) {
731
- super(...args_)
732
- Object.freeze(this)
733
- }
734
- error(): never { throw new RdErr(`${this.constructor.name} at '${this.isAt}' is a system-level resource thus not subject to modification.`) }
735
- override async lock(): Promise<never> { return this.error() }
688
+ async size(): Promise<0> { return 0 }
689
+ sizeSync(): 0 { return 0 }
690
+ error(): never { throw new Err(`${this.constructor.name} at '${this.isAt}' is a system-level resource thus not subject to modification.`) }
691
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be locked */
692
+ override lock(): never { return this.error() }
693
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be locked */
736
694
  override lockSync(): never { return this.error() }
737
- override async delete(): Promise<never> { return this.error() }
695
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be deleted */
696
+ override delete(): never { return this.error() }
697
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be deleted */
738
698
  override deleteSync(): never { return this.error() }
739
- override async move(): Promise<never> { return this.error() }
699
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be moved */
700
+ override move(): never { return this.error() }
701
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be moved */
740
702
  override moveSync(): never { return this.error() }
741
- override async copy(): Promise<never> { return this.error() }
703
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be copied */
704
+ override copy(): never { return this.error() }
705
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be copied */
742
706
  override copySync(): never { return this.error() }
743
- override async rename(): Promise<never> { return this.error() }
707
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be renamed */
708
+ override rename(): never { return this.error() }
709
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be renamed */
744
710
  override renameSync(): never { return this.error() }
745
-
711
+
746
712
  override isUnusable(): this is UnusableRoad { return true as const }
747
713
  }
748
714
  export class BlockDevice extends UnusableRoad {
749
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isBlockDevice() }
750
- checkSync(): boolean { return fs.lstatSync(this.isAt).isBlockDevice() }
715
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isBlockDevice() } catch { return false } }
716
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isBlockDevice() } catch { return false } }
751
717
  override isBlockDevice(): this is BlockDevice { return true as const }
752
718
  }
753
719
  export class CharacterDevice extends UnusableRoad {
754
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isCharacterDevice() }
755
- checkSync(): boolean { return fs.lstatSync(this.isAt).isCharacterDevice() }
720
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isCharacterDevice() } catch { return false } }
721
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isCharacterDevice() } catch { return false } }
756
722
  override isCharacterDevice(): this is CharacterDevice { return true as const }
757
723
  }
758
724
  export class Fifo extends UnusableRoad {
759
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isFIFO() }
760
- checkSync(): boolean { return fs.lstatSync(this.isAt).isFIFO() }
725
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isFIFO() } catch { return false } }
726
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isFIFO() } catch { return false } }
761
727
  override isFifo(): this is Fifo { return true as const }
762
728
  }
763
729
  export class Socket extends UnusableRoad {
764
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isSocket() }
765
- checkSync(): boolean { return fs.lstatSync(this.isAt).isSocket() }
730
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isSocket() } catch { return false } }
731
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isSocket() } catch { return false } }
766
732
  override isSocket(): this is Socket { return true as const }
767
- }
768
-
769
-
770
-
771
- let finalizer: FinalizationRegistry<string> | null = null
772
- let toDelete: Set<string> | null = null
773
- let exitHandlerRegistered: boolean | null = null
774
- /**
775
- * Forcefully cleans up all files and folders registered for cleanup on exit.
776
- */
777
- function forceCleanupToDelete() {
778
- for (const path of toDelete ?? [])
779
- try { fs.rmSync(path, { force: true, recursive: true }) } catch {}
780
- toDelete?.clear()
781
- toDelete = null
782
- finalizer = null
783
- if (exitHandlerRegistered)
784
- process.off('exit', forceCleanupToDelete)
785
- exitHandlerRegistered = false
786
- }
787
- export function registerToCleanup(self_: Road) {
788
- finalizer ??= new FinalizationRegistry<string>(p => { try { fs.rmSync(p, { force: true, recursive: true }) } catch {}; toDelete?.delete(p) })
789
- toDelete ??= new Set()
790
- if (!exitHandlerRegistered) {
791
- process.once('exit', forceCleanupToDelete)
792
- exitHandlerRegistered = true
793
- }
794
- toDelete.add(self_.isAt)
795
- finalizer.register(self_, self_.isAt, self_)
796
- }
797
-
798
-
799
- export function Temp<T extends Road>(createable_: { createSync: (at: string) => T }, autoCleanup_: boolean): T & Disposable & AsyncDisposable {
800
- let t = createable_.createSync(tmp().join(`instrumentality@${cr.randomUUID()}`))
801
- if (autoCleanup_)
802
- registerToCleanup(t)
803
- return Object.freeze(Object.assign(t, {
804
- [Symbol.dispose]() { try { t.deleteSync() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) },
805
- async [Symbol.asyncDispose]() { try { await t.delete() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) }
806
- }))
807
- }11
733
+ }