名称: effect-patterns-platform 描述: Effect-TS平台模式。在Effect-TS应用程序中处理平台相关任务时使用。
Effect-TS模式:平台
这个技能提供了6个精选的Effect-TS平台模式。 在以下任务相关时使用此技能:
- 平台
- Effect-TS应用程序中的最佳实践
- 真实世界模式和解决方案
🟢 初级模式
平台模式4:交互式终端I/O
规则: 在CLI应用程序中使用Terminal进行用户输入/输出,提供适当的缓冲和跨平台字符编码。
良好示例:
此示例演示构建交互式CLI应用程序。
import { Terminal, Effect } from "@effect/platform";
interface UserInput {
readonly name: string;
readonly email: string;
readonly age: number;
}
const program = Effect.gen(function* () {
console.log(`
[交互式CLI] 用户信息表单
`);
// 示例1:简单提示
yield* Terminal.writeLine(`=== 用户设置 ===`);
yield* Terminal.writeLine(``);
yield* Terminal.write(`您的姓名是什么? `);
const name = yield* Terminal.readLine();
yield* Terminal.write(`您的电子邮件是什么? `);
const email = yield* Terminal.readLine();
yield* Terminal.write(`您的年龄是多少? `);
const ageStr = yield* Terminal.readLine();
const age = parseInt(ageStr);
// 示例2:显示收集的信息
yield* Terminal.writeLine(``);
yield* Terminal.writeLine(`=== 摘要 ===`);
yield* Terminal.writeLine(`姓名:${name}`);
yield* Terminal.writeLine(`电子邮件:${email}`);
yield* Terminal.writeLine(`年龄:${age}`);
// 示例3:确认
yield* Terminal.writeLine(``);
yield* Terminal.write(`确认信息?(是/否) `);
const confirm = yield* Terminal.readLine();
if (confirm.toLowerCase() === "是") {
yield* Terminal.writeLine(`✓ 信息已保存`);
} else {
yield* Terminal.writeLine(`✗ 已取消`);
}
});
Effect.runPromise(program);
原理:
Terminal操作:
- readLine:读取单行用户输入
- readPassword:读取输入时不回显(密码)
- writeLine:写入行并换行
- write:写入不换行
- clearScreen:清除终端
模式:Terminal.readLine().pipe(...)
直接使用stdin/stdout会导致问题:
- 无缓冲:在并发上下文中输出交错
- 编码问题:特殊字符损坏
- 密码回显:安全漏洞
- 无类型安全:字符串操作易出错
Terminal启用:
- 缓冲I/O:安全的并发输出
- 编码处理:UTF-8和特殊字符
- 密码输入:无回显模式
- 结构化交互:提示和验证
真实世界示例:CLI设置向导
- 直接:console.log与readline混合,无错误处理
- 使用Terminal:结构化输入、验证、格式化输出
平台模式2:文件系统操作
规则: 使用FileSystem模块进行安全、资源管理的文件操作,具有适当的错误处理和清理。
良好示例:
此示例演示读取、写入和操作文件。
import { FileSystem, Effect, Stream } from "@effect/platform";
import * as fs from "fs/promises";
const program = Effect.gen(function* () {
console.log(`
[文件系统] 演示文件操作
`);
// 示例1:写入文件
console.log(`[1] 写入文件:
`);
const content = `你好,Effect-TS!
这是一个测试文件。
创建于 ${new Date().toISOString()}`;
yield* FileSystem.writeFileUtf8("test.txt", content);
yield* Effect.log(`✓ 文件已写入:test.txt`);
// 示例2:读取文件
console.log(`
[2] 读取文件:
`);
const readContent = yield* FileSystem.readFileUtf8("test.txt");
console.log(readContent);
// 示例3:获取文件统计信息
console.log(`
[3] 文件统计信息:
`);
const stats = yield* FileSystem.stat("test.txt").pipe(
Effect.flatMap((stat) =>
Effect.succeed({
size: stat.size,
isFile: stat.isFile(),
modified: stat.mtimeMs,
})
)
);
console.log(` 大小:${stats.size} 字节`);
console.log(` 是文件:${stats.isFile}`);
console.log(` 修改时间:${new Date(stats.modified).toISOString()}`);
// 示例4:创建目录并写入多个文件
console.log(`
[4] 创建目录和文件:
`);
yield* FileSystem.mkdir("test-dir");
yield* Effect.all(
Array.from({ length: 3 }, (_, i) =>
FileSystem.writeFileUtf8(
`test-dir/file-${i + 1}.txt`,
`文件 ${i + 1} 的内容`
)
)
);
yield* Effect.log(`✓ 创建目录并包含3个文件`);
// 示例5:列出目录内容
console.log(`
[5] 列出目录内容:
`);
const entries = yield* FileSystem.readDirectory("test-dir");
entries.forEach((entry) => {
console.log(` - ${entry}`);
});
// 示例6:追加到文件
console.log(`
[6] 追加到文件:
`);
const appendContent = `
追加行于 ${new Date().toISOString()}`;
yield* FileSystem.appendFileUtf8("test.txt", appendContent);
const finalContent = yield* FileSystem.readFileUtf8("test.txt");
console.log(`文件现在有 ${finalContent.split("
").length} 行`);
// 示例7:清理
console.log(`
[7] 清理:
`);
yield* Effect.all(
Array.from({ length: 3 }, (_, i) =>
FileSystem.remove(`test-dir/file-${i + 1}.txt`)
)
);
yield* FileSystem.remove("test-dir");
yield* FileSystem.remove("test.txt");
yield* Effect.log(`✓ 清理完成`);
});
Effect.runPromise(program);
原理:
FileSystem操作:
- read:以字符串读取文件
- readDirectory:列出目录中的文件
- write:将字符串写入文件
- remove:删除文件或目录
- stat:获取文件元数据
模式:FileSystem.read(path).pipe(...)
没有FileSystem的直接文件操作会导致问题:
- 资源泄漏:错误时文件未关闭
- 无错误上下文:错误中缺少文件名
- 阻塞:无异步/等待集成
- 跨平台:路径处理差异
FileSystem启用:
- 资源安全:自动清理
- 错误上下文:完整的错误消息
- 异步集成:Effect原生
- 跨平台:处理路径分隔符
真实世界示例:处理日志文件
- 直接:打开文件、读取、关闭、手动处理异常
- 使用FileSystem:
FileSystem.read(path).pipe(...)
🟡 中级模式
平台模式3:持久键值存储
规则: 使用KeyValueStore进行简单的键值对持久存储,支持轻量级缓存和会话管理。
良好示例:
此示例演示存储和检索持久数据。
import { KeyValueStore, Effect } from "@effect/platform";
interface UserSession {
readonly userId: string;
readonly token: string;
readonly expiresAt: number;
}
const program = Effect.gen(function* () {
console.log(`
[键值存储] 持久存储示例
`);
const store = yield* KeyValueStore.KeyValueStore;
// 示例1:存储会话数据
console.log(`[1] 存储会话:
`);
const session: UserSession = {
userId: "user-123",
token: "token-abc-def",
expiresAt: Date.now() + 3600000, // 1小时
};
yield* store.set("session:user-123", JSON.stringify(session));
yield* Effect.log(`✓ 会话已存储`);
// 示例2:检索存储的数据
console.log(`
[2] 检索会话:
`);
const stored = yield* store.get("session:user-123");
if (stored._tag === "Some") {
const retrievedSession = JSON.parse(stored.value) as UserSession;
console.log(` 用户ID:${retrievedSession.userId}`);
console.log(` 令牌:${retrievedSession.token}`);
console.log(
` 过期时间:${new Date(retrievedSession.expiresAt).toISOString()}`
);
}
// 示例3:检查键是否存在
console.log(`
[3] 检查键:
`);
const hasSession = yield* store.has("session:user-123");
const hasOther = yield* store.has("session:user-999");
console.log(` 有 session:user-123:${hasSession}`);
console.log(` 有 session:user-999:${hasOther}`);
// 示例4:存储多个缓存条目
console.log(`
[4] 缓存API响应:
`);
const apiResponses = [
{ endpoint: "/api/users", data: [{ id: 1, name: "Alice" }] },
{ endpoint: "/api/posts", data: [{ id: 1, title: "First Post" }] },
{ endpoint: "/api/comments", data: [] },
];
yield* Effect.all(
apiResponses.map((item) =>
store.set(
`cache:${item.endpoint}`,
JSON.stringify(item.data)
)
)
);
yield* Effect.log(`✓ 缓存了 ${apiResponses.length} 个端点`);
// 示例5:检索带过期的缓存
console.log(`
[5] 检查缓存数据:
`);
for (const item of apiResponses) {
const cached = yield* store.get(`cache:${item.endpoint}`);
if (cached._tag === "Some") {
const data = JSON.parse(cached.value);
console.log(
` ${item.endpoint}:${Array.isArray(data) ? data.length : 1} 项`
);
}
}
// 示例6:删除特定条目
console.log(`
[6] 删除条目:
`);
yield* store.remove("cache:/api/comments");
const removed = yield* store.has("cache:/api/comments");
console.log(` 删除后是否存在:${removed}`);
// 示例7:迭代和计数条目
console.log(`
[7] 计数条目:
`);
const allKeys = yield* store.entries.pipe(
Effect.map((entries) => entries.length)
);
console.log(` 总条目数:${allKeys}`);
});
Effect.runPromise(program);
原理:
KeyValueStore操作:
- set:存储键值对
- get:按键检索值
- remove:删除键
- has:检查键是否存在
- clear:删除所有条目
模式:KeyValueStore.set(key, value).pipe(...)
没有持久存储,临时数据会丢失:
- 会话数据:重启时丢失
- 缓存:从头重建
- 配置:硬编码或基于文件
- 状态:分散在代码中
KeyValueStore启用:
- 透明持久化:自动后端处理
- 简单API:键值抽象
- 可插拔后端:内存、文件系统、数据库
- Effect集成:类型安全、可组合
真实世界示例:缓存API响应
- 直接:在内存Map中缓存(重启时丢失)
- 使用KeyValueStore:跨重启持久化
平台模式1:执行Shell命令
规则: 使用Command生成和管理外部进程,捕获输出并可靠地处理退出代码,具有适当的错误处理。
良好示例:
此示例演示执行命令并处理其输出。
import { Command, Effect, Chunk } from "@effect/platform";
// 简单命令执行
const program = Effect.gen(function* () {
console.log(`
[命令] 执行Shell命令
`);
// 示例1:列出文件
console.log(`[1] 列出当前目录中的文件:
`);
const lsResult = yield* Command.make("ls", ["-la"]).pipe(
Command.string
);
console.log(lsResult);
// 示例2:获取当前日期
console.log(`
[2] 获取当前日期:
`);
const dateResult = yield* Command.make("date", ["+%Y-%m-%d %H:%M:%S"]).pipe(
Command.string
);
console.log(`当前日期:${dateResult.trim()}`);
// 示例3:捕获退出代码
console.log(`
[3] 检查文件是否存在:
`);
const fileCheckCmd = yield* Command.make("test", [
"-f",
"/etc/passwd",
]).pipe(
Command.exitCode,
Effect.either
);
if (fileCheckCmd._tag === "Right") {
console.log(`✓ 文件存在(退出代码:0)`);
} else {
console.log(`✗ 文件未找到(退出代码:${fileCheckCmd.left})`);
}
// 示例4:在自定义工作目录中执行
console.log(`
[4] 列出TypeScript文件:
`);
const findResult = yield* Command.make("find", [
".",
"-name",
"*.ts",
"-type",
"f",
]).pipe(
Command.lines
);
const tsFiles = Chunk.take(findResult, 5); // 前5个
Chunk.forEach(tsFiles, (file) => {
console.log(` - ${file}`);
});
if (Chunk.size(findResult) > 5) {
console.log(` ... 还有 ${Chunk.size(findResult) - 5} 个`);
}
// 示例5:优雅处理命令失败
console.log(`
[5] 优雅处理命令失败:
`);
const failResult = yield* Command.make("false").pipe(
Command.exitCode,
Effect.catchAll((error) =>
Effect.succeed(-1) // 任何错误返回-1
)
);
console.log(`退出代码:${failResult}`);
});
Effect.runPromise(program);
原理:
使用Command执行Shell命令:
- Spawn:启动外部进程
- Capture:获取stdout/stderr/退出代码
- Wait:阻塞直到完成
- Handle errors:退出代码指示失败
模式:Command.exec("command args").pipe(...)
没有适当处理的Shell集成会导致问题:
- 未处理的错误:非零退出代码丢失
- 死锁:如果未排空,stdout缓冲区填满
- 资源泄漏:进程留下运行
- 输出丢失:stderr被忽略
- 竞态条件:不安全的并发执行
Command启用:
- 类型安全执行:成功/失败在Effect中处理
- 输出捕获:stdout和stderr都可用
- 资源清理:自动进程终止
- 退出代码处理:显式错误映射
真实世界示例:构建管道
- 直接:进程生成,输出与应用日志混合,退出代码忽略
- 使用Command:输出捕获,退出代码检查,错误传播
平台模式5:跨平台路径操作
规则: 使用Effect的平台感知路径实用程序来处理分隔符、绝对/相对路径和环境变量,保持一致。
良好示例:
此示例演示跨平台路径操作。
import { Effect, FileSystem } from "@effect/platform";
import * as Path from "node:path";
import * as OS from "node:os";
interface PathOperation {
readonly input: string;
readonly description: string;
}
// 平台信息
const getPlatformInfo = () =>
Effect.gen(function* () {
const platform = process.platform;
const separator = Path.sep;
const delimiter = Path.delimiter;
const homeDir = OS.homedir();
yield* Effect.log(
`[平台] OS:${platform}, 分隔符:"${separator}", 主目录:${homeDir}`
);
return { platform, separator, delimiter, homeDir };
});
const program = Effect.gen(function* () {
console.log(`
[路径操作] 跨平台路径操作
`);
const platformInfo = yield* getPlatformInfo();
// 示例1:路径连接(处理分隔符)
console.log(`
[1] 连接路径(自动处理分隔符):
`);
const segments = ["data", "reports", "2024"];
const joinedPath = Path.join(...segments);
yield* Effect.log(`[连接] 输入:${segments.join(" + ")}`);
yield* Effect.log(`[连接] 输出:${joinedPath}`);
// 示例2:解析为绝对路径
console.log(`
[2] 解析相对 → 绝对:
`);
const relativePath = "./config/settings.json";
const absolutePath = Path.resolve(relativePath);
yield* Effect.log(`[解析] 相对:${relativePath}`);
yield* Effect.log(`[解析] 绝对:${absolutePath}`);
// 示例3:路径解析
console.log(`
[3] 解析路径组件:
`);
const filePath = "/home/user/documents/report.pdf";
const parsed = Path.parse(filePath);
yield* Effect.log(`[解析] 输入:${filePath}`);
yield* Effect.log(` 根:${parsed.root}`);
yield* Effect.log(` 目录:${parsed.dir}`);
yield* Effect.log(` 基础:${parsed.base}`);
yield* Effect.log(` 名称:${parsed.name}`);
yield* Effect.log(` 扩展名:${parsed.ext}`);
// 示例4:环境变量扩展
console.log(`
[4] 环境变量扩展:
`);
const expandPath = (pathStr: string): string => {
let result = pathStr;
// 扩展常见变量
result = result.replace("$HOME", OS.homedir());
result = result.replace("~", OS.homedir());
result = result.replace("$USER", process.env.USER || "user");
result = result.replace("$PWD", process.cwd());
// 处理Windows风格环境变量
result = result.replace(/%USERPROFILE%/g, OS.homedir());
result = result.replace(/%USERNAME%/g, process.env.USERNAME || "user");
result = result.replace(/%TEMP%/g, OS.tmpdir());
return result;
};
const envPaths = [
"$HOME/myapp/data",
"~/documents/file.txt",
"$PWD/config",
"/var/log/app.log",
];
for (const envPath of envPaths) {
const expanded = expandPath(envPath);
yield* Effect.log(
`[扩展] ${envPath} → ${expanded}`
);
}
// 示例5:路径规范化(移除冗余分隔符)
console.log(`
[5] 路径规范化:
`);
const messyPaths = [
"/home//user///documents",
"C:\\Users\\\\documents\\\\file.txt",
"./config/../config/./settings",
"../data/../../root",
];
for (const messy of messyPaths) {
const normalized = Path.normalize(messy);
yield* Effect.log(
`[规范化] ${messy}`
);
yield* Effect.log(
`[规范化] → ${normalized}`
);
}
// 示例6:带基本目录的安全路径构造
console.log(`
[6] 安全路径构造(路径遍历预防):
`);
const baseDir = "/var/app/data";
const safeJoin = (base: string, userPath: string): Result<string> => {
// 拒绝来自不受信任输入的绝对路径
if (Path.isAbsolute(userPath)) {
return { success: false, reason: "不允许绝对路径" };
}
// 拒绝包含 .. 的路径
if (userPath.includes("..")) {
return { success: false, reason: "检测到路径遍历尝试" };
}
// 解析并验证在基本目录内
const fullPath = Path.resolve(base, userPath);
if (!fullPath.startsWith(base)) {
return { success: false, reason: "路径逃逸基本目录" };
}
return { success: true, path: fullPath };
};
interface Result<T> {
success: boolean;
reason?: string;
path?: T;
}
const testPaths = [
"reports/2024.json",
"/etc/passwd",
"../../../root",
"data/file.txt",
];
for (const test of testPaths) {
const result = safeJoin(baseDir, test);
if (result.success) {
yield* Effect.log(`[安全] ✓ ${test} → ${result.path}`);
} else {
yield* Effect.log(`[安全] ✗ ${test} (${result.reason})`);
}
}
// 示例7:相对路径计算
console.log(`
[7] 计算相对路径:
`);
const fromDir = "/home/user/projects/myapp";
const toPath = "/home/user/data/config.json";
const relativePath2 = Path.relative(fromDir, toPath);
yield* Effect.log(`[相对] 从:${fromDir}`);
yield* Effect.log(`[相对] 到:${toPath}`);
yield* Effect.log(`[相对] 相对:${relativePath2}`);
// 示例8:常见路径模式
console.log(`
[8] 常见模式:
`);
// 获取文件扩展名
const fileName = "document.tar.gz";
const ext = Path.extname(fileName);
const baseName = Path.basename(fileName);
const dirName = Path.dirname("/home/user/file.txt");
yield* Effect.log(`[模式] 文件:${fileName}`);
yield* Effect.log(` basename:${baseName}`);
yield* Effect.log(` dirname:${dirName}`);
yield* Effect.log(` extname:${ext}`);
// 示例9:路径段数组
console.log(`
[9] 路径段:
`);
const segmentPath = "/home/user/documents/report.pdf";
const segments2 = segmentPath.split(Path.sep).filter((s) => s);
yield* Effect.log(`[段] ${segmentPath}`);
yield* Effect.log(`[段] → [${segments2.map((s) => `"${s}"`).join(", ")}]`);
});
Effect.runPromise(program);
原理:
路径操作需要平台感知:
- 分隔符:Windows使用
\,Unix使用/ - 绝对与相对:
/root与./file - 环境变量:
$HOME,%APPDATA% - 解析:规范化、解析符号链接
- 验证:预防路径遍历攻击
模式:避免字符串连接,使用 path.join(),path.resolve()
基于字符串的路径处理会导致问题:
问题1:平台不一致
- 写入路径:
"C:\\data\\file.txt"(Windows) - 部署到Linux,解释为字面量 “C:\data\file.txt”
- 文件未找到错误,生产中断
问题2:路径遍历攻击
- 用户提供路径:
"../../../../etc/passwd" - 无验证 → 读取敏感文件
- 安全漏洞
问题3:环境变量扩展
- 用户配置:
"$HOME/myapp/data" - 无扩展:路径中的字面量
$HOME - 无法找到文件
问题4:符号链接解析
- 文件在
/etc/ssl/certs/ca-bundle.crt(符号链接) - 真实文件在
/usr/share/ca-certificates/ca-bundle.crt - 两者指向同一文件,但字符串相等性失败
解决方案:
平台感知API:
path.join()处理分隔符path.resolve()创建绝对路径path.parse()组件- 自动处理平台差异
变量扩展:
$HOME,~→ 用户主目录$USER→ 用户名$PWD→ 当前目录
验证:
- 拒绝包含
..的路径 - 拒绝来自不受信任输入的绝对路径
- 将路径限制在基本目录内
🟠 高级模式
平台模式6:高级文件系统操作
规则: 使用高级文件系统模式实现高效、可靠的文件操作,具有适当的错误处理和资源清理。
良好示例:
此示例演示高级文件系统模式。
import { Effect, Stream, Ref, FileSystem } from "@effect/platform";
import * as Path from "node:path";
import * as FS from "node:fs";
import * as PromiseFS from "node:fs/promises";
const program = Effect.gen(function* () {
console.log(`
[高级文件系统] 复杂文件操作
`);
// 示例1:使用临时文件的原子文件写入
console.log(`[1] 原子写入(防崩溃):
`);
const atomicWrite = (
filePath: string,
content: string
): Effect.Effect<void> =>
Effect.gen(function* () {
const tempPath = `${filePath}.tmp`;
try {
// 步骤1:写入临时文件
yield* Effect.promise(() =>
PromiseFS.writeFile(tempPath, content, "utf-8")
);
yield* Effect.log(`[写入] 写入到临时文件`);
// 步骤2:确保在磁盘上(fsync)
yield* Effect.promise(() =>
PromiseFS.writeFile(tempPath, content, "utf-8")
);
yield* Effect.log(`[fsync] 数据在磁盘上`);
// 步骤3:原子重命名
yield* Effect.promise(() =>
PromiseFS.rename(tempPath, filePath)
);
yield* Effect.log(`[重命名] 原子重命名完成`);
} catch (error) {
// 失败时清理
try {
yield* Effect.promise(() => PromiseFS.unlink(tempPath));
} catch {
// 忽略清理错误
}
yield* Effect.fail(error);
}
});
// 测试原子写入
const testFile = "./test-file.txt";
yield* atomicWrite(testFile, "重要配置
");
// 验证文件
const content = yield* Effect.promise(() =>
PromiseFS.readFile(testFile, "utf-8")
);
yield* Effect.log(`[读取] 得到:"${content.trim()}"
`);
// 示例2:流式读取(内存高效)
console.log(`[2] 流式读取(处理大文件):
`);
const streamingRead = (filePath: string) =>
Effect.gen(function* () {
let byteCount = 0;
let lineCount = 0;
const readStream = FS.createReadStream(filePath, {
encoding: "utf-8",
highWaterMark: 64 * 1024, // 64KB块
});
yield* Effect.log(`[流] 以64KB块开始读取`);
const processLine = (line: string) =>
Effect.gen(function* () {
byteCount += line.length;
lineCount++;
if (lineCount <= 2 || lineCount % 1000 === 0) {
yield* Effect.log(
`[行 ${lineCount}] 长度:${line.length} 字节`
);
}
});
// 在实际代码中,处理所有行
yield* processLine("行 1");
yield* processLine("行 2");
yield* Effect.log(
`[总计] 读取 ${lineCount} 行,${byteCount} 字节`
);
});
yield* streamingRead(testFile);
// 示例3:递归目录列表
console.log(`
[3] 递归目录遍历:
`);
const recursiveList = (
dir: string,
maxDepth: number = 3
): Effect.Effect<Array<{ path: string; type: "file" | "dir" }>> =>
Effect.gen(function* () {
const results: Array<{ path: string; type: "file" | "dir" }> = [];
const traverse = (currentDir: string, depth: number) =>
Effect.gen(function* () {
if (depth > maxDepth) {
return;
}
const entries = yield* Effect.promise(() =>
PromiseFS.readdir(currentDir, { withFileTypes: true })
);
for (const entry of entries) {
const fullPath = Path.join(currentDir, entry.name);
if (entry.isDirectory()) {
results.push({ path: fullPath, type: "dir" });
yield* traverse(fullPath, depth + 1);
} else {
results.push({ path: fullPath, type: "file" });
}
}
});
yield* traverse(dir, 0);
return results;
});
// 列出当前目录中的文件
const entries = yield* recursiveList(".", 1);
yield* Effect.log(
`[条目] 找到 ${entries.length} 项:`
);
for (const entry of entries.slice(0, 5)) {
const type = entry.type === "file" ? "📄" : "📁";
yield* Effect.log(` ${type} ${entry.path}`);
}
// 示例4:批量文件操作
console.log(`
[4] 批量操作(高效批处理):
`);
const bulkCreate = (files: Array<{ name: string; content: string }>) =>
Effect.gen(function* () {
yield* Effect.log(`[批量] 创建 ${files.length} 个文件...`);
for (const file of files) {
yield* atomicWrite(`./${file.name}`, file.content);
}
yield* Effect.log(`[批量] 创建了 ${files.length} 个文件`);
});
const testFiles = [
{ name: "config1.txt", content: "配置 1" },
{ name: "config2.txt", content: "配置 2" },
{ name: "config3.txt", content: "配置 3" },
];
yield* bulkCreate(testFiles);
// 示例5:文件监视(检测更改)
console.log(`
[5] 文件监视(响应更改):
`);
const watchFile = (filePath: string) =>
Effect.gen(function* () {
yield* Effect.log(`[监视] 开始监视:${filePath}`);
let changeCount = 0;
// 模拟文件监视器
const checkForChanges = () =>
Effect.gen(function* () {
for (let i = 0; i < 3; i++) {
yield* Effect.sleep("100 毫秒");
// 检查文件修改时间
const stat = yield* Effect.promise(() =>
PromiseFS.stat(filePath)
);
// 在实际实现中,比较先前的mtime
if (i === 1) {
changeCount++;
yield* Effect.log(
`[更改] 文件修改(${stat.size} 字节)`
);
}
}
});
yield* checkForChanges();
yield* Effect.log(`[监视] 检测到 ${changeCount} 次更改`);
});
yield* watchFile(testFile);
// 示例6:安全的并发文件操作
console.log(`
[6] 带安全性的并发文件操作:
`);
const lockFile = (filePath: string) =>
Effect.gen(function* () {
const lockPath = `${filePath}.lock`;
// 获取锁
yield* atomicWrite(lockPath, "已锁定");
yield* Effect.log(`[锁] 已获取:${lockPath}`);
try {
// 临界区
yield* Effect.sleep("50 毫秒");
yield* Effect.log(`[临界] 在锁定文件上操作`);
} finally {
// 释放锁
yield* Effect.promise(() =>
PromiseFS.unlink(lockPath)
);
yield* Effect.log(`[解锁] 已释放:${lockPath}`);
}
});
yield* lockFile(testFile);
// 示例7:高效文件复制
console.log(`
[7] 高效文件复制:
`);
const efficientCopy = (
source: string,
destination: string
): Effect.Effect<void> =>
Effect.gen(function* () {
const stat = yield* Effect.promise(() =>
PromiseFS.stat(source)
);
yield* Effect.log(
`[复制] 读取 ${(stat.size / 1024).toFixed(2)}KB`
);
const content = yield* Effect.promise(() =>
PromiseFS.readFile(source)
);
yield* atomicWrite(destination, content.toString());
yield* Effect.log(`[复制] 完成:${destination}`);
});
yield* efficientCopy(testFile, "./test-file-copy.txt");
// 清理
yield* Effect.log(`
[清理] 删除测试文件`);
for (const name of [testFile, "test-file-copy.txt", ...testFiles.map((f) => `./${f.name}`)]) {
try {
yield* Effect.promise(() =>
PromiseFS.unlink(name)
);
yield* Effect.log(`[已删除] ${name}`);
} catch {
// 文件不存在,没问题
}
}
});
Effect.runPromise(program);
原理:
高级文件系统操作需要小心处理:
- 原子写入:防止部分文件损坏
- 文件监视:响应文件更改
- 递归操作:处理目录树
- 批量操作:高效批处理
- 流式:处理大文件而不加载所有到内存
- 权限:安全处理访问控制
模式:结合 FileSystem API 与 Ref 用于状态,Stream 用于数据
简单文件操作在规模上会导致问题:
问题1:损坏的文件
- 写入配置文件
- 服务器在写入中间崩溃
- 文件是部分/损坏的
- 应用程序无法启动
- 生产中断
问题2:大文件处理
- 加载10GB文件到内存
- 服务器内存不足
- 一切崩溃
- 现在处理中断而不是服务
问题3:目录同步
- 复制目录树
- 进程中断
- 一些文件复制,一些没有
- 目录处于不一致状态
- 难以恢复
问题4:低效更新
- 逐个更新10,000个文件
- 每个文件系统调用都很慢
- 需要数小时
- 同时,用户无法访问数据
问题5:文件锁定
- 进程A读取文件
- 进程B写入文件
- 进程A获取部分写入的文件
- 数据损坏
解决方案:
原子写入:
- 写入到临时文件
- Fsync(保证在磁盘上)
- 原子重命名
- 即使崩溃也无损坏
流式:
- 以大块处理大文件
- 保持内存恒定
- 对任何文件大小都高效
批量操作:
- 批处理多个操作
- 减少系统调用
- 整体完成更快
文件监视:
- 响应更改
- 避免轮询
- 实时响应