instrumentality 0.0.6 → 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,26 +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}. */
342
+ static readonly mk: typeof File.create = File.create
343
+ /** Synchronous version of {@link File.create}. */
283
344
  static createSync(at_: string) {
284
345
  try { fs.accessSync(at_, fs.constants.W_OK) }
285
346
  catch { fs.writeFileSync(at_, "") }
286
347
  return new File(at_, true)
287
348
  }
349
+ /** Alias for {@link File.createSync}. */
350
+ static readonly mkSync: typeof File.createSync = File.createSync
288
351
 
352
+ /** The file extension of this file, including the leading dot. */
289
353
  get ext() { return ph.extname(this.isAt) }
354
+ /** The file name without its extension. */
290
355
  get noExt() { return ph.basename(this.isAt, this.ext) }
291
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
+ */
292
363
  async read(): Promise<Buffer>
293
364
  async read(encoding_: BufferEncoding, flag_?: string): Promise<string>
294
365
  async read(encoding_?: BufferEncoding, flag_?: string): Promise<Buffer | string> {
@@ -306,16 +377,45 @@ export class File extends Road {
306
377
  return fs.readFileSync(this.isAt)
307
378
  }
308
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
+
309
410
  async *itBuff(chunkSize_: number = 64 * 1024, flags_: string | number = 'r', mode_?: fs.Mode) {
310
411
  const fd = await fp.open(this.isAt, flags_, mode_)
311
412
  try {
312
413
  const buffer = Buffer.alloc(chunkSize_)
313
414
  let bytesRead: number
314
415
  do {
315
- const readResult = await fd.read(buffer, 0, chunkSize_, null)
316
- bytesRead = readResult.bytesRead
416
+ bytesRead = (await fd.read(buffer, 0, chunkSize_, null)).bytesRead
317
417
  if (bytesRead > 0)
318
- yield buffer.subarray(0, bytesRead)
418
+ yield Buffer.from(buffer.subarray(0, bytesRead))
319
419
  } while (bytesRead === chunkSize_)
320
420
  } finally {
321
421
  await fd.close()
@@ -329,12 +429,81 @@ export class File extends Road {
329
429
  do {
330
430
  bytesRead = fs.readSync(fd, buffer, 0, chunkSize_, null)
331
431
  if (bytesRead > 0)
332
- yield buffer.subarray(0, bytesRead)
432
+ yield Buffer.from(buffer.subarray(0, bytesRead))
333
433
  } while (bytesRead === chunkSize_)
334
434
  } finally {
335
435
  fs.closeSync(fd)
336
436
  }
337
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
+ }
338
507
 
339
508
  async write(data_: Buffer | string, options_?: fs.WriteFileOptions) {
340
509
  using _ = await this.lock()
@@ -353,134 +522,16 @@ export class File extends Road {
353
522
  fs.appendFileSync(this.isAt, data_, options_)
354
523
  }
355
524
 
356
- async delete() {
357
- using _ = await this.lock()
358
- await fp.rm(this.isAt, { force: true })
359
- }
360
- deleteSync() {
361
- using _ = this.lockSync()
362
- fs.rmSync(this.isAt, { force: true })
363
- }
364
- async move(into_: Folder) {
365
- using _ = await this.lock()
366
- const newPath = into_.join(this.name)
367
- await fp.rename(this.isAt, newPath)
368
- this.pointsTo = newPath
369
- }
370
- moveSync(into_: Folder) {
371
- using _ = this.lockSync()
372
- const newPath = into_.join(this.name)
373
- fs.renameSync(this.isAt, newPath)
374
- this.pointsTo = newPath
375
- }
376
- async copy(into_: Folder): Promise<this> {
377
- const newPath = into_.join(this.name)
378
- await fp.copyFile(this.isAt, newPath)
379
- return new File(newPath, false) as this
380
- }
381
- copySync(into_: Folder): this {
382
- const newPath = into_.join(this.name)
383
- fs.copyFileSync(this.isAt, newPath)
384
- return new File(newPath, false) as this
385
- }
386
- async rename(to_: string) {
387
- using _ = await this.lock()
388
- const newPath = this.parent().join(to_)
389
- await fp.rename(this.isAt, newPath)
390
- this.pointsTo = newPath
391
- }
392
- renameSync(to_: string) {
393
- using _ = this.lockSync()
394
- const newPath = this.parent().join(to_)
395
- fs.renameSync(this.isAt, newPath)
396
- this.pointsTo = newPath
397
- }
398
-
399
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isFile() }
400
- 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 } }
401
527
 
