{"version":3,"file":"contentWatcherLock.mjs","names":[],"sources":["../../../src/utils/contentWatcherLock.ts"],"sourcesContent":["import { rmSync } from 'node:fs';\nimport { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/**\n * What kind of process is watching the content declarations.\n *\n * - `cli` — the `intlayer watch` / `intlayer build --watch` command.\n * - `bundler` — an Intlayer bundler plugin (e.g. `withIntlayer` on Next.js).\n */\nexport type ContentWatcherSource = 'cli' | 'bundler';\n\nexport type ContentWatcherOwner = {\n  /** PID of the process holding the watcher. */\n  pid: number;\n  source: ContentWatcherSource;\n  /**\n   * How the owner should be named in a message, e.g. `intlayer watch` or\n   * `next-intlayer`.\n   */\n  label: string;\n};\n\n/**\n * Set on every process spawned by `intlayer watch --with` (and\n * `intlayer build --watch --with`), so a bundler plugin running as that child\n * knows a CLI watcher is already covering the project — without having to\n * depend on the two processes reaching the lock file in a given order.\n *\n * Holds the label of the command that set it.\n */\nexport const CLI_CONTENT_WATCHER_ENV_VAR = 'INTLAYER_CLI_CONTENT_WATCHER';\n\n/** Name of the file materialising the ownership of the content watcher. */\nconst CONTENT_WATCHER_LOCK_FILE_NAME = 'intlayer-content-watcher.lock';\n\n/** How many times taking the lock may follow up on a reclaimed stale one. */\nconst ACQUIRE_ATTEMPTS = 3;\n\n/** Lock owned by this process, released on exit. */\nlet ownedLockFilePath: string | undefined;\n\n// The one place that has to stay synchronous: Node runs `exit` listeners to\n// completion without an event loop turn, so a promise scheduled here would\n// never settle and the lock would outlive the process — stalling the next dev\n// session until its PID check reclaims it.\nprocess.on('exit', () => {\n  if (!ownedLockFilePath) return;\n\n  try {\n    rmSync(ownedLockFilePath, { force: true });\n  } catch {}\n});\n\n/**\n * Path of the lock coordinating every content watcher of one project.\n *\n * Deliberately at the root of `.intlayer` rather than in its `cache`\n * subdirectory: `cleanOutputDir` wipes every subdirectory of `.intlayer`, so a\n * lock kept in `cache` would be erased by any `prepareIntlayer` that cleans —\n * after which a bundler plugin would see no owner and start a second watcher\n * next to the `intlayer watch` still running. The root itself is never removed.\n *\n * @param configuration - The resolved Intlayer configuration.\n */\nexport const getContentWatcherLockFilePath = (\n  configuration: IntlayerConfig\n): string => join(configuration.system.tempDir, CONTENT_WATCHER_LOCK_FILE_NAME);\n\n/**\n * Whether a PID belongs to a process that is still running.\n */\nconst getIsProcessAlive = (pid: number): boolean => {\n  if (pid === process.pid) return true;\n\n  try {\n    // Signal 0 checks for existence without delivering a signal.\n    process.kill(pid, 0);\n    return true;\n  } catch (error) {\n    // EPERM means the process exists but belongs to another user.\n    return (error as NodeJS.ErrnoException).code === 'EPERM';\n  }\n};\n\n/**\n * Reads the lock, removing it when it describes a process that has since died.\n *\n * An unreadable or malformed lock is treated as abandoned: a corrupted file\n * must never block the watcher for a whole dev session.\n *\n * @returns The live owner, or `null` when nothing is watching.\n */\nexport const getContentWatcherOwner = async (\n  configuration: IntlayerConfig\n): Promise<ContentWatcherOwner | null> => {\n  const lockFilePath = getContentWatcherLockFilePath(configuration);\n\n  let owner: Partial<ContentWatcherOwner>;\n\n  try {\n    owner = JSON.parse(await readFile(lockFilePath, 'utf8'));\n  } catch {\n    return null;\n  }\n\n  if (owner.pid && getIsProcessAlive(owner.pid)) {\n    return {\n      pid: owner.pid,\n      source: owner.source ?? 'bundler',\n      label: owner.label ?? 'Intlayer',\n    };\n  }\n\n  try {\n    await rm(lockFilePath, { force: true });\n  } catch {}\n\n  return null;\n};\n\n/**\n * Tries to become the process that watches this project's content\n * declarations.\n *\n * Creating the file with `wx` is atomic, so exactly one process wins even when\n * several try at the same moment — which is the normal case on Next.js, where\n * `next.config.*` is evaluated in more than one process.\n *\n * @param configuration - The resolved Intlayer configuration.\n * @param owner - How this process should describe itself to the others.\n * @returns `true` when this process now owns the watcher.\n */\nexport const acquireContentWatcherLock = async (\n  configuration: IntlayerConfig,\n  owner: Omit<ContentWatcherOwner, 'pid'>\n): Promise<boolean> => {\n  const lockFilePath = getContentWatcherLockFilePath(configuration);\n  const data = JSON.stringify({\n    pid: process.pid,\n    ...owner,\n  } satisfies ContentWatcherOwner);\n\n  // Bounded rather than recursive: the retry only exists to follow up on a lock\n  // `getContentWatcherOwner` has just reclaimed, and two processes reclaiming\n  // and re-taking it in turn must not spin forever.\n  for (let attempt = 0; attempt < ACQUIRE_ATTEMPTS; attempt++) {\n    try {\n      await mkdir(dirname(lockFilePath), { recursive: true });\n      await writeFile(lockFilePath, data, { flag: 'wx' });\n\n      ownedLockFilePath = lockFilePath;\n\n      return true;\n    } catch (error) {\n      if ((error as NodeJS.ErrnoException).code !== 'EEXIST') return false;\n\n      // A lock left behind by a dead process is reclaimed by\n      // `getContentWatcherOwner`, leaving the next attempt free to take it.\n      if (await getContentWatcherOwner(configuration)) return false;\n    }\n  }\n\n  return false;\n};\n\n/**\n * The CLI watcher command this process was spawned by, or `null` when it was\n * not started through `intlayer watch --with`.\n */\nexport const getCliContentWatcherLabel = (): string | null =>\n  process.env[CLI_CONTENT_WATCHER_ENV_VAR] || null;\n\n/**\n * Registers a CLI command as the content watcher of this project.\n *\n * Call it *before* spawning a `--with` child: taking the lock first means the\n * bundler plugin inside that child never races the command for it, and the\n * environment marker makes the child's decision independent of the filesystem\n * altogether.\n *\n * @param configuration - The resolved Intlayer configuration.\n * @param label - How the command should be named in a message.\n * @returns The bundler already watching this project, or `null` when the\n * command took the watcher for itself. A CLI watcher never stands down on that\n * answer — watching is what it was asked to do — but reporting it lets the user\n * know the parallel command has become redundant.\n */\nexport const claimCliContentWatcher = async (\n  configuration: IntlayerConfig,\n  label: string\n): Promise<ContentWatcherOwner | null> => {\n  // Set before the first `await`: a `--with` child spawned while the lock is\n  // still being written must already see the marker in its environment.\n  process.env[CLI_CONTENT_WATCHER_ENV_VAR] = label;\n\n  const hasAcquiredLock = await acquireContentWatcherLock(configuration, {\n    source: 'cli',\n    label,\n  });\n\n  if (hasAcquiredLock) return null;\n\n  const owner = await getContentWatcherOwner(configuration);\n\n  return owner?.source === 'bundler' ? owner : null;\n};\n\n/** Keeps the redundant-watcher notice to one per process. */\nlet hasReportedRedundantWatcher = false;\n\ntype RedundantWatcherReport = {\n  /** Name of the CLI command watching, e.g. `intlayer watch`. */\n  cliLabel: string;\n  /** Name of the bundler integration watching, e.g. `next-intlayer`. */\n  bundlerLabel: string;\n};\n\n/**\n * Reports a CLI watcher running alongside a bundler integration that watches\n * on its own, and points at the parallel command as the part to drop.\n *\n * Emitted at most once per process: a bundler config is evaluated several times\n * per command, and callers may re-enter this on a retry loop.\n *\n * @param configuration - The resolved Intlayer configuration.\n * @param report - Who is watching in parallel with whom.\n */\nexport const reportRedundantContentWatcher = (\n  configuration: IntlayerConfig,\n  { cliLabel, bundlerLabel }: RedundantWatcherReport\n): void => {\n  if (hasReportedRedundantWatcher) return;\n  hasReportedRedundantWatcher = true;\n\n  getAppLogger(configuration)(\n    [\n      colorize(cliLabel, ANSIColors.BLUE, ANSIColors.BEIGE),\n      'is watching your content declarations in parallel with',\n      `${colorize(bundlerLabel, ANSIColors.BLUE, ANSIColors.BEIGE)},`,\n      'which watches them on its own.',\n      'The parallel watch command is no longer needed — running your dev server on its own is enough.',\n    ],\n    { level: 'warn' }\n  );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAkCA,MAAa,8BAA8B;;AAG3C,MAAM,iCAAiC;;AAGvC,MAAM,mBAAmB;;AAGzB,IAAI;AAMJ,QAAQ,GAAG,cAAc;CACvB,IAAI,CAAC,mBAAmB;CAExB,IAAI;EACF,OAAO,mBAAmB,EAAE,OAAO,KAAK,CAAC;CAC3C,QAAQ,CAAC;AACX,CAAC;;;;;;;;;;;;AAaD,MAAa,iCACX,kBACW,KAAK,cAAc,OAAO,SAAS,8BAA8B;;;;AAK9E,MAAM,qBAAqB,QAAyB;CAClD,IAAI,QAAQ,QAAQ,KAAK,OAAO;CAEhC,IAAI;EAEF,QAAQ,KAAK,KAAK,CAAC;EACnB,OAAO;CACT,SAAS,OAAO;EAEd,OAAQ,MAAgC,SAAS;CACnD;AACF;;;;;;;;;AAUA,MAAa,yBAAyB,OACpC,kBACwC;CACxC,MAAM,eAAe,8BAA8B,aAAa;CAEhE,IAAI;CAEJ,IAAI;EACF,QAAQ,KAAK,MAAM,MAAM,SAAS,cAAc,MAAM,CAAC;CACzD,QAAQ;EACN,OAAO;CACT;CAEA,IAAI,MAAM,OAAO,kBAAkB,MAAM,GAAG,GAC1C,OAAO;EACL,KAAK,MAAM;EACX,QAAQ,MAAM,UAAU;EACxB,OAAO,MAAM,SAAS;CACxB;CAGF,IAAI;EACF,MAAM,GAAG,cAAc,EAAE,OAAO,KAAK,CAAC;CACxC,QAAQ,CAAC;CAET,OAAO;AACT;;;;;;;;;;;;;AAcA,MAAa,4BAA4B,OACvC,eACA,UACqB;CACrB,MAAM,eAAe,8BAA8B,aAAa;CAChE,MAAM,OAAO,KAAK,UAAU;EAC1B,KAAK,QAAQ;EACb,GAAG;CACL,CAA+B;CAK/B,KAAK,IAAI,UAAU,GAAG,UAAU,kBAAkB,WAChD,IAAI;EACF,MAAM,MAAM,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;EACtD,MAAM,UAAU,cAAc,MAAM,EAAE,MAAM,KAAK,CAAC;EAElD,oBAAoB;EAEpB,OAAO;CACT,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO;EAI/D,IAAI,MAAM,uBAAuB,aAAa,GAAG,OAAO;CAC1D;CAGF,OAAO;AACT;;;;;AAMA,MAAa,kCACX,QAAQ,uCAAoC;;;;;;;;;;;;;;;;AAiB9C,MAAa,yBAAyB,OACpC,eACA,UACwC;CAGxC,QAAQ,IAAI,+BAA+B;CAO3C,IAAI,MAL0B,0BAA0B,eAAe;EACrE,QAAQ;EACR;CACF,CAAC,GAEoB,OAAO;CAE5B,MAAM,QAAQ,MAAM,uBAAuB,aAAa;CAExD,OAAO,OAAO,WAAW,YAAY,QAAQ;AAC/C;;AAGA,IAAI,8BAA8B;;;;;;;;;;;AAmBlC,MAAa,iCACX,eACA,EAAE,UAAU,mBACH;CACT,IAAI,6BAA6B;CACjC,8BAA8B;CAE9B,aAAa,aAAa,CAAC,CACzB;EACE,SAAS,UAAU,WAAW,MAAM,WAAW,KAAK;EACpD;EACA,GAAG,SAAS,cAAc,WAAW,MAAM,WAAW,KAAK,EAAE;EAC7D;EACA;CACF,GACA,EAAE,OAAO,OAAO,CAClB;AACF"}