instrumentality 0.0.7 → 0.0.8

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.
@@ -98,84 +120,80 @@ 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}'`)
128
+ throw new Err(`Type mismatch: '${this.isAt}'`)
107
129
  }
108
130
 
131
+ /** @returns An instance of {@link Folder} representing the parent directory of the current road. */
132
+ parent(): Folder { return new Folder(ph.dirname(this.isAt), false) }
133
+ /**
134
+ * 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.
135
+ *
136
+ * @yields Each ancestor folder as a {@link Folder} instance.
137
+ */
138
+ *ancestorsIt() {
139
+ let current: Folder = this.parent()
140
+ let parent = current.parent()
141
+ while (current.isAt !== parent.isAt) {
142
+ yield current
143
+ current = parent
144
+ parent = current.parent()
145
+ }
146
+ }
147
+ /** @returns An array of {@link Folder} instances representing the ancestors of the current road. */
148
+ ancestors(): Folder[] { return [...this.ancestorsIt()] }
149
+
109
150
  /**
110
151
  * 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
152
  *
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
153
  * @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.
154
+ * @throws If the road is immutable, a {@link Err} will be thrown.
115
155
  *
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).
156
+ * @remarks This method MUST be used with a `using` statement to ensure that the lock is released properly. Failing to do so WILL result in deadlocks and other concurrency issues.
157
+ * 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 (for which the `using` statement is a convenient way to do so).
158
+ * @remarks Methods may call other methods that each acquire their own locks, and release them independently, meaning from the perspective of other instances, the lock may appear to be free even if the original method still has work to do.
159
+ * To fix this, it's recommeneded to aquire a lock at the beginning and use fs/fp operations within the locked context.
118
160
  */
119
- protected async lock(cb_ = () => {}): Promise<AsyncDisposable & Disposable> {
161
+ protected async lock(): Promise<Disposable & AsyncDisposable> {
120
162
  if (!this.mutable)
121
- throw new RdErr(`Road to '${this.isAt}' is immutable.`)
122
- lockedRoads ??= new Map<string, Promise<void>>()
163
+ throw new Err(`Road to '${this.isAt}' is immutable.`)
164
+ lockedRoads ??= new Map()
165
+ const { promise, resolve } = Promise.withResolvers<void>()
166
+ const previous = lockedRoads.get(this.isAt)
167
+ lockedRoads.set(this.isAt, promise)
123
168
  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_()
169
+ const dispose = () => {
170
+ resolve()
171
+ if (lockedRoads!.get(isAt) === promise)
133
172
  lockedRoads!.delete(isAt)
134
- }
135
173
  }
174
+ await previous
175
+ return { [Symbol.dispose]: dispose, [Symbol.asyncDispose]: dispose as any }
136
176
  }
137
177
  /**
138
178
  * 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.
179
+ * @throws If the road is currently locked by another operation as it cannot wait for the lock to be released in a synchronous context.
140
180
  */
141
- protected lockSync(cb_ = () => {}): Disposable & AsyncDisposable {
181
+ protected lockSync(): Disposable & AsyncDisposable {
142
182
  if (!this.mutable)
143
- throw new RdErr(`Road to '${this.isAt}' is immutable.`)
144
- lockedRoads ??= new Map<string, Promise<void>>()
183
+ throw new Err(`Road to '${this.isAt}' is immutable.`)
184
+ lockedRoads ??= new Map()
185
+ const { promise, resolve } = Promise.withResolvers<void>()
186
+ if (lockedRoads.has(this.isAt))
187
+ throw new Err(`Road to '${this.isAt}' is already locked.`)
188
+ lockedRoads.set(this.isAt, promise)
145
189
  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]() {
190
+ const dispose = () => {
191
+ resolve()
192
+ if (lockedRoads!.get(isAt) === promise)
151
193
  lockedRoads!.delete(isAt)
152
- cb_()
153
- },
154
- async [Symbol.asyncDispose]() {
155
- lockedRoads!.delete(isAt)
156
- cb_()
157
- }
158
- }
159
- }
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) }
163
- /**
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.
167
- */
168
- *ancestorsIt() {
169
- let current: Folder = this.parent()
170
- let parent = current.parent()
171
- while (current.isAt !== parent.isAt) {
172
- yield current
173
- current = parent
174
- parent = current.parent()
175
194
  }
195
+ return { [Symbol.dispose]: dispose, [Symbol.asyncDispose]: dispose as any }
176
196
  }