402
528
  override isFile(): this is File { return true as const }
403
529
  }
404
530
 
405
531
 
406
- // Bizarre functions for file I/O
407
- export async function hash(f_: File, algorithm_?: string, options_?: cr.HashOptions): Promise<Buffer>
408
- export async function hash(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<string>
409
- export async function hash(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<Buffer | string> {
410
- const hash = cr.createHash(algorithm_, options_)
411
- for await (const chunk of f_.itBuff())
412
- hash.update(chunk)
413
- return encoding_ ? hash.digest(encoding_) : hash.digest()
414
- }
415
- export function hashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions): Buffer
416
- export function hashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): string
417
- export function hashSync(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Buffer | string {
418
- const hash = cr.createHash(algorithm_, options_)
419
- for (const chunk of f_.itBuffSync())
420
- hash.update(chunk)
421
- return encoding_ ? hash.digest(encoding_) : hash.digest()
422
- }
423
-
424
- export async function streamHash(f_: File, algorithm_?: string, options_?: cr.HashOptions): Promise<Buffer>
425
- export async function streamHash(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<string>
426
- export async function streamHash(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Promise<Buffer | string> {
427
- const hash = cr.createHash(algorithm_, options_)
428
- for await (const chunk of f_.itBuff())
429
- hash.update(chunk)
430
- return encoding_ ? hash.digest(encoding_) : hash.digest()
431
- }
432
- export function streamHashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions): Buffer
433
- export function streamHashSync(f_: File, algorithm_?: string, options_?: cr.HashOptions, encoding_?: BufferEncoding): string
434
- export function streamHashSync(f_: File, algorithm_ = "sha256", options_?: cr.HashOptions, encoding_?: BufferEncoding): Buffer | string {
435
- const hash = cr.createHash(algorithm_, options_)
436
- for (const chunk of f_.itBuffSync())
437
- hash.update(chunk)
438
- return encoding_ ? hash.digest(encoding_) : hash.digest()
439
- }
440
-
441
- export async function* itLines(f_: File, options_: fs.ReadStreamOptions = { encoding: 'utf-8' }) {
442
- const readStream = fs.createReadStream(f_.isAt, options_)
443
- const rlInterface = rl.createInterface({ input: readStream, crlfDelay: Infinity })
444
- try {
445
- for await (const line of rlInterface)
446
- yield line
447
- } finally {
448
- rlInterface.close()
449
- readStream.destroy()
450
- }
451
- }
452
-
453
- export async function fileSameAs(f1_: File, f2_: File): Promise<boolean> {
454
- if (f1_.isAt === f2_.isAt)
455
- return true
456
- else if ((await fp.lstat(f1_.isAt)).size !== (await fp.lstat(f2_.isAt)).size)
457
- return false
458
- const iter1 = f1_.itBuff()
459
- const iter2 = f2_.itBuff()
460
- while (true) {
461
- const [a, b] = await Promise.all([iter1.next(), iter2.next()])
462
- if (a.done && b.done) return true
463
- if (a.done !== b.done) return false
464
- if (!a.value!.equals(b.value!)) return false
465
- }
466
- }
467
- export function fileSameAsSync(f1_: File, f2_: File): boolean {
468
- if (f1_.isAt === f2_.isAt)
469
- return true
470
- else if (fs.statSync(f1_.isAt).size !== fs.statSync(f2_.isAt).size)
471
- return false
472
- const iter1 = f1_.itBuffSync()
473
- const iter2 = f2_.itBuffSync()
474
- while (true) {
475
- const a = iter1.next()
476
- const b = iter2.next()
477
- if (a.done && b.done) return true
478
- if (a.done !== b.done) return false
479
- if (!a.value!.equals(b.value!)) return false
480
- }
481
- }
482
-
483
532
 
533
+ /** Helper type for filtering {@link Road} instances. */
534
+ export type filter_t<T extends Road> = ((road: Road) => road is T)
484
535
 
485
536
  export class Folder extends Road {
486
537
  static async create(at_: string) {
@@ -488,61 +539,88 @@ export class Folder extends Road {
488
539
  catch { await fp.mkdir(at_, { recursive: true }) }
489
540
  return new Folder(at_, false)
490
541
  }
542
+ static readonly mk: typeof Folder.create = Folder.create
491
543
  static createSync(at_: string) {
492
544
  try { fs.accessSync(at_, fsc.W_OK) }
493
545
  catch { fs.mkdirSync(at_, { recursive: true }) }
494
546
  return new Folder(at_, false)
495
547
  }
548
+ static readonly mkSync: typeof Folder.createSync = Folder.createSync
496
549
 
497
550
  join(...paths_: string[]) {
498
551
  return ph.join(this.isAt, ...paths_)
499
552
  }
500
553
 
501
554
  it(): AsyncIterable<Road>
502
- it<T extends Road>(expectedType_: new (..._: any[]) => T): AsyncIterable<T>
503
- async *it<T extends Road>(expectedType_?: new (..._: any[]) => T): AsyncIterable<Road> | AsyncIterable<T> {
504
- for (const entryName of await fp.readdir(this.isAt)) {
505
- const road = await factory(this.join(entryName))
506
- if (!expectedType_ || road instanceof expectedType_)
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))
507
560
  yield road
508
561
  }
509
562
  }
510
563
  itSync(): Iterable<Road>
511
- itSync<T extends Road>(expectedType_: new (..._: any[]) => T): Iterable<T>
512
- *itSync<T extends Road>(expectedType_?: new (..._: any[]) => T): Iterable<Road> | Iterable<T> {
513
- for (const entry of fs.readdirSync(this.isAt)) {
514
- const road = factorySync(this.join(entry))
515
- if (!expectedType_ || road instanceof expectedType_)
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))
516
569
  yield road
517
570
  }
518
571
  }
