1408 lines
39 KiB
TypeScript
1408 lines
39 KiB
TypeScript
/**
|
|
* Defines the portable file system service for Effect programs.
|
|
*
|
|
* `FileSystem` is the boundary between Effect code and the host file system.
|
|
* Platform packages provide concrete layers, while this module defines the
|
|
* operations for reading, writing, inspecting, streaming, and watching files.
|
|
* Operations return `Effect`, `Stream`, or `Sink` values and fail with
|
|
* `PlatformError`. The module also includes file handles, size helpers, open
|
|
* flags, watch events, and the watch backend service.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
import * as Arr from "./Array.ts"
|
|
import * as Brand from "./Brand.ts"
|
|
import * as Cause from "./Cause.ts"
|
|
import * as Context from "./Context.ts"
|
|
import * as Effect from "./Effect.ts"
|
|
import { pipe } from "./Function.ts"
|
|
import * as Layer from "./Layer.ts"
|
|
import * as Option from "./Option.ts"
|
|
import { badArgument, type PlatformError, systemError } from "./PlatformError.ts"
|
|
import { hasProperty } from "./Predicate.ts"
|
|
import type * as Pull from "./Pull.ts"
|
|
import type { Scope } from "./Scope.ts"
|
|
import * as Sink from "./Sink.ts"
|
|
import * as Stream from "./Stream.ts"
|
|
|
|
const TypeId = "~effect/platform/FileSystem"
|
|
|
|
/**
|
|
* Core interface for file system operations in Effect.
|
|
*
|
|
* **Details**
|
|
*
|
|
* The FileSystem interface provides a comprehensive set of file and directory operations
|
|
* that work cross-platform. All operations return Effect values that can be composed,
|
|
* transformed, and executed safely with proper error handling.
|
|
*
|
|
* **Example** (Accessing file system operations)
|
|
*
|
|
* ```ts
|
|
* import { Console, Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // Basic file operations
|
|
* const exists = yield* fs.exists("./config.json")
|
|
* if (!exists) {
|
|
* yield* fs.writeFileString("./config.json", "{\"env\": \"development\"}")
|
|
* }
|
|
*
|
|
* // Directory operations
|
|
* yield* fs.makeDirectory("./logs", { recursive: true })
|
|
*
|
|
* // File information
|
|
* const stats = yield* fs.stat("./config.json")
|
|
* yield* Console.log(`File size: ${stats.size} bytes`)
|
|
*
|
|
* // Streaming operations
|
|
* const content = yield* fs.readFileString("./config.json")
|
|
* yield* Console.log("Config:", content)
|
|
* })
|
|
* ```
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface FileSystem {
|
|
readonly [TypeId]: typeof TypeId
|
|
|
|
/**
|
|
* Checks whether a file can be accessed.
|
|
* You can optionally specify the level of access to check for.
|
|
*/
|
|
readonly access: (
|
|
path: string,
|
|
options?: {
|
|
readonly ok?: boolean | undefined
|
|
readonly readable?: boolean | undefined
|
|
readonly writable?: boolean | undefined
|
|
}
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Copy a file or directory from `fromPath` to `toPath`.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Equivalent to `cp -r`.
|
|
*/
|
|
readonly copy: (
|
|
fromPath: string,
|
|
toPath: string,
|
|
options?: {
|
|
readonly overwrite?: boolean | undefined
|
|
readonly preserveTimestamps?: boolean | undefined
|
|
}
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Copy a file from `fromPath` to `toPath`.
|
|
*/
|
|
readonly copyFile: (
|
|
fromPath: string,
|
|
toPath: string
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Change the permissions of a file.
|
|
*/
|
|
readonly chmod: (
|
|
path: string,
|
|
mode: number
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Change the owner and group of a file.
|
|
*/
|
|
readonly chown: (
|
|
path: string,
|
|
uid: number,
|
|
gid: number
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Glob a directory.
|
|
*/
|
|
readonly glob: (
|
|
pattern: string,
|
|
options?: {
|
|
readonly root?: string | undefined
|
|
readonly exclude?: ReadonlyArray<string> | undefined
|
|
}
|
|
) => Effect.Effect<Array<string>, PlatformError>
|
|
/**
|
|
* Checks whether a path exists.
|
|
*/
|
|
readonly exists: (
|
|
path: string
|
|
) => Effect.Effect<boolean, PlatformError>
|
|
/**
|
|
* Create a hard link from `fromPath` to `toPath`.
|
|
*/
|
|
readonly link: (
|
|
fromPath: string,
|
|
toPath: string
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Create a directory at `path`. You can optionally specify the mode and
|
|
* whether to recursively create nested directories.
|
|
*/
|
|
readonly makeDirectory: (
|
|
path: string,
|
|
options?: {
|
|
readonly recursive?: boolean | undefined
|
|
readonly mode?: number | undefined
|
|
}
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Create a temporary directory.
|
|
*
|
|
* **Details**
|
|
*
|
|
* By default the directory will be created inside the system's default
|
|
* temporary directory, but you can specify a different location by setting
|
|
* the `directory` option.
|
|
*
|
|
* You can also specify a prefix for the directory name by setting the
|
|
* `prefix` option.
|
|
*/
|
|
readonly makeTempDirectory: (options?: {
|
|
readonly directory?: string | undefined
|
|
readonly prefix?: string | undefined
|
|
}) => Effect.Effect<string, PlatformError>
|
|
/**
|
|
* Create a temporary directory inside a scope.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Functionally equivalent to `makeTempDirectory`, but the directory will be
|
|
* automatically deleted when the scope is closed.
|
|
*/
|
|
readonly makeTempDirectoryScoped: (options?: {
|
|
readonly directory?: string | undefined
|
|
readonly prefix?: string | undefined
|
|
}) => Effect.Effect<string, PlatformError, Scope>
|
|
/**
|
|
* Create a temporary file.
|
|
* The directory creation is functionally equivalent to `makeTempDirectory`.
|
|
* The file name will be a randomly generated string.
|
|
*/
|
|
readonly makeTempFile: (options?: {
|
|
readonly directory?: string | undefined
|
|
readonly prefix?: string | undefined
|
|
readonly suffix?: string | undefined
|
|
}) => Effect.Effect<string, PlatformError>
|
|
/**
|
|
* Create a temporary file inside a scope.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Functionally equivalent to `makeTempFile`, but the file will be
|
|
* automatically deleted when the scope is closed.
|
|
*/
|
|
readonly makeTempFileScoped: (options?: {
|
|
readonly directory?: string | undefined
|
|
readonly prefix?: string | undefined
|
|
readonly suffix?: string | undefined
|
|
}) => Effect.Effect<string, PlatformError, Scope>
|
|
/**
|
|
* Open a file at `path` with the specified `options`.
|
|
*
|
|
* **Details**
|
|
*
|
|
* The file handle will be automatically closed when the scope is closed.
|
|
*/
|
|
readonly open: (
|
|
path: string,
|
|
options?: {
|
|
readonly flag?: OpenFlag | undefined
|
|
readonly mode?: number | undefined
|
|
}
|
|
) => Effect.Effect<File, PlatformError, Scope>
|
|
/**
|
|
* List the contents of a directory.
|
|
*
|
|
* **Details**
|
|
*
|
|
* You can recursively list the contents of nested directories by setting the
|
|
* `recursive` option.
|
|
*/
|
|
readonly readDirectory: (
|
|
path: string,
|
|
options?: {
|
|
readonly recursive?: boolean | undefined
|
|
}
|
|
) => Effect.Effect<Array<string>, PlatformError>
|
|
/**
|
|
* Read the contents of a file.
|
|
*/
|
|
readonly readFile: (
|
|
path: string
|
|
) => Effect.Effect<Uint8Array, PlatformError>
|
|
/**
|
|
* Read the contents of a file.
|
|
*/
|
|
readonly readFileString: (
|
|
path: string,
|
|
encoding?: string
|
|
) => Effect.Effect<string, PlatformError>
|
|
/**
|
|
* Read the destination of a symbolic link.
|
|
*/
|
|
readonly readLink: (
|
|
path: string
|
|
) => Effect.Effect<string, PlatformError>
|
|
/**
|
|
* Resolve a path to its canonicalized absolute pathname.
|
|
*/
|
|
readonly realPath: (
|
|
path: string
|
|
) => Effect.Effect<string, PlatformError>
|
|
/**
|
|
* Remove a file or directory.
|
|
*/
|
|
readonly remove: (
|
|
path: string,
|
|
options?: {
|
|
/**
|
|
* When `true`, you can recursively remove nested directories.
|
|
*/
|
|
readonly recursive?: boolean | undefined
|
|
/**
|
|
* When `true`, exceptions will be ignored if `path` does not exist.
|
|
*/
|
|
readonly force?: boolean | undefined
|
|
}
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Rename a file or directory.
|
|
*/
|
|
readonly rename: (
|
|
oldPath: string,
|
|
newPath: string
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Create a writable `Sink` for the specified `path`.
|
|
*/
|
|
readonly sink: (
|
|
path: string,
|
|
options?: {
|
|
readonly flag?: OpenFlag | undefined
|
|
readonly mode?: number | undefined
|
|
}
|
|
) => Sink.Sink<void, Uint8Array, never, PlatformError>
|
|
/**
|
|
* Get information about a file at `path`.
|
|
*/
|
|
readonly stat: (
|
|
path: string
|
|
) => Effect.Effect<File.Info, PlatformError>
|
|
/**
|
|
* Create a readable `Stream` for the specified `path`.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Changing the `bufferSize` option will change the internal buffer size of
|
|
* the stream. It defaults to `4`.
|
|
*
|
|
* The `chunkSize` option will change the size of the chunks emitted by the
|
|
* stream. It defaults to 64kb.
|
|
*
|
|
* Changing `offset` and `bytesToRead` will change the offset and the number
|
|
* of bytes to read from the file.
|
|
*/
|
|
readonly stream: (
|
|
path: string,
|
|
options?: {
|
|
readonly bytesToRead?: SizeInput | undefined
|
|
readonly chunkSize?: SizeInput | undefined
|
|
readonly offset?: SizeInput | undefined
|
|
}
|
|
) => Stream.Stream<Uint8Array, PlatformError>
|
|
/**
|
|
* Create a symbolic link from `fromPath` to `toPath`.
|
|
*/
|
|
readonly symlink: (
|
|
fromPath: string,
|
|
toPath: string
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Truncate a file to a specified length. If the `length` is not specified,
|
|
* the file will be truncated to length `0`.
|
|
*/
|
|
readonly truncate: (
|
|
path: string,
|
|
length?: SizeInput
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Change the file system timestamps of the file at `path`.
|
|
*/
|
|
readonly utimes: (
|
|
path: string,
|
|
atime: Date | number,
|
|
mtime: Date | number
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Watch a directory or file for changes
|
|
*/
|
|
readonly watch: (path: string) => Stream.Stream<WatchEvent, PlatformError>
|
|
/**
|
|
* Write data to a file at `path`.
|
|
*/
|
|
readonly writeFile: (
|
|
path: string,
|
|
data: Uint8Array,
|
|
options?: {
|
|
readonly flag?: OpenFlag | undefined
|
|
readonly mode?: number | undefined
|
|
}
|
|
) => Effect.Effect<void, PlatformError>
|
|
/**
|
|
* Write a string to a file at `path`.
|
|
*/
|
|
readonly writeFileString: (
|
|
path: string,
|
|
data: string,
|
|
options?: {
|
|
readonly flag?: OpenFlag | undefined
|
|
readonly mode?: number | undefined
|
|
}
|
|
) => Effect.Effect<void, PlatformError>
|
|
}
|
|
|
|
/**
|
|
* Represents a file size in bytes using a branded bigint.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This type ensures type safety when working with file sizes, preventing
|
|
* accidental mixing of regular numbers with size values. The underlying
|
|
* bigint allows for handling very large file sizes beyond JavaScript's
|
|
* number precision limits.
|
|
*
|
|
* **Example** (Creating branded file sizes)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* // Create sizes using the Size constructor
|
|
* const smallFile = FileSystem.Size(1024) // 1 KB
|
|
* const largeFile = FileSystem.Size(BigInt("9007199254740992")) // Very large
|
|
*
|
|
* // Use with file operations
|
|
* const truncateToSize = Effect.fnUntraced(function*(path: string, size: FileSystem.Size) {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
* return yield* fs.truncate(path, size)
|
|
* })
|
|
* ```
|
|
*
|
|
* @category sizes
|
|
* @since 4.0.0
|
|
*/
|
|
export type Size = Brand.Branded<bigint, "Size">
|
|
|
|
/**
|
|
* Input type for size parameters that accepts multiple numeric types.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This union type allows file system operations to accept size values in
|
|
* different formats for convenience, which are then normalized to the
|
|
* branded `Size` type internally.
|
|
*
|
|
* **Example** (Using size inputs)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // All of these are valid SizeInput values
|
|
* yield* fs.truncate("file1.txt", 1024) // number
|
|
* yield* fs.truncate("file2.txt", BigInt(2048)) // bigint
|
|
* yield* fs.truncate("file3.txt", FileSystem.Size(4096)) // Size
|
|
* })
|
|
* ```
|
|
*
|
|
* @category sizes
|
|
* @since 4.0.0
|
|
*/
|
|
export type SizeInput = bigint | number | Size
|
|
|
|
/**
|
|
* Creates a `Size` from various numeric input types.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Converts numbers, bigints, or existing Size values into a properly
|
|
* branded Size type. This function handles the conversion and ensures
|
|
* type safety for file size operations.
|
|
*
|
|
* **Example** (Converting size inputs)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* // From number
|
|
* const size1 = FileSystem.Size(1024)
|
|
* console.log(typeof size1) // "bigint"
|
|
*
|
|
* // From bigint
|
|
* const size2 = FileSystem.Size(BigInt(2048))
|
|
*
|
|
* // From existing Size (identity)
|
|
* const size3 = FileSystem.Size(size1)
|
|
*
|
|
* // Use in file operations
|
|
* const readChunk = (path: string, chunkSize: number) =>
|
|
* Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
* return fs.stream(path, {
|
|
* chunkSize: FileSystem.Size(chunkSize)
|
|
* })
|
|
* })
|
|
* ```
|
|
*
|
|
* @category sizes
|
|
* @since 4.0.0
|
|
*/
|
|
export const Size = (bytes: SizeInput): Size => typeof bytes === "bigint" ? bytes as Size : BigInt(bytes) as Size
|
|
|
|
/**
|
|
* Creates a `Size` representing kilobytes (1024 bytes).
|
|
*
|
|
* **Details**
|
|
*
|
|
* Converts a number of kilobytes to the equivalent size in bytes.
|
|
* Uses binary kilobytes (1024 bytes) rather than decimal (1000 bytes).
|
|
*
|
|
* **Example** (Creating kibibyte sizes)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // Create a 64 KiB buffer size for streaming
|
|
* const bufferSize = FileSystem.KiB(64)
|
|
*
|
|
* const stream = fs.stream("large-file.txt", {
|
|
* chunkSize: bufferSize
|
|
* })
|
|
*
|
|
* // Truncate file to 100 KiB
|
|
* yield* fs.truncate("data.txt", FileSystem.KiB(100))
|
|
* })
|
|
* ```
|
|
*
|
|
* @category sizes
|
|
* @since 4.0.0
|
|
*/
|
|
export const KiB = (n: number): Size => Size(n * 1024)
|
|
|
|
/**
|
|
* Creates a `Size` representing mebibytes (1024² bytes).
|
|
*
|
|
* **Details**
|
|
*
|
|
* Converts a number of mebibytes to the equivalent size in bytes.
|
|
* Uses binary mebibytes (1,048,576 bytes) rather than decimal megabytes.
|
|
*
|
|
* **Example** (Creating mebibyte sizes)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // Set a 10 MiB chunk size for large file operations
|
|
* const largeChunkSize = FileSystem.MiB(10)
|
|
*
|
|
* const stream = fs.stream("video.mp4", {
|
|
* chunkSize: largeChunkSize
|
|
* })
|
|
*
|
|
* // Check if file is larger than 100 MiB
|
|
* const stats = yield* fs.stat("archive.zip")
|
|
* const maxSize = FileSystem.MiB(100)
|
|
* if (stats.size > maxSize) {
|
|
* yield* Effect.log("File is very large!")
|
|
* }
|
|
* })
|
|
* ```
|
|
*
|
|
* @category sizes
|
|
* @since 4.0.0
|
|
*/
|
|
export const MiB = (n: number): Size => Size(n * 1024 * 1024)
|
|
|
|
/**
|
|
* Creates a `Size` representing gibibytes (1024³ bytes).
|
|
*
|
|
* **Details**
|
|
*
|
|
* Converts a number of gibibytes to the equivalent size in bytes.
|
|
* Uses binary gibibytes (1,073,741,824 bytes) rather than decimal gigabytes.
|
|
*
|
|
* **Example** (Creating gibibyte sizes)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // Use GiB values as size thresholds
|
|
* const maxArchiveSize = FileSystem.GiB(1)
|
|
* console.log(maxArchiveSize.toString()) // "1073741824"
|
|
*
|
|
* const tempFile = yield* fs.makeTempFile({ prefix: "archive-" })
|
|
* yield* fs.writeFileString(tempFile, "backup data")
|
|
*
|
|
* const info = yield* fs.stat(tempFile)
|
|
* console.log(info.size < maxArchiveSize) // true
|
|
*
|
|
* yield* fs.remove(tempFile)
|
|
* })
|
|
* ```
|
|
*
|
|
* @category sizes
|
|
* @since 4.0.0
|
|
*/
|
|
export const GiB = (n: number): Size => Size(n * 1024 * 1024 * 1024)
|
|
|
|
/**
|
|
* Creates a `Size` representing tebibytes (1024⁴ bytes).
|
|
*
|
|
* **Details**
|
|
*
|
|
* Converts a number of tebibytes to the equivalent size in bytes.
|
|
* Uses binary tebibytes (1,099,511,627,776 bytes) rather than decimal terabytes.
|
|
*
|
|
* **Example** (Creating tebibyte sizes)
|
|
*
|
|
* ```ts
|
|
* import { Console, Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // Check if we're dealing with very large files
|
|
* const stats = yield* fs.stat("database-backup.sql")
|
|
* const oneTiB = FileSystem.TiB(1)
|
|
*
|
|
* if (stats.size > oneTiB) {
|
|
* yield* Console.log("This is a very large database backup!")
|
|
*
|
|
* // Use larger chunk sizes for such files
|
|
* const stream = fs.stream("database-backup.sql", {
|
|
* chunkSize: FileSystem.MiB(100) // 100 MiB chunks
|
|
* })
|
|
* }
|
|
* })
|
|
* ```
|
|
*
|
|
* @category sizes
|
|
* @since 4.0.0
|
|
*/
|
|
export const TiB = (n: number): Size => Size(n * 1024 * 1024 * 1024 * 1024)
|
|
|
|
const bigint1024 = BigInt(1024)
|
|
const bigintPiB = bigint1024 * bigint1024 * bigint1024 * bigint1024 * bigint1024
|
|
|
|
/**
|
|
* Creates a `Size` representing pebibytes (1024⁵ bytes).
|
|
*
|
|
* **Details**
|
|
*
|
|
* Converts a number of pebibytes to the equivalent size in bytes.
|
|
* Uses binary pebibytes (1,125,899,906,842,624 bytes) rather than decimal petabytes.
|
|
* This function uses BigInt arithmetic to handle the very large numbers involved.
|
|
*
|
|
* **Example** (Creating pebibyte sizes)
|
|
*
|
|
* ```ts
|
|
* import { Console, Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // For extremely large data processing scenarios
|
|
* const massiveDataset = FileSystem.PiB(2) // 2 PiB
|
|
*
|
|
* // This would typically be used in enterprise/cloud scenarios
|
|
* yield* Console.log(`Processing ${massiveDataset} bytes of data`)
|
|
*
|
|
* // Such large files would require specialized streaming
|
|
* const stream = fs.stream("massive-dataset.bin", {
|
|
* chunkSize: FileSystem.GiB(1), // 1 GiB chunks
|
|
* offset: FileSystem.TiB(100) // Start from 100 TiB offset
|
|
* })
|
|
* })
|
|
* ```
|
|
*
|
|
* @category sizes
|
|
* @since 4.0.0
|
|
*/
|
|
export const PiB = (n: number): Size => Size(BigInt(n) * bigintPiB)
|
|
|
|
/**
|
|
* File open flags that determine how a file is opened and what operations are allowed.
|
|
*
|
|
* **Details**
|
|
*
|
|
* These flags correspond to standard POSIX file open modes and control the file access
|
|
* permissions and behavior when opening files.
|
|
*
|
|
* - `"r"` - Read-only. File must exist.
|
|
* - `"r+"` - Read/write. File must exist.
|
|
* - `"w"` - Write-only. Truncates file to zero length or creates new file.
|
|
* - `"wx"` - Like 'w' but fails if file exists.
|
|
* - `"w+"` - Read/write. Truncates file to zero length or creates new file.
|
|
* - `"wx+"` - Like 'w+' but fails if file exists.
|
|
* - `"a"` - Write-only. Appends to file or creates new file.
|
|
* - `"ax"` - Like 'a' but fails if file exists.
|
|
* - `"a+"` - Read/write. Appends to file or creates new file.
|
|
* - `"ax+"` - Like 'a+' but fails if file exists.
|
|
*
|
|
* **Example** (Opening files with flags)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // Open for reading only
|
|
* const readFile = yield* fs.open("data.txt", { flag: "r" })
|
|
*
|
|
* // Open for writing, truncating existing content
|
|
* const writeFile = yield* fs.open("output.txt", { flag: "w" })
|
|
*
|
|
* // Open for appending
|
|
* const appendFile = yield* fs.open("log.txt", { flag: "a" })
|
|
*
|
|
* // Open for read/write, but fail if file doesn't exist
|
|
* const editFile = yield* fs.open("config.json", { flag: "r+" })
|
|
* })
|
|
* ```
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export type OpenFlag =
|
|
| "r"
|
|
| "r+"
|
|
| "w"
|
|
| "wx"
|
|
| "w+"
|
|
| "wx+"
|
|
| "a"
|
|
| "ax"
|
|
| "a+"
|
|
| "ax+"
|
|
|
|
/**
|
|
* Service tag for platform file-system operations.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to access or provide operations for files, directories, permissions,
|
|
* streams, and sinks through the Effect context.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This key is used to provide and access the FileSystem service in the Effect context.
|
|
*
|
|
* **Example** (Accessing and providing FileSystem)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* // Access the FileSystem service
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* const exists = yield* fs.exists("./data.txt")
|
|
* if (exists) {
|
|
* const content = yield* fs.readFileString("./data.txt")
|
|
* yield* Effect.log("File content:", content)
|
|
* }
|
|
* })
|
|
*
|
|
* // Provide a custom FileSystem implementation
|
|
* declare const platformImpl: Omit<
|
|
* FileSystem.FileSystem,
|
|
* "exists" | "readFileString" | "stream" | "sink" | "writeFileString"
|
|
* >
|
|
* const customFs = FileSystem.make(platformImpl)
|
|
*
|
|
* const withCustomFs = Effect.provideService(
|
|
* program,
|
|
* FileSystem.FileSystem,
|
|
* customFs
|
|
* )
|
|
* ```
|
|
*
|
|
* @category services
|
|
* @since 4.0.0
|
|
*/
|
|
export const FileSystem: Context.Service<FileSystem, FileSystem> = Context.Service("effect/platform/FileSystem")
|
|
|
|
/**
|
|
* Creates a FileSystem implementation from a partial implementation.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to build a concrete `FileSystem` service from platform-specific core
|
|
* operations while deriving the convenience methods that can be implemented
|
|
* from them.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This function takes a partial FileSystem implementation and automatically provides
|
|
* default implementations for `exists`, `readFileString`, `stream`, `sink`, and
|
|
* `writeFileString` methods based on the provided core methods.
|
|
*
|
|
* @see {@link makeNoop} for a testing stub that accepts method overrides without requiring a complete implementation
|
|
* @see {@link layerNoop} for providing a no-op `FileSystem` as a `Layer` in tests
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const make = (
|
|
impl: Omit<FileSystem, typeof TypeId | "exists" | "readFileString" | "stream" | "sink" | "writeFileString">
|
|
): FileSystem =>
|
|
FileSystem.of({
|
|
...impl,
|
|
[TypeId]: TypeId,
|
|
exists: (path) =>
|
|
pipe(
|
|
impl.access(path),
|
|
Effect.as(true),
|
|
Effect.catchTag(
|
|
"PlatformError",
|
|
(e) => e.reason._tag === "NotFound" ? Effect.succeed(false) : Effect.fail(e)
|
|
)
|
|
),
|
|
readFileString: (path, encoding) =>
|
|
Effect.flatMap(impl.readFile(path), (_) =>
|
|
Effect.try({
|
|
try: () => new TextDecoder(encoding).decode(_),
|
|
catch: (cause) =>
|
|
badArgument({
|
|
module: "FileSystem",
|
|
method: "readFileString",
|
|
description: "invalid encoding",
|
|
cause
|
|
})
|
|
})),
|
|
stream: Effect.fnUntraced(function*(path, options) {
|
|
const file = yield* impl.open(path, { flag: "r" })
|
|
if (options?.offset) {
|
|
yield* file.seek(options.offset, "start")
|
|
}
|
|
const bytesToRead = options?.bytesToRead !== undefined ? Size(options.bytesToRead) : undefined
|
|
let totalBytesRead = BigInt(0)
|
|
const chunkSize = Size(options?.chunkSize ?? 64 * 1024)
|
|
const readChunk = file.readAlloc(chunkSize)
|
|
return Stream.fromPull(Effect.succeed(
|
|
Effect.flatMap(
|
|
Effect.suspend((): Pull.Pull<Option.Option<Uint8Array>, PlatformError> => {
|
|
if (bytesToRead !== undefined && bytesToRead <= totalBytesRead) {
|
|
return Cause.done()
|
|
}
|
|
return bytesToRead !== undefined && (bytesToRead - totalBytesRead) < chunkSize
|
|
? file.readAlloc(bytesToRead - totalBytesRead)
|
|
: readChunk
|
|
}),
|
|
Option.match({
|
|
onNone: () => Cause.done(),
|
|
onSome: (buf) => {
|
|
totalBytesRead += BigInt(buf.length)
|
|
return Effect.succeed(Arr.of(buf))
|
|
}
|
|
})
|
|
)
|
|
))
|
|
}, Stream.unwrap),
|
|
sink: (path, options) =>
|
|
pipe(
|
|
impl.open(path, { flag: "w", ...options }),
|
|
Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))),
|
|
Sink.unwrap
|
|
),
|
|
writeFileString: (path, data, options) =>
|
|
Effect.flatMap(
|
|
Effect.try({
|
|
try: () => new TextEncoder().encode(data),
|
|
catch: (cause) =>
|
|
badArgument({
|
|
module: "FileSystem",
|
|
method: "writeFileString",
|
|
description: "could not encode string",
|
|
cause
|
|
})
|
|
}),
|
|
(_) => impl.writeFile(path, _, options)
|
|
)
|
|
})
|
|
|
|
const notFound = (method: string, path: string) =>
|
|
systemError({
|
|
module: "FileSystem",
|
|
method,
|
|
_tag: "NotFound",
|
|
description: "No such file or directory",
|
|
pathOrDescriptor: path
|
|
})
|
|
|
|
/**
|
|
* Creates a stub `FileSystem` implementation for tests.
|
|
*
|
|
* **Details**
|
|
*
|
|
* By default, `exists` returns `false`, `remove` succeeds, many file operations
|
|
* fail with `PlatformError` `NotFound`, and temporary-directory/file operations
|
|
* die as not implemented. Pass method overrides to provide the behavior needed
|
|
* by a specific test without touching the real file system.
|
|
*
|
|
* **Example** (Creating a no-op FileSystem)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem, PlatformError } from "effect"
|
|
*
|
|
* // Create a test filesystem that only allows reading specific files
|
|
* const testFs = FileSystem.makeNoop({
|
|
* readFileString: (path) => {
|
|
* if (path === "test-config.json") {
|
|
* return Effect.succeed("{\"test\": true}")
|
|
* }
|
|
* return Effect.fail(
|
|
* PlatformError.systemError({
|
|
* _tag: "NotFound",
|
|
* module: "FileSystem",
|
|
* method: "readFileString",
|
|
* description: "File not found",
|
|
* pathOrDescriptor: path
|
|
* })
|
|
* )
|
|
* },
|
|
* exists: (path) => Effect.succeed(path === "test-config.json")
|
|
* })
|
|
*
|
|
* // Use in tests
|
|
* const program = Effect.gen(function*() {
|
|
* const content = yield* testFs.readFileString("test-config.json")
|
|
* // Will succeed with mocked content
|
|
* })
|
|
*
|
|
* // Test with the no-op filesystem
|
|
* const testProgram = Effect.provideService(
|
|
* program,
|
|
* FileSystem.FileSystem,
|
|
* testFs
|
|
* )
|
|
* ```
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const makeNoop = (fileSystem: Partial<FileSystem>): FileSystem =>
|
|
FileSystem.of({
|
|
[TypeId]: TypeId,
|
|
access(path) {
|
|
return Effect.fail(notFound("access", path))
|
|
},
|
|
chmod(path) {
|
|
return Effect.fail(notFound("chmod", path))
|
|
},
|
|
chown(path) {
|
|
return Effect.fail(notFound("chown", path))
|
|
},
|
|
copy(path) {
|
|
return Effect.fail(notFound("copy", path))
|
|
},
|
|
copyFile(path) {
|
|
return Effect.fail(notFound("copyFile", path))
|
|
},
|
|
glob(pattern) {
|
|
return Effect.fail(notFound("glob", pattern))
|
|
},
|
|
exists() {
|
|
return Effect.succeed(false)
|
|
},
|
|
link(path) {
|
|
return Effect.fail(notFound("link", path))
|
|
},
|
|
makeDirectory() {
|
|
return Effect.die("not implemented")
|
|
},
|
|
makeTempDirectory() {
|
|
return Effect.die("not implemented")
|
|
},
|
|
makeTempDirectoryScoped() {
|
|
return Effect.die("not implemented")
|
|
},
|
|
makeTempFile() {
|
|
return Effect.die("not implemented")
|
|
},
|
|
makeTempFileScoped() {
|
|
return Effect.die("not implemented")
|
|
},
|
|
open(path) {
|
|
return Effect.fail(notFound("open", path))
|
|
},
|
|
readDirectory(path) {
|
|
return Effect.fail(notFound("readDirectory", path))
|
|
},
|
|
readFile(path) {
|
|
return Effect.fail(notFound("readFile", path))
|
|
},
|
|
readFileString(path) {
|
|
return Effect.fail(notFound("readFileString", path))
|
|
},
|
|
readLink(path) {
|
|
return Effect.fail(notFound("readLink", path))
|
|
},
|
|
realPath(path) {
|
|
return Effect.fail(notFound("realPath", path))
|
|
},
|
|
remove() {
|
|
return Effect.void
|
|
},
|
|
rename(oldPath) {
|
|
return Effect.fail(notFound("rename", oldPath))
|
|
},
|
|
sink(path) {
|
|
return Sink.fail(notFound("sink", path))
|
|
},
|
|
stat(path) {
|
|
return Effect.fail(notFound("stat", path))
|
|
},
|
|
stream(path) {
|
|
return Stream.fail(notFound("stream", path))
|
|
},
|
|
symlink(fromPath) {
|
|
return Effect.fail(notFound("symlink", fromPath))
|
|
},
|
|
truncate(path) {
|
|
return Effect.fail(notFound("truncate", path))
|
|
},
|
|
utimes(path) {
|
|
return Effect.fail(notFound("utimes", path))
|
|
},
|
|
watch(path) {
|
|
return Stream.fail(notFound("watch", path))
|
|
},
|
|
writeFile(path) {
|
|
return Effect.fail(notFound("writeFile", path))
|
|
},
|
|
writeFileString(path) {
|
|
return Effect.fail(notFound("writeFileString", path))
|
|
},
|
|
...fileSystem
|
|
})
|
|
|
|
/**
|
|
* Creates a Layer that provides a no-op FileSystem implementation for testing.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This is a convenience function that wraps `makeNoop` in a Layer, making it easy
|
|
* to provide the test filesystem to your Effect programs.
|
|
*
|
|
* **Example** (Providing a no-op FileSystem layer)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem } from "effect"
|
|
*
|
|
* // Create a test layer with specific behaviors
|
|
* const testLayer = FileSystem.layerNoop({
|
|
* readFileString: (path) => Effect.succeed("mocked content"),
|
|
* exists: () => Effect.succeed(true)
|
|
* })
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
* const content = yield* fs.readFileString("any-file.txt")
|
|
* return content
|
|
* })
|
|
*
|
|
* // Provide the test layer
|
|
* const testProgram = Effect.provide(program, testLayer)
|
|
* ```
|
|
*
|
|
* @category layers
|
|
* @since 4.0.0
|
|
*/
|
|
export const layerNoop = (fileSystem: Partial<FileSystem>): Layer.Layer<FileSystem> =>
|
|
Layer.succeed(FileSystem)(makeNoop(fileSystem))
|
|
|
|
/**
|
|
* Runtime type identifier attached to `FileSystem.File` handles and used by
|
|
* `isFile` to recognize them.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This marker is part of the runtime representation of file handles. Prefer
|
|
* `isFile` when narrowing unknown values.
|
|
*
|
|
* @see {@link File} for the open file handle shape that carries this marker
|
|
* @see {@link isFile} for the public guard that checks this marker
|
|
*
|
|
* @category type IDs
|
|
* @since 4.0.0
|
|
*/
|
|
export const FileTypeId = "~effect/platform/FileSystem/File"
|
|
|
|
/**
|
|
* Returns `true` if a value is a `File` handle by checking for the
|
|
* `FileTypeId` marker.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use when accepting an unknown value and you need to narrow it to a `File`
|
|
* before calling file-handle operations.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This is a structural marker check. It does not validate the marker value or
|
|
* the shape of the file handle.
|
|
*
|
|
* @see {@link File} for the file-handle interface narrowed by this guard
|
|
* @see {@link FileTypeId} for the runtime marker checked by this guard
|
|
*
|
|
* @category file
|
|
* @since 4.0.0
|
|
*/
|
|
export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId)
|
|
|
|
/**
|
|
* Interface representing an open file handle.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Provides low-level file operations including reading, writing, seeking,
|
|
* and retrieving file information. File handles are automatically managed
|
|
* within scoped operations to ensure proper cleanup.
|
|
*
|
|
* **Example** (Working with file handles)
|
|
*
|
|
* ```ts
|
|
* import { Console, Effect, FileSystem } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // Open a file and work with the handle
|
|
* yield* Effect.scoped(
|
|
* Effect.gen(function*() {
|
|
* const file = yield* fs.open("./data.txt", { flag: "r+" })
|
|
*
|
|
* // Get file information
|
|
* const stats = yield* file.stat
|
|
* yield* Console.log(`File size: ${stats.size} bytes`)
|
|
*
|
|
* // Read from specific position
|
|
* yield* file.seek(10, "start")
|
|
* const buffer = new Uint8Array(5)
|
|
* const bytesRead = yield* file.read(buffer)
|
|
* yield* Console.log(`Read ${bytesRead} bytes:`, buffer)
|
|
*
|
|
* // Write data
|
|
* const data = new TextEncoder().encode("Hello")
|
|
* yield* file.write(data)
|
|
* yield* file.sync // Flush to disk
|
|
* })
|
|
* )
|
|
* })
|
|
* ```
|
|
*
|
|
* @category file
|
|
* @since 4.0.0
|
|
*/
|
|
export interface File {
|
|
readonly [FileTypeId]: typeof FileTypeId
|
|
readonly fd: File.Descriptor
|
|
readonly stat: Effect.Effect<File.Info, PlatformError>
|
|
readonly seek: (offset: SizeInput, from: SeekMode) => Effect.Effect<void>
|
|
readonly sync: Effect.Effect<void, PlatformError>
|
|
readonly read: (buffer: Uint8Array) => Effect.Effect<Size, PlatformError>
|
|
readonly readAlloc: (size: SizeInput) => Effect.Effect<Option.Option<Uint8Array>, PlatformError>
|
|
readonly truncate: (length?: SizeInput) => Effect.Effect<void, PlatformError>
|
|
readonly write: (buffer: Uint8Array) => Effect.Effect<Size, PlatformError>
|
|
readonly writeAll: (buffer: Uint8Array) => Effect.Effect<void, PlatformError>
|
|
}
|
|
|
|
/**
|
|
* Namespace containing types associated with open file handles, including file
|
|
* descriptors, entry kinds, and stat information.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
export declare namespace File {
|
|
/**
|
|
* Branded type for file descriptors.
|
|
*
|
|
* **Details**
|
|
*
|
|
* File descriptors are numeric handles used by the operating system
|
|
* to identify open files. The branded type ensures type safety.
|
|
*
|
|
* @category file
|
|
* @since 4.0.0
|
|
*/
|
|
export type Descriptor = Brand.Branded<number, "FileDescriptor">
|
|
|
|
/**
|
|
* Enumeration of possible file system entry types.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Represents the different types of entries that can exist in a file system,
|
|
* from regular files to special device files and symbolic links.
|
|
*
|
|
* @category file
|
|
* @since 4.0.0
|
|
*/
|
|
export type Type =
|
|
| "File"
|
|
| "Directory"
|
|
| "SymbolicLink"
|
|
| "BlockDevice"
|
|
| "CharacterDevice"
|
|
| "FIFO"
|
|
| "Socket"
|
|
| "Unknown"
|
|
|
|
/**
|
|
* Comprehensive file information structure.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Contains metadata about a file or directory including type, timestamps,
|
|
* permissions, and size information. This structure is returned by file
|
|
* stat operations.
|
|
*
|
|
* **Example** (Inspecting file information)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem, Option } from "effect"
|
|
*
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* const path = yield* fs.makeTempFile({ prefix: "info-" })
|
|
* yield* fs.writeFileString(path, "hello")
|
|
*
|
|
* const info: FileSystem.File.Info = yield* fs.stat(path)
|
|
*
|
|
* console.log(`File type: ${info.type}`) // "File type: File"
|
|
* console.log(`File size: ${info.size} bytes`) // "File size: 5 bytes"
|
|
* console.log(`Mode: ${info.mode.toString(8)}`) // Octal permissions
|
|
*
|
|
* // Handle optional timestamps without inventing a fallback date
|
|
* const modified = Option.match(info.mtime, {
|
|
* onNone: () => "unavailable",
|
|
* onSome: (mtime) => mtime.toISOString()
|
|
* })
|
|
* console.log(`Modified: ${modified}`)
|
|
*
|
|
* // Check if it's a regular file
|
|
* if (info.type === "File") {
|
|
* console.log("Processing regular file...") // "Processing regular file..."
|
|
* }
|
|
*
|
|
* yield* fs.remove(path)
|
|
* })
|
|
* ```
|
|
*
|
|
* @category file
|
|
* @since 4.0.0
|
|
*/
|
|
export interface Info {
|
|
readonly type: Type
|
|
readonly mtime: Option.Option<Date>
|
|
readonly atime: Option.Option<Date>
|
|
readonly birthtime: Option.Option<Date>
|
|
readonly dev: number
|
|
readonly ino: Option.Option<number>
|
|
readonly mode: number
|
|
readonly nlink: Option.Option<number>
|
|
readonly uid: Option.Option<number>
|
|
readonly gid: Option.Option<number>
|
|
readonly rdev: Option.Option<number>
|
|
readonly size: Size
|
|
readonly blksize: Option.Option<Size>
|
|
readonly blocks: Option.Option<number>
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a `File.Descriptor` from a number.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to brand an operating-system file descriptor number when implementing a
|
|
* `FileSystem` that returns custom `File` handles.
|
|
*
|
|
* **Details**
|
|
*
|
|
* `File.Descriptor` is a branded integer handle used by operating systems to
|
|
* identify open files.
|
|
*
|
|
* **Gotchas**
|
|
*
|
|
* This constructor is nominal and does not check that the number is an integer
|
|
* or that it refers to an open file descriptor.
|
|
*
|
|
* @see {@link File.Descriptor} for the branded descriptor type produced by this constructor
|
|
* @see {@link File} for file handles that expose a descriptor through `fd`
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const FileDescriptor = Brand.nominal<File.Descriptor>()
|
|
|
|
/**
|
|
* Specifies the reference point for seeking within an open file.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use with `File` handles when positioning the cursor before a read or write
|
|
* and the offset must be interpreted from either the start of the file or the
|
|
* current cursor.
|
|
*
|
|
* **Details**
|
|
*
|
|
* - `"start"` seeks from the beginning of the file.
|
|
* - `"current"` seeks from the current cursor position.
|
|
*
|
|
* @see {@link File} for the open file handle API whose `seek` method consumes this mode
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export type SeekMode = "start" | "current"
|
|
|
|
/**
|
|
* Represents file system events emitted when watching files or directories.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use when consuming file system watch streams and pattern matching on `_tag`
|
|
* to handle created, updated, or removed paths.
|
|
*
|
|
* **Details**
|
|
*
|
|
* The union covers create, update, and remove events. Each event carries the
|
|
* reported `path`.
|
|
*
|
|
* @see {@link FileSystem} for the service interface whose `watch` operation emits these events
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export type WatchEvent = WatchEvent.Create | WatchEvent.Update | WatchEvent.Remove
|
|
|
|
/**
|
|
* Namespace containing the concrete event shapes emitted by `FileSystem.watch`.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
export declare namespace WatchEvent {
|
|
/**
|
|
* Event representing the creation of a new file or directory.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This event is triggered when a new file or directory is created
|
|
* in the watched location.
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface Create {
|
|
readonly _tag: "Create"
|
|
readonly path: string
|
|
}
|
|
|
|
/**
|
|
* Event representing the modification of an existing file or directory.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This event is triggered when an existing file or directory is
|
|
* modified in the watched location.
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface Update {
|
|
readonly _tag: "Update"
|
|
readonly path: string
|
|
}
|
|
|
|
/**
|
|
* Event representing the deletion of a file or directory.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This event is triggered when a file or directory is deleted
|
|
* from the watched location.
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface Remove {
|
|
readonly _tag: "Remove"
|
|
readonly path: string
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Service key for file system watch backend implementations.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This service provides the low-level file watching capabilities that can be
|
|
* implemented differently on various platforms (e.g., inotify on Linux,
|
|
* FSEvents on macOS, etc.).
|
|
*
|
|
* **Example** (Providing a custom watch backend)
|
|
*
|
|
* ```ts
|
|
* import { Effect, FileSystem, Option, Stream } from "effect"
|
|
*
|
|
* // Custom watch backend implementation
|
|
* const customWatchBackend = {
|
|
* register: (path: string, stat: FileSystem.File.Info) => {
|
|
* // Implementation would depend on platform
|
|
* return Option.some(Stream.empty) // Placeholder implementation
|
|
* }
|
|
* }
|
|
*
|
|
* // Provide custom watch backend
|
|
* const program = Effect.gen(function*() {
|
|
* const fs = yield* FileSystem.FileSystem
|
|
*
|
|
* // File watching will use the custom backend
|
|
* const watcher = fs.watch("./directory")
|
|
* })
|
|
*
|
|
* const withCustomBackend = Effect.provideService(
|
|
* program,
|
|
* FileSystem.WatchBackend,
|
|
* customWatchBackend
|
|
* )
|
|
* ```
|
|
*
|
|
* @category file watcher
|
|
* @since 4.0.0
|
|
*/
|
|
export class WatchBackend extends Context.Service<WatchBackend, {
|
|
readonly register: (path: string, stat: File.Info) => Option.Option<Stream.Stream<WatchEvent, PlatformError>>
|
|
}>()("effect/platform/FileSystem/WatchBackend") {}
|