177
- /** @returns An array of {@link Folder} instances representing the ancestors of the current road. */
178
- ancestors(): Folder[] { return [...this.ancestorsIt()] }
179
197
 
180
198
  /**
181
199
  * Watches the current entry for changes and resolves when the entry becomes accessible (i.e., exists and can be accessed).
@@ -223,20 +241,56 @@ export abstract class Road {
223
241
  lstat() { return fp.lstat(this.isAt) }
224
242
  /** @returns The result of {@link fs.lstatSync} for the current entry. */
225
243
  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
244
 
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>
245
+ /** Get the size of the this entry in bytes. */
246
+ abstract size(): Promise<number>
247
+ abstract sizeSync(): number
248
+
249
+ /** Wrapper around {@link fp.rm} with locking. */
250
+ async delete(): Promise<void> {
251
+ using _ = await this.lock()
252
+ await fp.rm(this.isAt, { recursive: true, force: true })
253
+ }
254
+ /** Wrapper around {@link fs.rmSync} with locking. */
255
+ deleteSync(): void {
256
+ using _ = this.lockSync()
257
+ fs.rmSync(this.isAt, { recursive: true, force: true })
258
+ }
259
+ async copy(into_: Folder, options_?: fs.CopyOptions): Promise<this> {
260
+ const newPath = into_.join(this.name)
261
+ await fp.cp(this.isAt, newPath, { recursive: true, ...options_ })
262
+ return new (this.constructor as new (path: string, typeCheck: boolean) => this)(newPath, false)
263
+ }
264
+ copySync(into_: Folder, options_?: fs.CopySyncOptions): this {
265
+ const newPath = into_.join(this.name)
266
+ fs.cpSync(this.isAt, newPath, { recursive: true, ...options_ })
267
+ return new (this.constructor as new (path: string, typeCheck: boolean) => this)(newPath, false)
268
+ }
269
+ async move(into_: Folder): Promise<void> {
270
+ using _ = await this.lock()
271
+ const newPath = into_.join(this.name)
272
+ await fp.rename(this.isAt, newPath)
273
+ this.pointsTo = newPath
274
+ }
275
+ moveSync(into_: Folder): void {
276
+ using _ = this.lockSync()
277
+ const newPath = into_.join(this.name)
278
+ fs.renameSync(this.isAt, newPath)
279
+ this.pointsTo = newPath
280
+ }
281
+ async rename(newName_: string): Promise<void> {
282
+ using _ = await this.lock()
283
+ const newPath = this.parent().join(newName_)
284
+ await fp.rename(this.isAt, newPath)
285
+ this.pointsTo = newPath
286
+ }
287
+ renameSync(newName_: string): void {
288
+ using _ = this.lockSync()
289
+ const newPath = this.parent().join(newName_)
290
+ fs.renameSync(this.isAt, newPath)
291
+ this.pointsTo = newPath
292
+ }
293
+
240
294
 
241
295
  // jsdocs for the abstract methods are in the subclasses
242
296
  abstract check(): Promise<boolean>