572
+
519
573
  async list(): Promise<Road[]>
520
- async list<T extends Road>(expectedType_: new (..._: any[]) => T): Promise<T[]>
521
- async list<T extends Road>(expectedType_?: new (..._: any[]) => T): Promise<Road[] | T[]> {
522
- 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))
523
577
  const resolvedEntries = await Promise.all(entries)
524
- if (!expectedType_)
578
+ if (!filter_)
525
579
  return resolvedEntries
526
- return resolvedEntries.filter(entry => entry instanceof expectedType_) as unknown as T[]
580
+ return resolvedEntries.filter(entry => filter_(entry))
527
581
  }
528
582
  listSync(): Road[]
529
- listSync<T extends Road>(expectedType_: new (..._: any[]) => T): T[]
530
- listSync<T extends Road>(expectedType_?: new (..._: any[]) => T): Road[] | T[] {
531
- const entries = fs.readdirSync(this.isAt).map(entry => factorySync(this.join(entry)))
532
- if (!expectedType_)
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_)
533
587
  return entries
534
- 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
+ }
535
614
  }
536
615
 
537
616
  async find(name_: string): Promise<Road | null>
538
- async find<T extends Road>(name_: string, expectedType_: new (..._: any[]) => T): Promise<T | null>
539
- async find<T extends Road>(name_: string, expectedType_?: new (..._: any[]) => T): Promise<Road | T | null> {
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> {
540
619
  try {
541
- await fp.access(this.join(name_), fs.constants.F_OK)
542
620
  const found = await factory(this.join(name_))
543
- if (!expectedType_)
621
+ if (!expect_)
544
622
  return found
545
- if (found instanceof expectedType_)
623
+ if (found instanceof expect_)
546
624
  return found as T
547
625
  return null
548
626
  } catch {
@@ -550,16 +628,16 @@ export class Folder extends Road {
550
628
  }
551
629
  }
552
630
  findSync(name_: string): Road | null
553
- findSync<T extends Road>(name_: string, expectedType_: new (..._: any[]) => T): T | null
554
- findSync<T extends Road>(name_: string, expectedType_?: new (..._: any[]) => T): Road | T | null {
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 {
555
633
  try {
556
634
  const found = factorySync(this.join(name_))
557
- if (!expectedType_)
635
+ if (!expect_)
558
636
  return found
559
- if (found instanceof expectedType_)
637
+ if (found instanceof expect_)
560
638
  return found as T
561
639
  return null
562
- } catch {
640
+ } catch(e: unknown) {
563
641
  return null
564
642
  }
565
643
  }
@@ -575,57 +653,25 @@ export class Folder extends Road {
575
653
  return factorySync(newPath) as unknown as T
576
654
  }
577
655
 
578
- async delete(options_: fs.RmOptions = { recursive: true }) {
579
- using _ = await this.lock()
580
- await fp.rm(this.isAt, options_)
581
- }
582
- deleteSync(options_: fs.RmOptions = { recursive: true }) {
583
- using _ = this.lockSync()
584
- fs.rmSync(this.isAt, options_)
585
- }
586
- async move(into_: Folder) {
587
- using _ = await this.lock()
588
- const newPath = into_.join(this.name)
589
- await fp.rename(this.isAt, newPath)
590
- this.pointsTo = newPath
591
- }
592
- moveSync(into_: Folder) {
593
- using _ = this.lockSync()
594
- const newPath = into_.join(this.name)
595
- fs.renameSync(this.isAt, newPath)
596
- this.pointsTo = newPath
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
597
661
  }
598
- async copy(into_: Folder): Promise<this> {
599
- const newPath = into_.join(this.name)
600
- await fp.cp(this.isAt, newPath, { recursive: true })
601
- return new Folder(newPath, false) as this
602
- }
603
- copySync(into_: Folder): this {
604
- const newPath = into_.join(this.name)
605
- fs.cpSync(this.isAt, newPath, { recursive: true })
606
- return new Folder(newPath, false) as this
607
- }
608
- async rename(to_: string) {
609
- using _ = await this.lock()
610
- const newPath = this.parent().join(to_)
611
- await fp.rename(this.isAt, newPath)
612
- this.pointsTo = newPath
613
- }
614
- renameSync(to_: string) {
615
- using _ = this.lockSync()
616
- const newPath = this.parent().join(to_)
617
- fs.renameSync(this.isAt, newPath)
618
- this.pointsTo = newPath
662
+ sizeSync(): number {
663
+ let size = 0
664
+ for (const entry of this.itSync())
665
+ size += entry.sizeSync()
666
+ return size
619
667
  }
620
668
 
621
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isDirectory() }
622
- 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 } }
623
671
 
624
672
  override isFolder(): this is Folder { return true as const }
625
673
  override isDir(): this is Folder { return true as const }
626
674
  override isDirectory(): this is Folder { return true as const }
627
- override isDict(): this is Folder { return true as const }
628
- override isDictionary(): this is Folder { return true as const }
629
675
  }
