effect-start 0.9.0 → 0.11.0

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.
Files changed (54) hide show
  1. package/package.json +15 -14
  2. package/src/BundleHttp.test.ts +1 -1
  3. package/src/Commander.test.ts +15 -15
  4. package/src/Commander.ts +58 -88
  5. package/src/EncryptedCookies.test.ts +4 -4
  6. package/src/FileHttpRouter.test.ts +85 -16
  7. package/src/FileHttpRouter.ts +119 -32
  8. package/src/FileRouter.ts +62 -166
  9. package/src/FileRouterCodegen.test.ts +252 -66
  10. package/src/FileRouterCodegen.ts +13 -56
  11. package/src/FileRouterPattern.test.ts +116 -0
  12. package/src/FileRouterPattern.ts +59 -0
  13. package/src/FileRouter_path.test.ts +63 -102
  14. package/src/FileSystemExtra.test.ts +226 -0
  15. package/src/FileSystemExtra.ts +24 -60
  16. package/src/HttpAppExtra.test.ts +84 -0
  17. package/src/HttpAppExtra.ts +399 -47
  18. package/src/HttpUtils.test.ts +68 -0
  19. package/src/HttpUtils.ts +15 -0
  20. package/src/HyperHtml.ts +24 -5
  21. package/src/JsModule.test.ts +1 -1
  22. package/src/NodeFileSystem.ts +764 -0
  23. package/src/Random.ts +59 -0
  24. package/src/Route.test.ts +515 -18
  25. package/src/Route.ts +321 -166
  26. package/src/RouteRender.ts +40 -0
  27. package/src/Router.test.ts +416 -0
  28. package/src/Router.ts +288 -31
  29. package/src/RouterPattern.test.ts +655 -0
  30. package/src/RouterPattern.ts +416 -0
  31. package/src/Start.ts +14 -52
  32. package/src/TestHttpClient.test.ts +29 -0
  33. package/src/TestHttpClient.ts +122 -73
  34. package/src/assets.d.ts +39 -0
  35. package/src/bun/BunBundle.test.ts +0 -3
  36. package/src/bun/BunHttpServer.test.ts +74 -0
  37. package/src/bun/BunHttpServer.ts +259 -0
  38. package/src/bun/BunHttpServer_web.ts +384 -0
  39. package/src/bun/BunRoute.test.ts +514 -0
  40. package/src/bun/BunRoute.ts +427 -0
  41. package/src/bun/BunRoute_bundles.test.ts +218 -0
  42. package/src/bun/BunRuntime.ts +33 -0
  43. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  44. package/src/bun/_empty.html +1 -0
  45. package/src/bun/index.ts +2 -1
  46. package/src/index.ts +14 -14
  47. package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
  48. package/src/middlewares/BasicAuthMiddleware.ts +36 -0
  49. package/src/testing.ts +12 -3
  50. package/src/Datastar.test.ts +0 -267
  51. package/src/Datastar.ts +0 -68
  52. package/src/bun/BunFullstackServer.ts +0 -45
  53. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  54. package/src/jsx-datastar.d.ts +0 -63