@@ -250,10 +304,6 @@ export abstract class Road {
250
304
  isFolder(): this is Folder { return false as const }
251
305
  /** Type narrowing for {@link Folder} (similar to `instanceof` without unnecessary runtime checks). */
252
306
  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
307
  /** Type narrowing for {@link SymbolicLink} (similar to `instanceof` without unnecessary runtime checks). */
258
308
  isSymlink(): this is SymbolicLink { return false as const }
259
309
  /** Type narrowing for {@link SymbolicLink} (similar to `instanceof` without unnecessary runtime checks). */
@@ -269,28 +319,47 @@ export abstract class Road {
269
319
  /** Type narrowing for {@link Socket} (similar to `instanceof` without unnecessary runtime checks). */
270
320
  isSocket(): this is Socket { return false as const }
271
321
  }
272
- export type road_t = ConstructorParameters<typeof Road>
322
+ /** Constructor type for a subclass of {@link Road}. */
323
+ export type road_t<T extends Road> = new (...args_: ConstructorParameters<typeof Road>) => T
273
324
 
274
325
 
275
326
 
276
327
  /** Subclass of {@link Road} that represents a file. */
277
328
  export class File extends Road {
329
+ /**
330
+ * Creates a new file at the specified path if it does not already exist.
331
+ *
332
+ * @param at_ The path at which to create the file.
333
+ * @returns A promise that resolves to the newly created `File` instance.
334
+ * @throws if {@link fp.writeFile} throws.
335
+ */
278
336
  static async create(at_: string) {
279
337
  try { await fp.access(at_, fsc.W_OK) }
280
338
  catch { await fp.writeFile(at_, "") }
281
339
  return new File(at_, true)
282
340
  }
341
+ /** Alias for {@link File.create}. */
283
342
  static readonly mk: typeof File.create = File.create
343
+ /** Synchronous version of {@link File.create}. */
284
344
  static createSync(at_: string) {
285
345
  try { fs.accessSync(at_, fs.constants.W_OK) }
286
346
  catch { fs.writeFileSync(at_, "") }
287
347
  return new File(at_, true)
288
348
  }
349
+ /** Alias for {@link File.createSync}. */
289
350
  static readonly mkSync: typeof File.createSync = File.createSync
290
351
 
352
+ /** The file extension of this file, including the leading dot. */
291
353
  get ext() { return ph.extname(this.isAt) }
354
+ /** The file name without its extension. */
292
355
  get noExt() { return ph.basename(this.isAt, this.ext) }
293
356
 
357
+ /**
358
+ * Reads the contents of the file.
359
+ *
360
+ * @returns A promise that resolves to the contents of the file as a `Buffer` or `string`, depending on the specified encoding.
361
+ * @throws If the file can't be read due to permission issues or other filesystem errors.
362
+ */
294
363
  async read(): Promise<Buffer>
295
364
  async read(encoding_: BufferEncoding, flag_?: string): Promise<string>
296
365
  async read(encoding_?: BufferEncoding, flag_?: string): Promise<Buffer | string> {
@@ -308,16 +377,45 @@ export class File extends Road {
308
377
  return fs.readFileSync(this.isAt)
309
378
  }
310
379
 
380
+ async sameAs(other_: File): Promise<boolean> {
381
+ if (this.isAt === other_.isAt)
382
+ return true
383
+ else if ((await fp.lstat(this.isAt)).size !== (await fp.lstat(other_.isAt)).size)
384
+ return false
385
+ const iter1 = this.itBuff()
386
+ const iter2 = other_.itBuff()
387
+ while (true) {
388
+ const [a, b] = await Promise.all([iter1.next(), iter2.next()])
389
+ if (a.done && b.done) return true
390
+ if (a.done !== b.done) return false
391
+ if (!a.value!.equals(b.value!)) return false
392
+ }
393
+ }
394
+ sameAsSync(other_: File): boolean {
395
+ if (this.isAt === other_.isAt)
396
+ return true
397
+ else if (fs.lstatSync(this.isAt).size !== fs.lstatSync(other_.isAt).size)
398
+ return false
399
+ const iter1 = this.itBuffSync()
400
+ const iter2 = other_.itBuffSync()
401
+ while (true) {
402
+ const a = iter1.next()
403
+ const b = iter2.next()
404
+ if (a.done && b.done) return true
405
+ if (a.done !== b.done) return false
406
+ if (!a.value!.equals(b.value!)) return false
407
+ }
408
+ }
409
+
311
410
  async *itBuff(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode) {
312
411
  const fd = await fp.open(this.isAt, flags_, mode_)
313
412
  try {
314
413
  const buffer = Buffer.alloc(chunkSize_)
315
414
  let bytesRead: number
316
415
  do {
317
- const readResult = await fd.read(buffer, 0, chunkSize_, null)
318
- bytesRead = readResult.bytesRead
416
+ bytesRead = (await fd.read(buffer, 0, chunkSize_, null)).bytesRead
319
417
  if (bytesRead > 0)
320
- yield buffer.subarray(0, bytesRead)
418
+ yield Buffer.from(buffer.subarray(0, bytesRead))
321
419
  } while (bytesRead === chunkSize_)
322
420
  } finally {
323
421
  await fd.close()
@@ -331,12 +429,81 @@ export class File extends Road {
331
429
  do {
332
430
  bytesRead = fs.readSync(fd, buffer, 0, chunkSize_, null)
333
431
  if (bytesRead > 0)
334
- yield buffer.subarray(0, bytesRead)
432
+ yield Buffer.from(buffer.subarray(0, bytesRead))
335
433
  } while (bytesRead === chunkSize_)
336
434
  } finally {
337
435
  fs.closeSync(fd)
338
436
  }
339
437
  }
438
+ async *itLines(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode) {
439
+ const decoder = new TextDecoder("utf-8", { fatal: true })
440
+ let carry = ""
441
+
442
+ for await (const chunk of this.itBuff(chunkSize_, flags_, mode_)) {
443
+ carry += decoder.decode(chunk, { stream: true })
444
+ let newlineINdex: number
445
+ while ((newlineINdex = carry.indexOf("\n")) !== -1) {
446
+ const line = carry.slice(0, newlineINdex)
447
+ yield line.endsWith("\r") ? line.slice(0, -1) : line
448
+ carry = carry.slice(newlineINdex + 1)
449
+ }
450
+ }
451
+ carry += decoder.decode()
452
+ if (carry.length > 0)
453
+ yield carry.endsWith("\r") ? carry.slice(0, -1) : carry
454
+ }
455
+ *itLinesSync(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode) {
456
+ const decoder = new TextDecoder("utf-8", { fatal: true })
457
+ let carry = ""
458
+
459
+ for (const chunk of this.itBuffSync(chunkSize_, flags_, mode_)) {
460
+ carry += decoder.decode(chunk, { stream: true })
461
+ let newlineINdex: number
462
+ while ((newlineINdex = carry.indexOf("\n")) !== -1) {
463
+ const line = carry.slice(0, newlineINdex)
464
+ yield line.endsWith("\r") ? line.slice(0, -1) : line
465
+ carry = carry.slice(newlineINdex + 1)
466
+ }
467
+ }
468
+ carry += decoder.decode()
469
+ if (carry.length > 0)
470
+ yield carry.endsWith("\r") ? carry.slice(0, -1) : carry
471
+ }
472
+
473
+ async hash(algorithm_?: string, options_?: cr.HashOptions): Promise<Buffer>
474
+ async hash(algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<string>
475
+ async hash(algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<Buffer | string> {
476
+ const hash = cr.createHash(algorithm_, options_)
477
+ for await (const chunk of this.itBuff())
478
+ hash.update(chunk)
479
+ return encoding_ ? hash.digest(encoding_) : hash.digest()
480
+ }
481
+ hashSync(algorithm_?: string, options_?: cr.HashOptions): Buffer
482
+ hashSync(algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): string
483
+ hashSync(algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Buffer | string {
484
+ const hash = cr.createHash(algorithm_, options_)
485
+ for (const chunk of this.itBuffSync())
486
+ hash.update(chunk)
487
+ return encoding_ ? hash.digest(encoding_) : hash.digest()
488
+ }
489
+
490
+ async size(): Promise<number> { return (await this.lstat()).size }
491
+ sizeSync(): number { return this.lstatSync().size }
492
+
493
+ async writeAtomic(data_: Buffer | string, options_?: fs.WriteFileOptions) {
494
+ using _ = await this.lock()
495
+ const temp = await Temp(File) // Don't use `using` as we won't clean it up
496
+ await fp.rename(temp.isAt, this.parent().join(temp.name)) // Move temp file to the same directory as the target
497
+ await fp.writeFile(temp.isAt, data_, options_) // Write data to the temp file
498
+ await fp.rename(temp.isAt, this.isAt) // Replace the target file with the temp file atomically
499
+ }
500
+ writeAtomicSync(data_: Buffer | string, options_?: fs.WriteFileOptions) {
501
+ using _ = this.lockSync()
502
+ const temp = TempSync(File) // Don't use `using` as we won't clean it up
503
+ fs.renameSync(temp.isAt, this.parent().join(temp.name)) // Move temp file to the same directory as the target
504
+ fs.writeFileSync(temp.isAt, data_, options_) // Write data to the temp file
505
+ fs.renameSync(temp.isAt, this.isAt) // Replace the target file with the temp file atomically
506
+ }
340
507
 
341
508
  async write(data_: Buffer | string, options_?: fs.WriteFileOptions) {
342
509
  using _ = await this.lock()
@@ -355,134 +522,16 @@ export class File extends Road {
355
522
  fs.appendFileSync(this.isAt, data_, options_)
356
523
  }
357
524
 
358
- async delete() {
359
- using _ = await this.lock()
360
- await fp.rm(this.isAt, { force: true })
361
- }
362
- deleteSync() {
363
- using _ = this.lockSync()
364
- fs.rmSync(this.isAt, { force: true })
365
- }
366
- async move(into_: Folder) {
367
- using _ = await this.lock()
368
- const newPath = into_.join(this.name)
369
- await fp.rename(this.isAt, newPath)
370
- this.pointsTo = newPath
371
- }
372
- moveSync(into_: Folder) {
373
- 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
387
- }
388
- async rename(to_: string) {
389
- using _ = await this.lock()
390
- const newPath = this.parent().join(to_)
391
- await fp.rename(this.isAt, newPath)
392
- this.pointsTo = newPath
393
- }
394
- renameSync(to_: string) {
395
- using _ = this.lockSync()
396
- const newPath = this.parent().join(to_)
397
- fs.renameSync(this.isAt, newPath)
398
- this.pointsTo = newPath
399
- }
400
-
401
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isFile() }
402
- checkSync(): boolean { return fs.lstatSync(this.isAt).isFile() }
525
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isFile() } catch { return false } }
526
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isFile() } catch { return false } }
403
527
 
404
528
  override isFile(): this is File { return true as const }
405
529
  }
406
530
 
407
531
 
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
532
 
533
+ /** Helper type for filtering {@link Road} instances. */
534
+ export type filter_t<T extends Road> = ((road: Road) => road is T)
486
535
 
487
536
  export class Folder extends Road {
488
537
  static async create(at_: string) {
@@ -503,50 +552,75 @@ export class Folder extends Road {
503
552
  }
504
553
 
505
554
  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_)
555
+ it<T extends Road>(filter_: filter_t<T>): AsyncIterable<T>
556
+ async *it<T extends Road>(filter_?: filter_t<T>): AsyncIterable<Road> | AsyncIterable<T> {
557
+ for (const entry of await fp.readdir(this.isAt, { withFileTypes: true })) {
558
+ const road = new (resolveDirent(entry))(this.join(entry.name), false)
559
+ if (!filter_ || filter_(road))
511
560
  yield road
512
561
  }
513
562
  }
514
563
  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_)
564
+ itSync<T extends Road>(filter_: filter_t<T>): Iterable<T>
565
+ *itSync<T extends Road>(filter_?: filter_t<T>): Iterable<Road> | Iterable<T> {
566
+ for (const entry of fs.readdirSync(this.isAt, { withFileTypes: true })) {
567
+ const road = new (resolveDirent(entry))(this.join(entry.name), false)
568
+ if (!filter_ || filter_(road))
520
569
  yield road
521
570
  }
522
571
  }
572
+
523
573
  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)))