630
676
 
631
677
 
@@ -643,11 +689,13 @@ export class SymbolicLink extends Road {
643
689
  catch { await fp.symlink(target_.toString(), at_) }
644
690
  return new SymbolicLink(at_, false)
645
691
  }
692
+ static readonly mk: typeof SymbolicLink.create = SymbolicLink.create
646
693
  static createSync(at_: string, target_: string | Road) {
647
694
  try { fs.accessSync(at_, fs.constants.F_OK) }
648
695
  catch { fs.symlinkSync(target_.toString(), at_) }
649
696
  return new SymbolicLink(at_, false)
650
697
  }
698
+ static readonly mkSync: typeof SymbolicLink.createSync = SymbolicLink.createSync
651
699
 
652
700
  async target() {
653
701
  return factory(ph.resolve(ph.dirname(this.isAt), await fp.readlink(this.isAt)))
@@ -656,112 +704,106 @@ export class SymbolicLink extends Road {
656
704
  return factorySync(ph.resolve(ph.dirname(this.isAt), fs.readlinkSync(this.isAt)))
657
705
  }
658
706
  async retarget(to_: Road) {
659
- await this.delete()
660
- 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)
661
710
  }
662
711
  retargetSync(to_: Road) {
663
- this.deleteSync()
712
+ using _ = this.lockSync()
713
+ fs.unlinkSync(this.isAt)
664
714
  fs.symlinkSync(to_.isAt, this.isAt)
665
715
  }
666
716
 
667
- async delete() {
717
+ async size(): Promise<number> { return (await this.lstat()).size }
718
+ sizeSync(): number { return this.lstatSync().size }
719
+
720
+ override async delete() {
668
721
  using _ = await this.lock()
669
722
  await fp.unlink(this.isAt)
670
723
  }
671
- deleteSync() {
724
+ override deleteSync() {
672
725
  using _ = this.lockSync()
673
726
  fs.unlinkSync(this.isAt)
674
727
  }
675
- async move(into_: Folder) {
676
- using _ = await this.lock()
677
- const newPath = into_.join(this.name)
678
- await fp.rename(this.isAt, newPath)
679
- this.pointsTo = newPath
680
- }
681
- moveSync(into_: Folder) {
682
- using _ = this.lockSync()
683
- const newPath = into_.join(this.name)
684
- fs.renameSync(this.isAt, newPath)
685
- this.pointsTo = newPath
686
- }
687
- async copy(into_: Folder): Promise<this> {
688
- const newPath = into_.join(this.name)
689
- const target = await this.target()
690
- await fp.symlink(target.isAt, newPath)
691
- return new SymbolicLink(newPath, false) as this
692
- }
693
- copySync(into_: Folder): this {
694
- const newPath = into_.join(this.name)
695
- const target = this.targetSync()
696
- fs.symlinkSync(target.isAt, newPath)
697
- return new SymbolicLink(newPath, false) as this
698
- }
699
- async rename(to_: string) {
700
- using _ = await this.lock()
701
- const newPath = this.parent().join(to_)
702
- await fp.rename(this.isAt, newPath)
703
- this.pointsTo = newPath
704
- }
705
- renameSync(to_: string) {
706
- using _ = this.lockSync()
707
- const newPath = this.parent().join(to_)
708
- fs.renameSync(this.isAt, newPath)
709
- this.pointsTo = newPath
710
- }
711
728
 
712
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isSymbolicLink() }
713
- 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 } }
714
731
 
