{"version":3,"sources":["../../../../src/batch-settlement/server/fileStorage.ts"],"sourcesContent":["import { mkdir, open, readdir, readFile, unlink } from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nimport { isNodeEnoent, readJsonFile, writeJsonAtomic } from \"../storage-utils\";\nimport type { FileChannelStorageOptions } from \"../types\";\nimport type { ChannelStorage, Channel, ChannelUpdateResult } from \"./storage\";\n\nexport type { FileChannelStorageOptions };\n\n/**\n * Node.js file-backed {@link ChannelStorage} for the batched server scheme.\n */\nexport class FileChannelStorage implements ChannelStorage {\n  private readonly root: string;\n\n  /**\n   * Creates file-backed server channel storage under the given root directory.\n   *\n   * @param options - Configuration including the storage root directory.\n   */\n  constructor(options: FileChannelStorageOptions) {\n    this.root = options.directory;\n  }\n\n  /**\n   * Loads a persisted channel record, if present.\n   *\n   * @param channelId - The channel identifier (path segment is lowercased).\n   * @returns Parsed channel record or `undefined` when the file is missing.\n   */\n  async get(channelId: string): Promise<Channel | undefined> {\n    return readJsonFile<Channel>(this.filePath(channelId));\n  }\n\n  /**\n   * Lists all stored channel records by reading the server directory.\n   *\n   * @returns Channel records sorted by channelId; empty array if the directory is missing.\n   */\n  async list(): Promise<Channel[]> {\n    const dir = join(this.root, \"server\");\n    let names: string[];\n    try {\n      names = await readdir(dir);\n    } catch (err: unknown) {\n      if (isNodeEnoent(err)) return [];\n      throw err;\n    }\n\n    const channels: Channel[] = [];\n    for (const name of names) {\n      if (!name.endsWith(\".json\")) continue;\n      const path = join(dir, name);\n      try {\n        const raw = await readFile(path, \"utf8\");\n        channels.push(JSON.parse(raw) as Channel);\n      } catch (err: unknown) {\n        // Skip files that disappeared between readdir and readFile (e.g. concurrent delete).\n        // Rethrow other failures (corrupt JSON, permission denied) so callers see them.\n        if (isNodeEnoent(err)) continue;\n        throw err;\n      }\n    }\n    return channels.sort((a, b) => a.channelId.localeCompare(b.channelId));\n  }\n\n  /**\n   * Atomically inspects and mutates a channel record under a cross-process file lock.\n   *\n   * @param channelId - The channel identifier.\n   * @param update - Mutation callback. Return `undefined` to delete, or `current` to leave unchanged.\n   * @returns The final stored channel and whether storage updated, stayed unchanged, or deleted.\n   */\n  async updateChannel(\n    channelId: string,\n    update: (current: Channel | undefined) => Channel | undefined,\n  ): Promise<ChannelUpdateResult> {\n    const lockPath = this.filePath(channelId) + \".lock\";\n    await mkdir(dirname(lockPath), { recursive: true });\n    const lockHandle = await this.acquireLock(lockPath);\n\n    try {\n      const path = this.filePath(channelId);\n      let current: Channel | undefined;\n      try {\n        const raw = await readFile(path, \"utf8\");\n        current = JSON.parse(raw) as Channel;\n      } catch (err: unknown) {\n        if (!isNodeEnoent(err)) throw err;\n      }\n\n      const next = update(current);\n      if (next === current) {\n        return { channel: current, status: \"unchanged\" };\n      }\n\n      if (!next) {\n        try {\n          await unlink(path);\n        } catch (err: unknown) {\n          if (!isNodeEnoent(err)) throw err;\n        }\n        return { channel: undefined, status: current ? \"deleted\" : \"unchanged\" };\n      }\n\n      await writeJsonAtomic(path, next);\n      return { channel: next, status: \"updated\" };\n    } finally {\n      await lockHandle.close();\n      await unlink(lockPath).catch(() => {});\n    }\n  }\n\n  /**\n   * Absolute path to the JSON file for a channel.\n   *\n   * @param channelId - The channel identifier.\n   * @returns Filesystem path under `{root}/server/...`.\n   */\n  private filePath(channelId: string): string {\n    return join(this.root, \"server\", `${channelId.toLowerCase()}.json`);\n  }\n\n  /**\n   * Creates an exclusive lock file, polling until no other process holds it.\n   *\n   * @param lockPath - Absolute path for the lock file (created with `O_EXCL`).\n   * @returns Writable file handle for the lock file; caller must close it to release.\n   */\n  private async acquireLock(lockPath: string) {\n    while (true) {\n      try {\n        return await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);\n      } catch (err: unknown) {\n        if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") {\n          throw err;\n        }\n        await new Promise(resolve => setTimeout(resolve, 10));\n      }\n    }\n  }\n}\n"],"mappings":"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","names":[]}