574
+ async list<T extends Road>(filter_: filter_t<T>): Promise<T[]>
575
+ async list<T extends Road>(filter_?: filter_t<T>): Promise<Road[] | T[]> {
576
+ const entries = (await fp.readdir(this.isAt, { withFileTypes: true })).map(e => new (resolveDirent(e))(this.join(e.name), false))
527
577
  const resolvedEntries = await Promise.all(entries)
528
- if (!expectedType_)
578
+ if (!filter_)
529
579
  return resolvedEntries
530
- return resolvedEntries.filter(entry => entry instanceof expectedType_) as unknown as T[]
580
+ return resolvedEntries.filter(entry => filter_(entry))
531
581
  }
532
582
  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_)
583
+ listSync<T extends Road>(filter_: filter_t<T>): T[]
584
+ listSync<T extends Road>(filter_?: filter_t<T>): Road[] | T[] {
585
+ const entries = fs.readdirSync(this.isAt, { withFileTypes: true }).map(e => new (resolveDirent(e))(this.join(e.name), false))
586
+ if (!filter_)
537
587
  return entries
538
- return entries.filter(entry => entry instanceof expectedType_) as unknown as T[]
588
+ return entries.filter(entry => filter_(entry))
589
+ }
590
+
591
+ walk(): AsyncIterable<Road>
592
+ walk<T extends Road>(filter_: filter_t<T>): AsyncIterable<T>
593
+ walk(filter_: (r: Road) => boolean): AsyncIterable<Road>
594
+ async *walk<T extends Road>(filter_?: filter_t<T> | ((r: Road) => boolean)): AsyncIterable<T> | AsyncIterable<Road> {
595
+ for await (const entry of this.it()) {
596
+ if (!filter_ || filter_(entry))
597
+ yield entry
598
+
599
+ if (entry.isDir())
600
+ yield* entry.walk(filter_!)
601
+ }
602
+ }
603
+ walkSync(): Iterable<Road>
604
+ walkSync<T extends Road>(filter_: filter_t<T>): Iterable<T>
605
+ walkSync(filter_: (r: Road) => boolean): Iterable<Road>
606
+ *walkSync<T extends Road>(filter_?: filter_t<T> | ((r: Road) => boolean)): Iterable<T> | Iterable<Road> {
607
+ for (const entry of this.itSync()) {
608
+ if (!filter_ || filter_(entry))
609
+ yield entry
610
+
611
+ if (entry.isDir())
612
+ yield* entry.walkSync(filter_!)
613
+ }
539
614
  }