@@ -0,0 +1,764 @@
1
+ import { effectify } from "@effect/platform/Effectify"
2
+ import * as Error from "@effect/platform/Error"
3
+ import type {
4
+ PlatformError,
5
+ SystemErrorReason,
6
+ } from "@effect/platform/Error"
7
+ import { SystemError } from "@effect/platform/Error"
8
+ import * as FileSystem from "@effect/platform/FileSystem"
9
+ import type * as Context from "effect/Context"
10
+ import * as Effect from "effect/Effect"
11
+ import { pipe } from "effect/Function"
12
+ import * as Layer from "effect/Layer"
13
+ import * as Option from "effect/Option"
14
+ import * as Stream from "effect/Stream"
15
+ import * as Crypto from "node:crypto"
16
+ import * as NFS from "node:fs"
17
+ import * as NOS from "node:os"
18
+ import * as NPath from "node:path"
19
+
20
+ const handleBadArgument = (method: string) => (cause: unknown) =>
21
+ new Error.BadArgument({
22
+ module: "FileSystem",
23
+ method,
24
+ cause,
25
+ })
26
+
27
+ // == access
28
+
29
+ const access = (() => {
30
+ const nodeAccess = effectify(
31
+ NFS.access,
32
+ handleErrnoException("FileSystem", "access"),
33
+ handleBadArgument("access"),
34
+ )
35
+ return (path: string, options?: FileSystem.AccessFileOptions) => {
36
+ let mode = NFS.constants.F_OK
37
+ if (options?.readable) {
38
+ mode |= NFS.constants.R_OK
39
+ }
40
+ if (options?.writable) {
41
+ mode |= NFS.constants.W_OK
42
+ }
43
+ return nodeAccess(path, mode)
44
+ }
45
+ })()
46
+
47
+ // == copy
48
+
49
+ const copy = (() => {
50
+ const nodeCp = effectify(
51
+ NFS.cp,
52
+ handleErrnoException("FileSystem", "copy"),
53
+ handleBadArgument("copy"),
54
+ )
55
+ return (fromPath: string, toPath: string, options?: FileSystem.CopyOptions) =>
56
+ nodeCp(fromPath, toPath, {
57
+ force: options?.overwrite ?? false,
58
+ preserveTimestamps: options?.preserveTimestamps ?? false,
59
+ recursive: true,
60
+ })
61
+ })()
62
+
63
+ // == copyFile
64
+
65
+ const copyFile = (() => {
66
+ const nodeCopyFile = effectify(
67
+ NFS.copyFile,
68
+ handleErrnoException("FileSystem", "copyFile"),
69
+ handleBadArgument("copyFile"),
70
+ )
71
+ return (fromPath: string, toPath: string) => nodeCopyFile(fromPath, toPath)
72
+ })()
73
+
74
+ // == chmod
75
+
76
+ const chmod = (() => {
77
+ const nodeChmod = effectify(
78
+ NFS.chmod,
79
+ handleErrnoException("FileSystem", "chmod"),
80
+ handleBadArgument("chmod"),
81
+ )
82
+ return (path: string, mode: number) => nodeChmod(path, mode)
83
+ })()
84
+
85
+ // == chown
86
+
87
+ const chown = (() => {
88
+ const nodeChown = effectify(
89
+ NFS.chown,
90
+ handleErrnoException("FileSystem", "chown"),
91
+ handleBadArgument("chown"),
92
+ )
93
+ return (path: string, uid: number, gid: number) => nodeChown(path, uid, gid)
94
+ })()
95
+
96
+ // == link
97
+
98
+ const link = (() => {
99
+ const nodeLink = effectify(
100
+ NFS.link,
101
+ handleErrnoException("FileSystem", "link"),
102
+ handleBadArgument("link"),
103
+ )
104
+ return (existingPath: string, newPath: string) =>
105
+ nodeLink(existingPath, newPath)
106
+ })()
107
+
108
+ // == makeDirectory
109
+
110
+ const makeDirectory = (() => {
111
+ const nodeMkdir = effectify(
112
+ NFS.mkdir,
113
+ handleErrnoException("FileSystem", "makeDirectory"),
114
+ handleBadArgument("makeDirectory"),
115
+ )
116
+ return (path: string, options?: FileSystem.MakeDirectoryOptions) =>
117
+ nodeMkdir(path, {
118
+ recursive: options?.recursive ?? false,
119
+ mode: options?.mode,
120
+ })
121
+ })()
122
+
123
+ // == makeTempDirectory
124
+
125
+ const makeTempDirectoryFactory = (method: string) => {
126
+ const nodeMkdtemp = effectify(
127
+ NFS.mkdtemp,
128
+ handleErrnoException("FileSystem", method),
129
+ handleBadArgument(method),
130
+ )
131
+ return (options?: FileSystem.MakeTempDirectoryOptions) =>
132
+ Effect.suspend(() => {
133
+ const prefix = options?.prefix ?? ""
134
+ const directory = typeof options?.directory === "string"
135
+ ? NPath.join(options.directory, ".")
136
+ : NOS.tmpdir()
137
+
138
+ return nodeMkdtemp(
139
+ prefix ? NPath.join(directory, prefix) : directory + "/",
140
+ )
141
+ })
142
+ }
143
+ const makeTempDirectory = makeTempDirectoryFactory("makeTempDirectory")
144
+
145
+ // == remove
146
+
147
+ const removeFactory = (method: string) => {
148
+ const nodeRm = effectify(
149
+ NFS.rm,
150
+ handleErrnoException("FileSystem", method),
151
+ handleBadArgument(method),
152
+ )
153
+ return (path: string, options?: FileSystem.RemoveOptions) =>
154
+ nodeRm(
155
+ path,
156
+ {
157
+ recursive: options?.recursive ?? false,
158
+ force: options?.force ?? false,
159
+ },
160
+ )
161
+ }
162
+ const remove = removeFactory("remove")
163
+
164
+ // == makeTempDirectoryScoped
165
+
166
+ const makeTempDirectoryScoped = (() => {
167
+ const makeDirectory = makeTempDirectoryFactory("makeTempDirectoryScoped")
168
+ const removeDirectory = removeFactory("makeTempDirectoryScoped")
169
+ return (
170
+ options?: FileSystem.MakeTempDirectoryOptions,
171
+ ) =>
172
+ Effect.acquireRelease(
173
+ makeDirectory(options),
174
+ (directory) =>
175
+ Effect.orDie(removeDirectory(directory, { recursive: true })),
176
+ )
177
+ })()
178
+
179
+ // == open
180
+
181
+ const openFactory = (method: string) => {
182
+ const nodeOpen = effectify(
183
+ NFS.open,
184
+ handleErrnoException("FileSystem", method),
185
+ handleBadArgument(method),
186
+ )
187
+ const nodeClose = effectify(
188
+ NFS.close,
189
+ handleErrnoException("FileSystem", method),
190
+ handleBadArgument(method),
191
+ )
192
+
193
+ return (path: string, options?: FileSystem.OpenFileOptions) =>
194
+ pipe(
195
+ Effect.acquireRelease(
196
+ nodeOpen(path, options?.flag ?? "r", options?.mode),
197
+ (fd) => Effect.orDie(nodeClose(fd)),
198
+ ),
199
+ Effect.map((fd) =>
200
+ makeFile(
201
+ FileSystem.FileDescriptor(fd),
202
+ options?.flag?.startsWith("a") ?? false,
203
+ )
204
+ ),
205
+ )
206
+ }
207
+ const open = openFactory("open")
208
+
209
+ const makeFile = (() => {
210
+ const nodeReadFactory = (method: string) =>
211
+ effectify(
212
+ NFS.read,
213
+ handleErrnoException("FileSystem", method),
214
+ handleBadArgument(method),
215
+ )
216
+ const nodeRead = nodeReadFactory("read")
217
+ const nodeReadAlloc = nodeReadFactory("readAlloc")
218
+ const nodeStat = effectify(
219
+ NFS.fstat,
220
+ handleErrnoException("FileSystem", "stat"),
221
+ handleBadArgument("stat"),
222
+ )
223
+ const nodeTruncate = effectify(
224
+ NFS.ftruncate,
225
+ handleErrnoException("FileSystem", "truncate"),
226
+ handleBadArgument("truncate"),
227
+ )
228
+
229
+ const nodeSync = effectify(
230
+ NFS.fsync,
231
+ handleErrnoException("FileSystem", "sync"),
232
+ handleBadArgument("sync"),
233
+ )
234
+
235
+ const nodeWriteFactory = (method: string) =>
236
+ effectify(
237
+ NFS.write,
238
+ handleErrnoException("FileSystem", method),
239
+ handleBadArgument(method),
240
+ )
241
+ const nodeWrite = nodeWriteFactory("write")
242
+ const nodeWriteAll = nodeWriteFactory("writeAll")
243
+
244
+ class FileImpl implements FileSystem.File {
245
+ readonly [FileSystem.FileTypeId]: FileSystem.FileTypeId
246
+
247
+ private readonly semaphore = Effect.unsafeMakeSemaphore(1)
248
+ private position: bigint = 0n
249
+
250
+ constructor(
251
+ readonly fd: FileSystem.File.Descriptor,
252
+ private readonly append: boolean,
253
+ ) {
254
+ this[FileSystem.FileTypeId] = FileSystem.FileTypeId
255
+ }
256
+
257
+ get stat() {
258
+ return Effect.map(nodeStat(this.fd), makeFileInfo)
259
+ }
260
+
261
+ get sync() {
262
+ return nodeSync(this.fd)
263
+ }
264
+
265
+ seek(offset: FileSystem.SizeInput, from: FileSystem.SeekMode) {
266
+ const offsetSize = FileSystem.Size(offset)
267
+ return this.semaphore.withPermits(1)(
268
+ Effect.sync(() => {
269
+ if (from === "start") {
270
+ this.position = offsetSize
271
+ } else if (from === "current") {
272
+ this.position = this.position + offsetSize
273
+ }
274
+
275
+ return this.position
276
+ }),
277
+ )
278
+ }
279
+
280
+ read(buffer: Uint8Array) {
281
+ return this.semaphore.withPermits(1)(
282
+ Effect.map(
283
+ Effect.suspend(() =>
284
+ nodeRead(this.fd, {
285
+ buffer,
286
+ position: this.position,
287
+ })
288
+ ),
289
+ (bytesRead) => {
290
+ const sizeRead = FileSystem.Size(bytesRead)
291
+ this.position = this.position + sizeRead
292
+ return sizeRead
293
+ },
294
+ ),
295
+ )
296
+ }
297
+
298
+ readAlloc(size: FileSystem.SizeInput) {
299
+ const sizeNumber = Number(size)
300
+ return this.semaphore.withPermits(1)(Effect.flatMap(
301
+ Effect.sync(() => Buffer.allocUnsafeSlow(sizeNumber)),
302
+ (buffer) =>
303
+ Effect.map(
304
+ nodeReadAlloc(this.fd, {
305
+ buffer,
306
+ position: this.position,
307
+ }),
308
+ (bytesRead): Option.Option<Buffer> => {
309
+ if (bytesRead === 0) {
310
+ return Option.none()
311
+ }
312
+
313
+ this.position = this.position + BigInt(bytesRead)
314
+ if (bytesRead === sizeNumber) {
315
+ return Option.some(buffer)
316
+ }
317
+
318
+ const dst = Buffer.allocUnsafeSlow(bytesRead)
319
+ buffer.copy(dst, 0, 0, bytesRead)
320
+ return Option.some(dst)
321
+ },
322
+ ),
323
+ ))
324
+ }
325
+
326
+ truncate(length?: FileSystem.SizeInput) {
327
+ return this.semaphore.withPermits(1)(
328
+ Effect.map(
329
+ nodeTruncate(this.fd, length ? Number(length) : undefined),
330
+ () => {
331
+ if (!this.append) {
332
+ const len = BigInt(length ?? 0)
333
+ if (this.position > len) {
334
+ this.position = len
335
+ }
336
+ }
337
+ },
338
+ ),
339
+ )
340
+ }
341
+
342
+ write(buffer: Uint8Array) {
343
+ return this.semaphore.withPermits(1)(
344
+ Effect.map(
345
+ Effect.suspend(() =>
346
+ nodeWrite(
347
+ this.fd,
348
+ buffer,
349
+ undefined,
350
+ undefined,
351
+ this.append ? undefined : Number(this.position),
352
+ )
353
+ ),
354
+ (bytesWritten) => {
355
+ const sizeWritten = FileSystem.Size(bytesWritten)
356
+ if (!this.append) {
357
+ this.position = this.position + sizeWritten
358
+ }
359
+
360
+ return sizeWritten
361
+ },
362
+ ),
363
+ )
364
+ }
365
+
366
+ private writeAllChunk(
367
+ buffer: Uint8Array,
368
+ ): Effect.Effect<void, Error.PlatformError> {
369
+ return Effect.flatMap(
370
+ Effect.suspend(() =>
371
+ nodeWriteAll(
372
+ this.fd,
373
+ buffer,
374
+ undefined,
375
+ undefined,
376
+ this.append ? undefined : Number(this.position),
377
+ )
378
+ ),
379
+ (bytesWritten) => {
380
+ if (bytesWritten === 0) {
381
+ return Effect.fail(
382
+ new Error.SystemError({
383
+ module: "FileSystem",
384
+ method: "writeAll",
385
+ reason: "WriteZero",
386
+ pathOrDescriptor: this.fd,
387
+ description: "write returned 0 bytes written",
388
+ }),
389
+ )
390
+ }
391
+
392
+ if (!this.append) {
393
+ this.position = this.position + BigInt(bytesWritten)
394
+ }
395
+
396
+ return bytesWritten < buffer.length
397
+ ? this.writeAllChunk(buffer.subarray(bytesWritten))
398
+ : Effect.void
399
+ },
400
+ )
401
+ }
402
+
403
+ writeAll(buffer: Uint8Array) {
404
+ return this.semaphore.withPermits(1)(this.writeAllChunk(buffer))
405
+ }
406
+ }
407
+
408
+ return (fd: FileSystem.File.Descriptor, append: boolean): FileSystem.File =>
409
+ new FileImpl(fd, append)
410
+ })()
411
+
412
+ // == makeTempFile
413
+
414
+ const makeTempFileFactory = (method: string) => {
415
+ const makeDirectory = makeTempDirectoryFactory(method)
416
+ const open = openFactory(method)
417
+ const randomHexString = (bytes: number) =>
418
+ Effect.sync(() => Crypto.randomBytes(bytes).toString("hex"))
419
+ return (options?: FileSystem.MakeTempFileOptions) =>
420
+ pipe(
421
+ Effect.zip(makeDirectory(options), randomHexString(6)),
422
+ Effect.map(([directory, random]) =>
423
+ NPath.join(directory, random + (options?.suffix ?? ""))
424
+ ),
425
+ Effect.tap((path) => Effect.scoped(open(path, { flag: "w+" }))),
426
+ )
427
+ }
428
+ const makeTempFile = makeTempFileFactory("makeTempFile")
429
+
430
+ // == makeTempFileScoped
431
+
432
+ const makeTempFileScoped = (() => {
433
+ const makeFile = makeTempFileFactory("makeTempFileScoped")
434
+ const removeDirectory = removeFactory("makeTempFileScoped")
435
+ return (options?: FileSystem.MakeTempFileOptions) =>
436
+ Effect.acquireRelease(
437
+ makeFile(options),
438
+ (file) =>
439
+ Effect.orDie(removeDirectory(NPath.dirname(file), { recursive: true })),
440
+ )
441
+ })()
442
+
443
+ // == readDirectory
444
+
445
+ const readDirectory = (
446
+ path: string,
447
+ options?: FileSystem.ReadDirectoryOptions,
448
+ ) =>
449
+ Effect.tryPromise({
450
+ try: () => NFS.promises.readdir(path, options),
451
+ catch: (err) =>
452
+ handleErrnoException("FileSystem", "readDirectory")(err as any, [path]),
453
+ })
454
+
455
+ // == readFile
456
+
457
+ const readFile = (path: string) =>
458
+ Effect.async<Uint8Array, Error.PlatformError>((resume, signal) => {
459
+ try {
460
+ NFS.readFile(path, { signal }, (err, data) => {
461
+ if (err) {
462
+ resume(
463
+ Effect.fail(
464
+ handleErrnoException("FileSystem", "readFile")(err, [path]),
465
+ ),
466
+ )
467
+ } else {
468
+ resume(Effect.succeed(data))
469
+ }
470
+ })
471
+ } catch (err) {
472
+ resume(Effect.fail(handleBadArgument("readFile")(err)))
473
+ }
474
+ })
475
+
476
+ // == readLink
477
+
478
+ const readLink = (() => {
479
+ const nodeReadLink = effectify(
480
+ NFS.readlink,
481
+ handleErrnoException("FileSystem", "readLink"),
482
+ handleBadArgument("readLink"),
483
+ )
484
+ return (path: string) => nodeReadLink(path)
485
+ })()
486
+
487
+ // == realPath
488
+
489
+ const realPath = (() => {
490
+ const nodeRealPath = effectify(
491
+ NFS.realpath,
492
+ handleErrnoException("FileSystem", "realPath"),
493
+ handleBadArgument("realPath"),
494
+ )
495
+ return (path: string) => nodeRealPath(path)
496
+ })()
497
+
498
+ // == rename
499
+
500
+ const rename = (() => {
501
+ const nodeRename = effectify(
502
+ NFS.rename,
503
+ handleErrnoException("FileSystem", "rename"),
504
+ handleBadArgument("rename"),
505
+ )
506
+ return (oldPath: string, newPath: string) => nodeRename(oldPath, newPath)
507
+ })()
508
+
509
+ // == stat
510
+
511
+ const makeFileInfo = (stat: NFS.Stats): FileSystem.File.Info => ({
512
+ type: stat.isFile()
513
+ ? "File"
514
+ : stat.isDirectory()
515
+ ? "Directory"
516
+ : stat.isSymbolicLink()
517
+ ? "SymbolicLink"
518
+ : stat.isBlockDevice()
519
+ ? "BlockDevice"
520
+ : stat.isCharacterDevice()
521
+ ? "CharacterDevice"
522
+ : stat.isFIFO()
523
+ ? "FIFO"
524
+ : stat.isSocket()
525
+ ? "Socket"
526
+ : "Unknown",
527
+ mtime: Option.fromNullable(stat.mtime),
528
+ atime: Option.fromNullable(stat.atime),
529
+ birthtime: Option.fromNullable(stat.birthtime),
530
+ dev: stat.dev,
531
+ rdev: Option.fromNullable(stat.rdev),
532
+ ino: Option.fromNullable(stat.ino),
533
+ mode: stat.mode,
534
+ nlink: Option.fromNullable(stat.nlink),
535
+ uid: Option.fromNullable(stat.uid),
536
+ gid: Option.fromNullable(stat.gid),
537
+ size: FileSystem.Size(stat.size),
538
+ blksize: Option.map(Option.fromNullable(stat.blksize), FileSystem.Size),
539
+ blocks: Option.fromNullable(stat.blocks),
540
+ })
541
+ const stat = (() => {
542
+ const nodeStat = effectify(
543
+ NFS.stat,
544
+ handleErrnoException("FileSystem", "stat"),
545
+ handleBadArgument("stat"),
546
+ )
547
+ return (path: string) => Effect.map(nodeStat(path), makeFileInfo)
548
+ })()
549
+
550
+ // == symlink
551
+
552
+ const symlink = (() => {
553
+ const nodeSymlink = effectify(
554
+ NFS.symlink,
555
+ handleErrnoException("FileSystem", "symlink"),
556
+ handleBadArgument("symlink"),
557
+ )
558
+ return (target: string, path: string) => nodeSymlink(target, path)
559
+ })()
560
+
561
+ // == truncate
562
+
563
+ const truncate = (() => {
564
+ const nodeTruncate = effectify(
565
+ NFS.truncate,
566
+ handleErrnoException("FileSystem", "truncate"),
567
+ handleBadArgument("truncate"),
568
+ )
569
+ return (path: string, length?: FileSystem.SizeInput) =>
570
+ nodeTruncate(path, length !== undefined ? Number(length) : undefined)
571
+ })()
572
+
573
+ // == utimes
574
+
575
+ const utimes = (() => {
576
+ const nodeUtimes = effectify(
577
+ NFS.utimes,
578
+ handleErrnoException("FileSystem", "utime"),
579
+ handleBadArgument("utime"),
580
+ )
581
+ return (path: string, atime: number | Date, mtime: number | Date) =>
582
+ nodeUtimes(path, atime, mtime)
583
+ })()
584
+
585
+ // == watch
586
+
587
+ const watchNode = (path: string, options?: FileSystem.WatchOptions) =>
588
+ Stream.asyncScoped<FileSystem.WatchEvent, Error.PlatformError>((emit) =>
589
+ Effect.acquireRelease(
590
+ Effect.sync(() => {
591
+ const watcher = NFS.watch(
592
+ path,
593
+ { recursive: options?.recursive },
594
+ (event, path) => {
595
+ if (!path) return
596
+ switch (event) {
597
+ case "rename": {
598
+ emit.fromEffect(Effect.matchEffect(stat(path), {
599
+ onSuccess: (_) =>
600
+ Effect.succeed(FileSystem.WatchEventCreate({ path })),
601
+ onFailure: (err) =>
602
+ err._tag === "SystemError" && err.reason === "NotFound"
603
+ ? Effect.succeed(FileSystem.WatchEventRemove({ path }))
604
+ : Effect.fail(err),
605
+ }))
606
+ return
607
+ }
608
+ case "change": {
609
+ emit.single(FileSystem.WatchEventUpdate({ path }))
610
+ return
611
+ }
612
+ }
613
+ },
614
+ )
615
+ watcher.on("error", (error) => {
616
+ emit.fail(
617
+ new Error.SystemError({
618
+ module: "FileSystem",
619
+ reason: "Unknown",
620
+ method: "watch",
621
+ pathOrDescriptor: path,
622
+ cause: error,
623
+ }),
624
+ )
625
+ })
626
+ watcher.on("close", () => {
627
+ emit.end()
628
+ })
629
+ return watcher
630
+ }),
631
+ (watcher) => Effect.sync(() => watcher.close()),
632
+ )
633
+ )
634
+
635
+ const watch = (
636
+ backend: Option.Option<Context.Tag.Service<FileSystem.WatchBackend>>,
637
+ path: string,
638
+ options?: FileSystem.WatchOptions,
639
+ ) =>
640
+ stat(path).pipe(
641
+ Effect.map((stat) =>
642
+ backend.pipe(
643
+ Option.flatMap((_) => _.register(path, stat, options)),
644
+ Option.getOrElse(() => watchNode(path, options)),
645
+ )
646
+ ),
647
+ Stream.unwrap,
648
+ )
649
+
650
+ // == writeFile
651
+
652
+ const writeFile = (
653
+ path: string,
654
+ data: Uint8Array,
655
+ options?: FileSystem.WriteFileOptions,
656
+ ) =>
657
+ Effect.async<void, Error.PlatformError>((resume, signal) => {
658
+ try {
659
+ NFS.writeFile(path, data, {
660
+ signal,
661
+ flag: options?.flag,
662
+ mode: options?.mode,
663
+ }, (err) => {
664
+ if (err) {
665
+ resume(
666
+ Effect.fail(
667
+ handleErrnoException("FileSystem", "writeFile")(err, [path]),
668
+ ),
669
+ )
670
+ } else {
671
+ resume(Effect.void)
672
+ }
673
+ })
674
+ } catch (err) {
675
+ resume(Effect.fail(handleBadArgument("writeFile")(err)))
676
+ }
677
+ })
678
+
679
+ const makeFileSystem = Effect.map(
680
+ Effect.serviceOption(FileSystem.WatchBackend),
681
+ (backend) =>
682
+ FileSystem.make({
683
+ access,
684
+ chmod,
685
+ chown,
686
+ copy,
687
+ copyFile,
688
+ link,
689
+ makeDirectory,
690
+ makeTempDirectory,
691
+ makeTempDirectoryScoped,
692
+ makeTempFile,
693
+ makeTempFileScoped,
694
+ open,
695
+ readDirectory,
696
+ readFile,
697
+ readLink,
698
+ realPath,
699
+ remove,
700
+ rename,
701
+ stat,
702
+ symlink,
703
+ truncate,
704
+ utimes,
705
+ watch(path, options) {
706
+ return watch(backend, path, options)
707
+ },
708
+ writeFile,
709
+ }),
710
+ )
711
+
712
+ export const layer = Layer.effect(FileSystem.FileSystem, makeFileSystem)
713
+
714
+ export function handleErrnoException(
715
+ module: SystemError["module"],
716
+ method: string,
717
+ ) {
718
+ return function(
719
+ err: NodeJS.ErrnoException,
720
+ [path]: [path: NFS.PathLike | number, ...args: Array<any>],
721
+ ): PlatformError {
722
+ let reason: SystemErrorReason = "Unknown"
723
+
724
+ switch (err.code) {
725
+ case "ENOENT":
726
+ reason = "NotFound"
727
+ break
728
+
729
+ case "EACCES":
730
+ reason = "PermissionDenied"
731
+ break
732
+
733
+ case "EEXIST":
734
+ reason = "AlreadyExists"
735
+ break
736
+
737
+ case "EISDIR":
738
+ reason = "BadResource"
739
+ break
740
+
741
+ case "ENOTDIR":
742
+ reason = "BadResource"
743
+ break
744
+
745
+ case "EBUSY":
746
+ reason = "Busy"
747
+ break
748
+
749
+ case "ELOOP":
750
+ reason = "BadResource"
751
+ break
752
+ }
753
+
754
+ return new SystemError({
755
+ reason,
756
+ module,
757
+ method,
758
+ pathOrDescriptor: path as string | number,
759
+ syscall: err.syscall,
760
+ description: err.message,
761
+ cause: err,
762
+ })
763
+ }
764
+ }