715
732
  override isSymlink(): this is SymbolicLink { return true as const }
716
733
  override isSymbolicLink(): this is SymbolicLink { return true as const }
717
734
  }
718
735
  export { SymbolicLink as Symlink }
736
+ here().walk(r => !r.isSymlink())
719
737
 
720
738
 
721
739
 
722
740
  export abstract class UnusableRoad extends Road {
723
741
  override readonly mutable: boolean = false // Modification will cause system issues (e.g. deleting a device file)
724
- constructor(...args_: ConstructorParameters<typeof Road>) {
725
- super(...args_)
726
- Object.freeze(this)
727
- }
728
- error(): never { throw new RdErr(`${this.constructor.name} at '${this.isAt}' is a system-level resource thus not subject to modification.`) }
729
- override async lock(): Promise<never> { return this.error() }
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 */
730
748
  override lockSync(): never { return this.error() }
731
- 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 */
732
752
  override deleteSync(): never { return this.error() }
733
- 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 */
734
756
  override moveSync(): never { return this.error() }
735
- 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 */
736
760
  override copySync(): never { return this.error() }
737
- 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 */
738
764
  override renameSync(): never { return this.error() }
739
-
765
+
740
766
  override isUnusable(): this is UnusableRoad { return true as const }
741
767
  }
742
768
  export class BlockDevice extends UnusableRoad {
743
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isBlockDevice() }
744
- checkSync(): boolean { return fs.lstatSync(this.isAt).isBlockDevice() }
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 } }
745
771
  override isBlockDevice(): this is BlockDevice { return true as const }
746
772
  }
747
773
  export class CharacterDevice extends UnusableRoad {
748
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isCharacterDevice() }
749
- checkSync(): boolean { return fs.lstatSync(this.isAt).isCharacterDevice() }
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 } }
750
776
  override isCharacterDevice(): this is CharacterDevice { return true as const }
751
777
  }
752
778
  export class Fifo extends UnusableRoad {
753
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isFIFO() }
754
- checkSync(): boolean { return fs.lstatSync(this.isAt).isFIFO() }
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 } }
755
781
  override isFifo(): this is Fifo { return true as const }
756
782
  }
757
783
  export class Socket extends UnusableRoad {
758
- async check(): Promise<boolean> { return (await fp.lstat(this.isAt)).isSocket() }
759
- checkSync(): boolean { return fs.lstatSync(this.isAt).isSocket() }
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 } }
760
786
  override isSocket(): this is Socket { return true as const }
761
787
  }
762
788
 
763
789
 
764
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
+
765
807
  let finalizer: FinalizationRegistry<string> | null = null
766
808
  let toDelete: Set<string> | null = null
767
809
  let exitHandlerRegistered: boolean | null = null
@@ -790,12 +832,20 @@ export function registerToCleanup(self_: Road) {
790
832
  }
791
833
 
792
834
 
793
- export function Temp<T extends Road>(createable_: { createSync: (at: string) => T }, autoCleanup_: boolean): T & Disposable & AsyncDisposable {
794
- let t = createable_.createSync(tmp().join(`instrumentality@${cr.randomUUID()}`))
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()}`))
795
845
  if (autoCleanup_)
796
846
  registerToCleanup(t)
797
847
  return Object.freeze(Object.assign(t, {
798
848
  [Symbol.dispose]() { try { t.deleteSync() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) },
799
849
  async [Symbol.asyncDispose]() { try { await t.delete() } catch {} toDelete?.delete(t.isAt); finalizer?.unregister(t) }
800
850
  }))
801
- }11
851
+ }