540
615
 
541
616
  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> {
617
+ async find<T extends Road>(name_: string, expect_: road_t<T>): Promise<T | null>
618
+ async find<T extends Road>(name_: string, expect_?: road_t<T>): Promise<Road | T | null> {
544
619
  try {
545
- await fp.access(this.join(name_), fs.constants.F_OK)
546
620
  const found = await factory(this.join(name_))
547
- if (!expectedType_)
621
+ if (!expect_)
548
622
  return found
549
- if (found instanceof expectedType_)
623
+ if (found instanceof expect_)
550
624
  return found as T
551
625
  return null
552
626
  } catch {
@@ -554,16 +628,16 @@ export class Folder extends Road {
554
628
  }
555
629
  }
556
630
  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 {
631
+ findSync<T extends Road>(name_: string, expect_: road_t<T>): T | null
632
+ findSync<T extends Road>(name_: string, expect_?: road_t<T>): Road | T | null {
559
633
  try {
560
634
  const found = factorySync(this.join(name_))
561
- if (!expectedType_)
635
+ if (!expect_)
562
636
  return found
563
- if (found instanceof expectedType_)
637
+ if (found instanceof expect_)
564
638
  return found as T
565
639
  return null
566
- } catch {
640
+ } catch(e: unknown) {
567
641
  return null
568
642
  }
569
643
  }
@@ -579,57 +653,25 @@ export class Folder extends Road {
579
653
  return factorySync(newPath) as unknown as T
580
654
  }
581
655
 
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
656
+ async size(): Promise<number> {
657
+ let size = 0
658
+ for await (const entry of this.it())
659
+ size += await entry.size()
660
+ return size
601
661
  }
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
606
- }
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
617
- }
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
662
+ sizeSync(): number {
663
+ let size = 0
664
+ for (const entry of this.itSync())
665
+ size += entry.sizeSync()
666
+ return size
623
667
  }
624
668
 
625
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isDirectory() }
626
- checkSync(): boolean { return fs.lstatSync(this.isAt).isDirectory() }
669
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isDirectory() } catch { return false } }
670
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isDirectory() } catch { return false } }
627
671
 
