Effect-TS平台模式Skill effect-patterns-platform

这个技能提供了Effect-TS库中用于平台开发的6个精选模式,帮助开发者在构建CLI应用程序、处理文件系统、实现持久化存储和执行Shell命令时遵循最佳实践。包括交互式终端I/O、文件系统操作、键值存储、执行命令、跨平台路径操作和高级文件系统操作。适用于TypeScript后端开发、跨平台应用和函数式编程场景,关键词包括Effect-TS、平台模式、终端I/O、文件系统、键值存储、Shell命令、路径操作、TypeScript、函数式编程、后端开发。

后端开发 0 次安装 0 次浏览 更新于 3/8/2026

名称: 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原生
  • 跨平台:处理路径分隔符

真实世界示例:处理日志文件

  • 直接:打开文件、读取、关闭、手动处理异常
  • 使用FileSystemFileSystem.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(保证在磁盘上)
  • 原子重命名
  • 即使崩溃也无损坏

流式

  • 以大块处理大文件
  • 保持内存恒定
  • 对任何文件大小都高效

批量操作

  • 批处理多个操作
  • 减少系统调用
  • 整体完成更快

文件监视

  • 响应更改
  • 避免轮询
  • 实时响应