628
672
  override isFolder(): this is Folder { return true as const }
629
673
  override isDir(): this is Folder { return true as const }
630
674
  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
675
  }
634
676
 
635
677
 
@@ -662,112 +704,106 @@ export class SymbolicLink extends Road {
662
704
  return factorySync(ph.resolve(ph.dirname(this.isAt), fs.readlinkSync(this.isAt)))
663
705
  }
664
706
  async retarget(to_: Road) {
665
- await this.delete()
666
- return fp.symlink(to_.isAt, this.isAt)
707
+ using _ = await this.lock()
708
+ await fp.unlink(this.isAt)
709
+ await fp.symlink(to_.isAt, this.isAt)
667
710
  }
668
711
  retargetSync(to_: Road) {
669
- this.deleteSync()
712
+ using _ = this.lockSync()
713
+ fs.unlinkSync(this.isAt)
670
714
  fs.symlinkSync(to_.isAt, this.isAt)
671
715
  }
672
716
 
673
- async delete() {
717
+ async size(): Promise<number> { return (await this.lstat()).size }
718
+ sizeSync(): number { return this.lstatSync().size }
719
+
720
+ override async delete() {
674
721
  using _ = await this.lock()
675
722
  await fp.unlink(this.isAt)
676
723
  }
677
- deleteSync() {
724
+ override deleteSync() {
678
725
  using _ = this.lockSync()
679
726
  fs.unlinkSync(this.isAt)
680
727
  }
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
728
 
718
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isSymbolicLink() }
719
- checkSync(): boolean { return fs.lstatSync(this.isAt).isSymbolicLink() }
729
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isSymbolicLink() } catch { return false } }
730
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isSymbolicLink() } catch { return false } }
720
731
 
721
732
  override isSymlink(): this is SymbolicLink { return true as const }
722
733
  override isSymbolicLink(): this is SymbolicLink { return true as const }
723
734
  }
724
735
  export { SymbolicLink as Symlink }
736
+ here().walk(r => !r.isSymlink())
725
737
 
726
738
 
727
739
 
728
740
  export abstract class UnusableRoad extends Road {
729
741
  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() }
742
+ async size(): Promise<0> { return 0 }
743
+ sizeSync(): 0 { return 0 }
744
+ error(): never { throw new Err(`${this.constructor.name} at '${this.isAt}' is a system-level resource thus not subject to modification.`) }
745
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be locked */
746
+ override lock(): never { return this.error() }
747
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be locked */
736
748
  override lockSync(): never { return this.error() }
737
- override async delete(): Promise<never> { return this.error() }
749
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be deleted */
750
+ override delete(): never { return this.error() }
751
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be deleted */
738
752
  override deleteSync(): never { return this.error() }
739
- override async move(): Promise<never> { return this.error() }
753
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be moved */
754
+ override move(): never { return this.error() }
755
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be moved */
740
756
  override moveSync(): never { return this.error() }
741
- override async copy(): Promise<never> { return this.error() }
757
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be copied */
758
+ override copy(): never { return this.error() }
759
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be copied */
742
760
  override copySync(): never { return this.error() }
743
- override async rename(): Promise<never> { return this.error() }
761
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be renamed */
762
+ override rename(): never { return this.error() }
763
+ /** @deprecated System-level resources (UnusableRoad) can't/shouldn't be renamed */
744
764
  override renameSync(): never { return this.error() }
745
-
765
+
746
766
  override isUnusable(): this is UnusableRoad { return true as const }
747
767
  }
748
768
  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() }
769
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isBlockDevice() } catch { return false } }
770
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isBlockDevice() } catch { return false } }
751
771
  override isBlockDevice(): this is BlockDevice { return true as const }
752
772
  }
753
773
  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() }
774
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isCharacterDevice() } catch { return false } }
775
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isCharacterDevice() } catch { return false } }
756
776
  override isCharacterDevice(): this is CharacterDevice { return true as const }
757
777
  }
758
778
  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() }
779
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isFIFO() } catch { return false } }
780
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isFIFO() } catch { return false } }
761
781
  override isFifo(): this is Fifo { return true as const }
762
782
  }
763
783
  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() }
784
+ async check(): Promise<boolean> { try { return (await fp.lstat(this.isAt)).isSocket() } catch { return false } }
785
+ checkSync(): boolean { try { return fs.lstatSync(this.isAt).isSocket() } catch { return false } }
766
786
  override isSocket(): this is Socket { return true as const }
767
787
  }
768
788
 
769
789
 
770
790
 
791
+ export async function Temp<T extends Road>(createable_: { create: (at: string) => Promise<T> }): Promise<T & Disposable & AsyncDisposable> {
792
+ let t = await createable_.create(tmp().join(`instrumentality@${cr.randomUUID()}`))
793
+ return Object.assign(t, {
794
+ [Symbol.dispose]() { try { t.deleteSync() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) },
795
+ async [Symbol.asyncDispose]() { try { await t.delete() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) }
796
+ })
797
+ }
798
+ export function TempSync<T extends Road>(createable_: { createSync: (at: string) => T }): T & Disposable & AsyncDisposable {
799
+ let t = createable_.createSync(tmp().join(`instrumentality@${cr.randomUUID()}`))
800
+ return Object.assign(t, {
801
+ [Symbol.dispose]() { try { t.deleteSync() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) },
802
+ async [Symbol.asyncDispose]() { try { await t.delete() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) }
803
+ })
804
+ }
805
+
806
+
771
807
  let finalizer: FinalizationRegistry<string> | null = null
772
808
  let toDelete: Set<string> | null = null
773
809
  let exitHandlerRegistered: boolean | null = null
@@ -796,12 +832,20 @@ export function registerToCleanup(self_: Road) {
796
832
  }
797
833
 
798
834
 
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()}`))
835
+ export async function AutoTemp<T extends Road>(createable_: { mk: (at: string) => Promise<T> }): Promise<T & Disposable & AsyncDisposable> {
836
+ let t = await createable_.mk(tmp().join(`instrumentality@${cr.randomUUID()}`))
837
+ registerToCleanup(t)
838
+ return Object.freeze(Object.assign(t, {
839
+ [Symbol.dispose]() { try { t.deleteSync() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) },
840
+ async [Symbol.asyncDispose]() { try { await t.delete() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) }
841
+ }))
842
+ }
843
+ export function AutoTempSync<T extends Road>(createable_: { mk: (at: string) => T }, autoCleanup_: boolean): T & Disposable & AsyncDisposable {
844
+ let t = createable_.mk(tmp().join(`instrumentality@${cr.randomUUID()}`))
801
845
  if (autoCleanup_)
802
846
  registerToCleanup(t)
803
847
  return Object.freeze(Object.assign(t, {
804
848
  [Symbol.dispose]() { try { t.deleteSync() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) },
805
849
  async [Symbol.asyncDispose]() { try { await t.delete() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) }
806
850
  }))
807
- }11
851
+ }