@intlayer/engine 9.1.3 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/assets/initConfig/templates/cjs.txt +4 -1
  2. package/dist/assets/initConfig/templates/json.txt +4 -1
  3. package/dist/assets/initConfig/templates/mjs.txt +4 -1
  4. package/dist/assets/initConfig/templates/ts.txt +4 -1
  5. package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs +14 -2
  6. package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs.map +1 -1
  7. package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs +24 -4
  8. package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs.map +1 -1
  9. package/dist/cjs/buildIntlayerDictionary/writeFetchDictionary.cjs +43 -6
  10. package/dist/cjs/buildIntlayerDictionary/writeFetchDictionary.cjs.map +1 -1
  11. package/dist/cjs/buildIntlayerDictionary/writeNestedDictionary.cjs +96 -0
  12. package/dist/cjs/buildIntlayerDictionary/writeNestedDictionary.cjs.map +1 -0
  13. package/dist/cjs/docReview/alignBlocks.cjs +57 -8
  14. package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
  15. package/dist/cjs/docReview/rebuildDocument.cjs +37 -9
  16. package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
  17. package/dist/cjs/docReview/segmentDocument.cjs +19 -2
  18. package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
  19. package/dist/cjs/fetchDistantDictionaries.cjs +107 -13
  20. package/dist/cjs/fetchDistantDictionaries.cjs.map +1 -1
  21. package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs +1 -1
  22. package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs.map +1 -1
  23. package/dist/cjs/loadDictionaries/loadDictionaries.cjs +4 -2
  24. package/dist/cjs/loadDictionaries/loadDictionaries.cjs.map +1 -1
  25. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +3 -2
  26. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
  27. package/dist/cjs/loadDictionaries/log.cjs +28 -5
  28. package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
  29. package/dist/cjs/prepareIntlayer.cjs +7 -3
  30. package/dist/cjs/prepareIntlayer.cjs.map +1 -1
  31. package/dist/cjs/utils/chunkArray.cjs +24 -0
  32. package/dist/cjs/utils/chunkArray.cjs.map +1 -0
  33. package/dist/cjs/utils/index.cjs +2 -0
  34. package/dist/cjs/utils/runOnce.cjs +129 -50
  35. package/dist/cjs/utils/runOnce.cjs.map +1 -1
  36. package/dist/cjs/writeContentDeclaration/transformJSFile.cjs +153 -92
  37. package/dist/cjs/writeContentDeclaration/transformJSFile.cjs.map +1 -1
  38. package/dist/cjs/writeFileIfChanged.cjs +14 -7
  39. package/dist/cjs/writeFileIfChanged.cjs.map +1 -1
  40. package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs +15 -3
  41. package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs.map +1 -1
  42. package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs +24 -4
  43. package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs.map +1 -1
  44. package/dist/esm/buildIntlayerDictionary/writeFetchDictionary.mjs +43 -6
  45. package/dist/esm/buildIntlayerDictionary/writeFetchDictionary.mjs.map +1 -1
  46. package/dist/esm/buildIntlayerDictionary/writeNestedDictionary.mjs +92 -0
  47. package/dist/esm/buildIntlayerDictionary/writeNestedDictionary.mjs.map +1 -0
  48. package/dist/esm/docReview/alignBlocks.mjs +57 -8
  49. package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
  50. package/dist/esm/docReview/rebuildDocument.mjs +37 -9
  51. package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
  52. package/dist/esm/docReview/segmentDocument.mjs +19 -2
  53. package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
  54. package/dist/esm/fetchDistantDictionaries.mjs +107 -13
  55. package/dist/esm/fetchDistantDictionaries.mjs.map +1 -1
  56. package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs +1 -1
  57. package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs.map +1 -1
  58. package/dist/esm/loadDictionaries/loadDictionaries.mjs +4 -2
  59. package/dist/esm/loadDictionaries/loadDictionaries.mjs.map +1 -1
  60. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +3 -2
  61. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
  62. package/dist/esm/loadDictionaries/log.mjs +28 -5
  63. package/dist/esm/loadDictionaries/log.mjs.map +1 -1
  64. package/dist/esm/prepareIntlayer.mjs +7 -3
  65. package/dist/esm/prepareIntlayer.mjs.map +1 -1
  66. package/dist/esm/utils/chunkArray.mjs +22 -0
  67. package/dist/esm/utils/chunkArray.mjs.map +1 -0
  68. package/dist/esm/utils/index.mjs +2 -1
  69. package/dist/esm/utils/runOnce.mjs +129 -50
  70. package/dist/esm/utils/runOnce.mjs.map +1 -1
  71. package/dist/esm/writeContentDeclaration/transformJSFile.mjs +153 -92
  72. package/dist/esm/writeContentDeclaration/transformJSFile.mjs.map +1 -1
  73. package/dist/esm/writeFileIfChanged.mjs +15 -8
  74. package/dist/esm/writeFileIfChanged.mjs.map +1 -1
  75. package/dist/types/buildIntlayerDictionary/buildIntlayerDictionary.d.ts.map +1 -1
  76. package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts +14 -2
  77. package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts.map +1 -1
  78. package/dist/types/buildIntlayerDictionary/writeFetchDictionary.d.ts +16 -2
  79. package/dist/types/buildIntlayerDictionary/writeFetchDictionary.d.ts.map +1 -1
  80. package/dist/types/buildIntlayerDictionary/writeNestedDictionary.d.ts +54 -0
  81. package/dist/types/buildIntlayerDictionary/writeNestedDictionary.d.ts.map +1 -0
  82. package/dist/types/docReview/alignBlocks.d.ts +4 -2
  83. package/dist/types/docReview/alignBlocks.d.ts.map +1 -1
  84. package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
  85. package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
  86. package/dist/types/docReview/types.d.ts +9 -0
  87. package/dist/types/docReview/types.d.ts.map +1 -1
  88. package/dist/types/fetchDistantDictionaries.d.ts +26 -0
  89. package/dist/types/fetchDistantDictionaries.d.ts.map +1 -1
  90. package/dist/types/formatDictionary.d.ts +2 -0
  91. package/dist/types/formatDictionary.d.ts.map +1 -1
  92. package/dist/types/loadDictionaries/log.d.ts +16 -1
  93. package/dist/types/loadDictionaries/log.d.ts.map +1 -1
  94. package/dist/types/utils/chunkArray.d.ts +16 -0
  95. package/dist/types/utils/chunkArray.d.ts.map +1 -0
  96. package/dist/types/utils/index.d.ts +3 -2
  97. package/dist/types/utils/runOnce.d.ts +30 -6
  98. package/dist/types/utils/runOnce.d.ts.map +1 -1
  99. package/dist/types/writeContentDeclaration/transformJSFile.d.ts.map +1 -1
  100. package/package.json +11 -11
@@ -1 +1 @@
1
- {"version":3,"file":"loadRemoteDictionaries.mjs","names":[],"sources":["../../../src/loadDictionaries/loadRemoteDictionaries.ts"],"sourcesContent":["import { createIntlayerCMS } from '@intlayer/api';\nimport { dictionaryEndpoint } from '@intlayer/api/dictionary';\n// @ts-ignore @intlayer/backend is not build yet\nimport type { DictionaryAPI } from '@intlayer/backend';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getRemoteDictionaries } from '@intlayer/remote-dictionaries-entry';\nimport type {\n Dictionary,\n DictionaryId,\n DictionaryKey,\n} from '@intlayer/types/dictionary';\nimport { fetchDistantDictionaries } from '../fetchDistantDictionaries';\nimport { sortAlphabetically } from '../utils/sortAlphabetically';\nimport type { DictionariesStatus } from './loadDictionaries';\n\nexport const formatDistantDictionaries = (\n dictionaries: (DictionaryAPI | Dictionary)[]\n): Dictionary[] =>\n dictionaries.map((dict) => ({\n ...dict,\n localId: `${dict.key}::remote::${dict.id}`,\n location: 'remote' as const,\n }));\n\nexport const loadRemoteDictionaries = async (\n configuration = getConfiguration(),\n onStatusUpdate?: (status: DictionariesStatus[]) => void,\n options?: {\n onStartRemoteCheck?: () => void;\n onStopRemoteCheck?: () => void;\n onError?: (error: Error) => void;\n }\n): Promise<Dictionary[]> => {\n const { editor } = configuration;\n const remoteDictionariesRecord = getRemoteDictionaries(configuration);\n\n const hasRemoteDictionaries = Boolean(editor.clientId && editor.clientSecret);\n\n if (!hasRemoteDictionaries) return [];\n\n try {\n options?.onStartRemoteCheck?.();\n\n const dictionary = dictionaryEndpoint(createIntlayerCMS(configuration));\n\n // Get the list of dictionary keys\n const getDictionariesKeysResult =\n await dictionary.getDictionariesUpdateTimestamp();\n\n const distantDictionaryUpdateTimeStamp: Record<\n DictionaryId,\n { key: DictionaryKey; updatedAt: number }\n > | null = getDictionariesKeysResult.data;\n\n if (!distantDictionaryUpdateTimeStamp) {\n throw new Error('No distant dictionaries found');\n }\n\n const dictionariesIdToFetch = Object.entries(\n distantDictionaryUpdateTimeStamp\n ).filter(([dictionaryId, data]) => {\n // If remote doesn't provide updatedAt, fetch to be safe\n if (!data.updatedAt) return true;\n\n // If no local cache exists, fetch\n const local: Dictionary | undefined = remoteDictionariesRecord[\n data.key\n ]?.find((dictionary) => dictionary.id === dictionaryId);\n if (!local) return true;\n\n const localUpdatedAtRaw = (local as any)?.updatedAt as\n | number\n | string\n | undefined;\n\n const localUpdatedAt =\n typeof localUpdatedAtRaw === 'number'\n ? localUpdatedAtRaw\n : localUpdatedAtRaw\n ? new Date(localUpdatedAtRaw).getTime()\n : undefined;\n\n // If local timestamp missing or older than remote, fetch\n if (typeof localUpdatedAt !== 'number') return true;\n\n return data.updatedAt > localUpdatedAt;\n });\n\n const flatRemoteDictionariesRecord: DictionaryAPI[] = Object.values(\n remoteDictionariesRecord\n ).flat();\n\n const cachedDictionaries: Dictionary[] =\n flatRemoteDictionariesRecord.filter((dictionary) => {\n const remoteUpdatedAt =\n distantDictionaryUpdateTimeStamp[dictionary.id!].updatedAt;\n\n const localUpdatedAtRaw = dictionary.updatedAt;\n\n const localUpdatedAt =\n typeof localUpdatedAtRaw === 'number'\n ? localUpdatedAtRaw\n : localUpdatedAtRaw\n ? new Date(localUpdatedAtRaw).getTime()\n : undefined;\n\n // Consider as cached/imported when local exists and is up-to-date or newer\n return (\n typeof localUpdatedAt === 'number' &&\n typeof remoteUpdatedAt === 'number' &&\n localUpdatedAt >= remoteUpdatedAt\n );\n });\n\n // Report cached as already imported\n if (cachedDictionaries.length > 0) {\n onStatusUpdate?.(\n cachedDictionaries.map((dictionary) => ({\n dictionaryKey: dictionary.key,\n type: 'remote',\n status: 'imported',\n }))\n );\n }\n\n const orderedDistantDictionaryKeys = dictionariesIdToFetch\n .map(([, data]) => data.key)\n .sort(sortAlphabetically);\n\n // Report pending for keys to be fetched so totals are visible immediately\n if (orderedDistantDictionaryKeys.length > 0) {\n onStatusUpdate?.(\n orderedDistantDictionaryKeys.map((key) => ({\n dictionaryKey: key,\n type: 'remote',\n status: 'pending',\n }))\n );\n }\n\n const distantDictionariesData = await fetchDistantDictionaries(\n {\n dictionaryKeys: orderedDistantDictionaryKeys,\n },\n onStatusUpdate\n );\n\n const distantDictionaries: Dictionary[] = formatDistantDictionaries(\n distantDictionariesData\n );\n\n return [...cachedDictionaries, ...distantDictionaries];\n } catch (error) {\n options?.onError?.(error as Error);\n return [];\n } finally {\n options?.onStopRemoteCheck?.();\n }\n};\n"],"mappings":";;;;;;;;AAeA,MAAa,6BACX,iBAEA,aAAa,KAAK,UAAU;CAC1B,GAAG;CACH,SAAS,GAAG,KAAK,IAAI,YAAY,KAAK;CACtC,UAAU;AACZ,EAAE;AAEJ,MAAa,yBAAyB,OACpC,gBAAgB,iBAAiB,GACjC,gBACA,YAK0B;CAC1B,MAAM,EAAE,WAAW;CACnB,MAAM,2BAA2B,sBAAsB,aAAa;CAIpE,IAAI,CAF0B,QAAQ,OAAO,YAAY,OAAO,YAEvC,GAAG,OAAO,CAAC;CAEpC,IAAI;EACF,SAAS,qBAAqB;EAQ9B,MAAM,oCAGK,MATQ,mBAAmB,kBAAkB,aAAa,CAIpD,CAAC,CAAC,+BAA+B,EAKd,CAAC;EAErC,IAAI,CAAC,kCACH,MAAM,IAAI,MAAM,+BAA+B;EAGjD,MAAM,wBAAwB,OAAO,QACnC,gCACF,CAAC,CAAC,QAAQ,CAAC,cAAc,UAAU;GAEjC,IAAI,CAAC,KAAK,WAAW,OAAO;GAG5B,MAAM,QAAgC,yBACpC,KAAK,IACN,EAAE,MAAM,eAAe,WAAW,OAAO,YAAY;GACtD,IAAI,CAAC,OAAO,OAAO;GAEnB,MAAM,oBAAqB,OAAe;GAK1C,MAAM,iBACJ,OAAO,sBAAsB,WACzB,oBACA,oBACE,IAAI,KAAK,iBAAiB,CAAC,CAAC,QAAQ,IACpC;GAGR,IAAI,OAAO,mBAAmB,UAAU,OAAO;GAE/C,OAAO,KAAK,YAAY;EAC1B,CAAC;EAMD,MAAM,qBAJgD,OAAO,OAC3D,wBACF,CAAC,CAAC,KAG2B,CAAC,CAAC,QAAQ,eAAe;GAClD,MAAM,kBACJ,iCAAiC,WAAW,GAAI,CAAC;GAEnD,MAAM,oBAAoB,WAAW;GAErC,MAAM,iBACJ,OAAO,sBAAsB,WACzB,oBACA,oBACE,IAAI,KAAK,iBAAiB,CAAC,CAAC,QAAQ,IACpC;GAGR,OACE,OAAO,mBAAmB,YAC1B,OAAO,oBAAoB,YAC3B,kBAAkB;EAEtB,CAAC;EAGH,IAAI,mBAAmB,SAAS,GAC9B,iBACE,mBAAmB,KAAK,gBAAgB;GACtC,eAAe,WAAW;GAC1B,MAAM;GACN,QAAQ;EACV,EAAE,CACJ;EAGF,MAAM,+BAA+B,sBAClC,KAAK,GAAG,UAAU,KAAK,GAAG,CAAC,CAC3B,KAAK,kBAAkB;EAG1B,IAAI,6BAA6B,SAAS,GACxC,iBACE,6BAA6B,KAAK,SAAS;GACzC,eAAe;GACf,MAAM;GACN,QAAQ;EACV,EAAE,CACJ;EAGF,MAAM,0BAA0B,MAAM,yBACpC,EACE,gBAAgB,6BAClB,GACA,cACF;EAEA,MAAM,sBAAoC,0BACxC,uBACF;EAEA,OAAO,CAAC,GAAG,oBAAoB,GAAG,mBAAmB;CACvD,SAAS,OAAO;EACd,SAAS,UAAU,KAAc;EACjC,OAAO,CAAC;CACV,UAAU;EACR,SAAS,oBAAoB;CAC/B;AACF"}
1
+ {"version":3,"file":"loadRemoteDictionaries.mjs","names":[],"sources":["../../../src/loadDictionaries/loadRemoteDictionaries.ts"],"sourcesContent":["import { createIntlayerCMS } from '@intlayer/api';\nimport { dictionaryEndpoint } from '@intlayer/api/dictionary';\n// @ts-ignore @intlayer/backend is not build yet\nimport type { DictionaryAPI } from '@intlayer/backend';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getRemoteDictionaries } from '@intlayer/remote-dictionaries-entry';\nimport type {\n Dictionary,\n DictionaryId,\n DictionaryKey,\n} from '@intlayer/types/dictionary';\nimport { fetchDistantDictionaries } from '../fetchDistantDictionaries';\nimport { sortAlphabetically } from '../utils/sortAlphabetically';\nimport type { DictionariesStatus } from './loadDictionaries';\n\nexport const formatDistantDictionaries = (\n dictionaries: (DictionaryAPI | Dictionary)[]\n): Dictionary[] =>\n dictionaries.map((dict) => ({\n ...dict,\n localId: `${dict.key}::remote::${dict.id}`,\n location: 'remote' as const,\n }));\n\nexport const loadRemoteDictionaries = async (\n configuration = getConfiguration(),\n onStatusUpdate?: (status: DictionariesStatus[]) => void,\n options?: {\n onStartRemoteCheck?: () => void;\n onStopRemoteCheck?: () => void;\n onError?: (error: Error) => void;\n }\n): Promise<Dictionary[]> => {\n const { editor } = configuration;\n const remoteDictionariesRecord = getRemoteDictionaries(configuration);\n\n const hasRemoteDictionaries = Boolean(editor.clientId && editor.clientSecret);\n\n if (!hasRemoteDictionaries) return [];\n\n try {\n options?.onStartRemoteCheck?.();\n\n const dictionary = dictionaryEndpoint(createIntlayerCMS(configuration));\n\n // Get the list of dictionary keys\n const getDictionariesKeysResult =\n await dictionary.getDictionariesUpdateTimestamp();\n\n const distantDictionaryUpdateTimeStamp: Record<\n DictionaryId,\n { key: DictionaryKey; updatedAt: number }\n > | null = getDictionariesKeysResult.data;\n\n if (!distantDictionaryUpdateTimeStamp) {\n throw new Error('No distant dictionaries found');\n }\n\n const dictionariesIdToFetch = Object.entries(\n distantDictionaryUpdateTimeStamp\n ).filter(([dictionaryId, data]) => {\n // If remote doesn't provide updatedAt, fetch to be safe\n if (!data.updatedAt) return true;\n\n // If no local cache exists, fetch\n const local: Dictionary | undefined = remoteDictionariesRecord[\n data.key\n ]?.find((dictionary) => dictionary.id === dictionaryId);\n if (!local) return true;\n\n const localUpdatedAtRaw = (local as any)?.updatedAt as\n | number\n | string\n | undefined;\n\n const localUpdatedAt =\n typeof localUpdatedAtRaw === 'number'\n ? localUpdatedAtRaw\n : localUpdatedAtRaw\n ? new Date(localUpdatedAtRaw).getTime()\n : undefined;\n\n // If local timestamp missing or older than remote, fetch\n if (typeof localUpdatedAt !== 'number') return true;\n\n return data.updatedAt > localUpdatedAt;\n });\n\n const flatRemoteDictionariesRecord: DictionaryAPI[] = Object.values(\n remoteDictionariesRecord\n ).flat();\n\n const cachedDictionaries: Dictionary[] =\n flatRemoteDictionariesRecord.filter((dictionary) => {\n const remoteUpdatedAt =\n distantDictionaryUpdateTimeStamp[dictionary.id!].updatedAt;\n\n const localUpdatedAtRaw = dictionary.updatedAt;\n\n const localUpdatedAt =\n typeof localUpdatedAtRaw === 'number'\n ? localUpdatedAtRaw\n : localUpdatedAtRaw\n ? new Date(localUpdatedAtRaw).getTime()\n : undefined;\n\n // Consider as cached/imported when local exists and is up-to-date or newer\n return (\n typeof localUpdatedAt === 'number' &&\n typeof remoteUpdatedAt === 'number' &&\n localUpdatedAt >= remoteUpdatedAt\n );\n });\n\n // Report cached as already imported\n if (cachedDictionaries.length > 0) {\n onStatusUpdate?.(\n cachedDictionaries.map((dictionary) => ({\n dictionaryKey: dictionary.key,\n type: 'remote',\n status: 'imported',\n }))\n );\n }\n\n // Several qualified dictionaries can share a key, so keys are deduplicated:\n // one request per key already returns all of its siblings.\n const orderedDistantDictionaryKeys = [\n ...new Set(dictionariesIdToFetch.map(([, data]) => data.key)),\n ].sort(sortAlphabetically);\n\n // Report pending for keys to be fetched so totals are visible immediately\n if (orderedDistantDictionaryKeys.length > 0) {\n onStatusUpdate?.(\n orderedDistantDictionaryKeys.map((key) => ({\n dictionaryKey: key,\n type: 'remote',\n status: 'pending',\n }))\n );\n }\n\n const distantDictionariesData = await fetchDistantDictionaries(\n {\n dictionaryKeys: orderedDistantDictionaryKeys,\n },\n onStatusUpdate\n );\n\n const distantDictionaries: Dictionary[] = formatDistantDictionaries(\n distantDictionariesData\n );\n\n // Fetching by key also returns the up-to-date siblings of a stale\n // dictionary, so drop the cached copies the remote just sent back.\n const fetchedDictionaryIds = new Set(\n distantDictionaries.map((dictionary) => dictionary.id)\n );\n\n const notRefetchedCachedDictionaries = cachedDictionaries.filter(\n (dictionary) => !fetchedDictionaryIds.has(dictionary.id)\n );\n\n return [...notRefetchedCachedDictionaries, ...distantDictionaries];\n } catch (error) {\n options?.onError?.(error as Error);\n return [];\n } finally {\n options?.onStopRemoteCheck?.();\n }\n};\n"],"mappings":";;;;;;;;AAeA,MAAa,6BACX,iBAEA,aAAa,KAAK,UAAU;CAC1B,GAAG;CACH,SAAS,GAAG,KAAK,IAAI,YAAY,KAAK;CACtC,UAAU;AACZ,EAAE;AAEJ,MAAa,yBAAyB,OACpC,gBAAgB,iBAAiB,GACjC,gBACA,YAK0B;CAC1B,MAAM,EAAE,WAAW;CACnB,MAAM,2BAA2B,sBAAsB,aAAa;CAIpE,IAAI,CAF0B,QAAQ,OAAO,YAAY,OAAO,YAEvC,GAAG,OAAO,CAAC;CAEpC,IAAI;EACF,SAAS,qBAAqB;EAQ9B,MAAM,oCAGK,MATQ,mBAAmB,kBAAkB,aAAa,CAIpD,CAAC,CAAC,+BAA+B,EAKd,CAAC;EAErC,IAAI,CAAC,kCACH,MAAM,IAAI,MAAM,+BAA+B;EAGjD,MAAM,wBAAwB,OAAO,QACnC,gCACF,CAAC,CAAC,QAAQ,CAAC,cAAc,UAAU;GAEjC,IAAI,CAAC,KAAK,WAAW,OAAO;GAG5B,MAAM,QAAgC,yBACpC,KAAK,IACN,EAAE,MAAM,eAAe,WAAW,OAAO,YAAY;GACtD,IAAI,CAAC,OAAO,OAAO;GAEnB,MAAM,oBAAqB,OAAe;GAK1C,MAAM,iBACJ,OAAO,sBAAsB,WACzB,oBACA,oBACE,IAAI,KAAK,iBAAiB,CAAC,CAAC,QAAQ,IACpC;GAGR,IAAI,OAAO,mBAAmB,UAAU,OAAO;GAE/C,OAAO,KAAK,YAAY;EAC1B,CAAC;EAMD,MAAM,qBAJgD,OAAO,OAC3D,wBACF,CAAC,CAAC,KAG2B,CAAC,CAAC,QAAQ,eAAe;GAClD,MAAM,kBACJ,iCAAiC,WAAW,GAAI,CAAC;GAEnD,MAAM,oBAAoB,WAAW;GAErC,MAAM,iBACJ,OAAO,sBAAsB,WACzB,oBACA,oBACE,IAAI,KAAK,iBAAiB,CAAC,CAAC,QAAQ,IACpC;GAGR,OACE,OAAO,mBAAmB,YAC1B,OAAO,oBAAoB,YAC3B,kBAAkB;EAEtB,CAAC;EAGH,IAAI,mBAAmB,SAAS,GAC9B,iBACE,mBAAmB,KAAK,gBAAgB;GACtC,eAAe,WAAW;GAC1B,MAAM;GACN,QAAQ;EACV,EAAE,CACJ;EAKF,MAAM,+BAA+B,CACnC,GAAG,IAAI,IAAI,sBAAsB,KAAK,GAAG,UAAU,KAAK,GAAG,CAAC,CAC9D,CAAC,CAAC,KAAK,kBAAkB;EAGzB,IAAI,6BAA6B,SAAS,GACxC,iBACE,6BAA6B,KAAK,SAAS;GACzC,eAAe;GACf,MAAM;GACN,QAAQ;EACV,EAAE,CACJ;EAGF,MAAM,0BAA0B,MAAM,yBACpC,EACE,gBAAgB,6BAClB,GACA,cACF;EAEA,MAAM,sBAAoC,0BACxC,uBACF;EAIA,MAAM,uBAAuB,IAAI,IAC/B,oBAAoB,KAAK,eAAe,WAAW,EAAE,CACvD;EAMA,OAAO,CAAC,GAJ+B,mBAAmB,QACvD,eAAe,CAAC,qBAAqB,IAAI,WAAW,EAAE,CAGjB,GAAG,GAAG,mBAAmB;CACnE,SAAS,OAAO;EACd,SAAS,UAAU,KAAc;EACjC,OAAO,CAAC;CACV,UAAU;EACR,SAAS,oBAAoB;CAC/B;AACF"}
@@ -18,6 +18,7 @@ var DictionariesLogger = class {
18
18
  remoteError;
19
19
  pluginTotal = 0;
20
20
  pluginDone = 0;
21
+ pluginNames = [];
21
22
  pluginError;
22
23
  constructor() {
23
24
  this.prefix = getPrefix(log?.prefix) ?? "";
@@ -68,6 +69,18 @@ var DictionariesLogger = class {
68
69
  this.stopRemoteCheck();
69
70
  this.render();
70
71
  };
72
+ /**
73
+ * Register the names of the plugins providing dictionaries.
74
+ * They are displayed next to the plugin progress counters, e.g.
75
+ * `Plugin content: 6/6 (sync-json, load-po)`.
76
+ *
77
+ * @param names - Names of the plugins exposing a `loadDictionaries` hook
78
+ */
79
+ setPluginNames(names) {
80
+ if (this.isFinished) return;
81
+ this.pluginNames = names.filter((name) => Boolean(name));
82
+ this.render();
83
+ }
71
84
  setPluginTotal(total) {
72
85
  if (this.isFinished) return;
73
86
  this.pluginTotal = total;
@@ -79,9 +92,16 @@ var DictionariesLogger = class {
79
92
  this.pluginDone = done;
80
93
  this.render();
81
94
  }
82
- setPluginError(error) {
95
+ /**
96
+ * Flag the plugin line as errored.
97
+ *
98
+ * @param error - Error thrown while loading the plugin dictionaries
99
+ * @param pluginName - Name of the plugin that threw, prefixed to the message
100
+ */
101
+ setPluginError(error, pluginName) {
83
102
  if (this.isFinished) return;
84
- this.pluginError = extractErrorMessage(error);
103
+ const errorMessage = extractErrorMessage(error);
104
+ this.pluginError = pluginName ? `${pluginName}: ${errorMessage}` : errorMessage;
85
105
  this.render();
86
106
  }
87
107
  render() {
@@ -98,9 +118,12 @@ var DictionariesLogger = class {
98
118
  else if (remoteTotal === 0) lines.push(`${this.prefix} ${clock} Remote content: ${colorize("Check server", ANSIColors.BLUE)}`);
99
119
  else if (isRemoteDone) lines.push(`${this.prefix} ${v} Remote content: ${colorize(`${remoteDone}`, ANSIColors.GREEN)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`);
100
120
  else lines.push(`${this.prefix} ${clock} Remote content: ${colorize(`${remoteDone}`, ANSIColors.BLUE)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`);
101
- if (pluginTotal > 0 || this.pluginError) if (this.pluginError) lines.push(`${this.prefix} ${x} Plugin content: ${colorize(this.pluginError, ANSIColors.RED)}`);
102
- else if (isPluginDone) lines.push(`${this.prefix} ${v} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.GREEN)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}`);
103
- else lines.push(`${this.prefix} ${clock} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.BLUE)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}`);
121
+ if (pluginTotal > 0 || this.pluginError) {
122
+ const pluginNamesSuffix = this.pluginNames.length > 0 ? colorize(` (${this.pluginNames.join(", ")})`, ANSIColors.GREY) : "";
123
+ if (this.pluginError) lines.push(`${this.prefix} ${x} Plugin content: ${colorize(this.pluginError, ANSIColors.RED)}`);
124
+ else if (isPluginDone) lines.push(`${this.prefix} ${v} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.GREEN)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}${pluginNamesSuffix}`);
125
+ else lines.push(`${this.prefix} ${clock} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.BLUE)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}${pluginNamesSuffix}`);
126
+ }
104
127
  const currentState = lines.join("\n");
105
128
  if (currentState === this.lastRenderedState) return;
106
129
  this.lastRenderedState = currentState;
@@ -1 +1 @@
1
- {"version":3,"file":"log.mjs","names":[],"sources":["../../../src/loadDictionaries/log.ts"],"sourcesContent":["import { log } from '@intlayer/config/built';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n getPrefix,\n spinnerFrames,\n v,\n x,\n} from '@intlayer/config/logger';\nimport { extractErrorMessage } from '@intlayer/config/utils';\nimport type { DictionariesStatus } from './loadDictionaries';\n\nexport class DictionariesLogger {\n private statuses: DictionariesStatus[] = [];\n private spinnerTimer: NodeJS.Timeout | null = null;\n private spinnerIndex = 0;\n private renderedLines = 0;\n private readonly spinnerFrames = spinnerFrames;\n private isFinished = false;\n private readonly prefix: string;\n private lastRenderedState: string = '';\n private remoteCheckInProgress = false;\n private expectRemote = false;\n private remoteError: string | undefined;\n private pluginTotal = 0;\n private pluginDone = 0;\n private pluginError: string | undefined;\n\n constructor() {\n this.prefix = getPrefix(log?.prefix) ?? '';\n }\n\n setExpectRemote(expect: boolean) {\n this.expectRemote = expect;\n }\n\n startRemoteCheck() {\n if (this.isFinished) return;\n this.remoteCheckInProgress = true;\n this.startSpinner();\n this.render();\n }\n\n stopRemoteCheck() {\n this.remoteCheckInProgress = false;\n }\n\n update(newStatuses: DictionariesStatus[]) {\n if (this.isFinished) return;\n for (const status of newStatuses) {\n const index = this.statuses.findIndex(\n (s) =>\n s.dictionaryKey === status.dictionaryKey && s.type === status.type\n );\n if (index >= 0) {\n this.statuses[index] = status;\n } else {\n this.statuses.push(status);\n }\n }\n\n // If we expect remote fetch later, avoid rendering a local-only line first\n const { remoteTotal } = this.computeProgress();\n if (this.expectRemote && !this.remoteCheckInProgress && remoteTotal === 0) {\n // Do not start spinner or render yet; wait until remote check starts\n return;\n }\n\n this.startSpinner();\n this.render();\n }\n\n finish() {\n this.isFinished = true;\n this.stopSpinner();\n // Render final state and keep it visible\n this.render();\n }\n\n private startSpinner() {\n if (this.spinnerTimer || this.isFinished) return;\n this.spinnerTimer = setInterval(() => {\n this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;\n this.render();\n }, 100);\n }\n\n private stopSpinner() {\n if (!this.spinnerTimer) return;\n clearInterval(this.spinnerTimer);\n this.spinnerTimer = null;\n }\n\n public setRemoteError = (error?: Error) => {\n this.remoteError = extractErrorMessage(error);\n // Avoid rendering a transient remote-only line while the remote check flag is still true\n // Ensure local + remote are rendered together after a failure\n this.stopRemoteCheck();\n this.render();\n };\n\n setPluginTotal(total: number) {\n if (this.isFinished) return;\n this.pluginTotal = total;\n if (total > 0) {\n this.startSpinner();\n }\n this.render();\n }\n\n setPluginDone(done: number) {\n if (this.isFinished) return;\n this.pluginDone = done;\n this.render();\n }\n\n setPluginError(error?: Error) {\n if (this.isFinished) return;\n this.pluginError = extractErrorMessage(error);\n this.render();\n }\n\n private render() {\n const {\n localTotal,\n localDone,\n remoteTotal,\n remoteDone,\n pluginTotal,\n pluginDone,\n } = this.computeProgress();\n\n const frame = this.spinnerFrames[this.spinnerIndex];\n const clock = colorize(frame, ANSIColors.BLUE);\n const lines: string[] = [];\n\n const isLocalDone = localDone === localTotal;\n const isRemoteDone = remoteDone === remoteTotal;\n const isPluginDone = pluginDone === pluginTotal;\n\n const suppressLocalWhileCheckingRemote =\n this.expectRemote && this.remoteCheckInProgress && remoteTotal === 0;\n\n if (!suppressLocalWhileCheckingRemote) {\n if (isLocalDone) {\n lines.push(\n `${this.prefix} ${v} Local content: ${colorize(`${localDone}`, ANSIColors.GREEN)}${colorize(`/${localTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Local content: ${colorize(`${localDone}`, ANSIColors.BLUE)}${colorize(`/${localTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Single remote line: show error, check, or progress counts\n if (remoteTotal > 0 || this.remoteCheckInProgress || this.remoteError) {\n if (this.remoteError) {\n lines.push(\n `${this.prefix} ${x} Remote content: ${colorize(\n this.remoteError,\n ANSIColors.RED\n )}`\n );\n } else if (remoteTotal === 0) {\n lines.push(\n `${this.prefix} ${clock} Remote content: ${colorize('Check server', ANSIColors.BLUE)}`\n );\n } else if (isRemoteDone) {\n lines.push(\n `${this.prefix} ${v} Remote content: ${colorize(`${remoteDone}`, ANSIColors.GREEN)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Remote content: ${colorize(`${remoteDone}`, ANSIColors.BLUE)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Plugin line: show error or progress counts\n if (pluginTotal > 0 || this.pluginError) {\n if (this.pluginError) {\n lines.push(\n `${this.prefix} ${x} Plugin content: ${colorize(\n this.pluginError,\n ANSIColors.RED\n )}`\n );\n } else if (isPluginDone) {\n lines.push(\n `${this.prefix} ${v} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.GREEN)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.BLUE)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Check if the state has changed to avoid duplicate rendering\n const currentState = lines.join('\\n');\n if (currentState === this.lastRenderedState) {\n return;\n }\n this.lastRenderedState = currentState;\n\n if (this.renderedLines > 0) {\n process.stdout.write(`\\x1b[${this.renderedLines}F`);\n }\n\n const totalLinesToClear = Math.max(this.renderedLines, lines.length);\n for (let i = 0; i < totalLinesToClear; i++) {\n process.stdout.write('\\x1b[2K');\n const line = lines[i];\n if (line !== undefined) {\n process.stdout.write(line);\n }\n process.stdout.write('\\n');\n }\n\n this.renderedLines = lines.length;\n }\n\n private computeProgress() {\n const localKeys = new Set(\n this.statuses\n .filter((s) => s.type === 'local')\n .map((s) => s.dictionaryKey)\n );\n\n const localDoneKeys = new Set(\n this.statuses\n .filter(\n (s) =>\n s.type === 'local' && (s.status === 'built' || s.status === 'error')\n )\n .map((s) => s.dictionaryKey)\n );\n\n const remoteKeys = new Set(\n this.statuses\n .filter((s) => s.type === 'remote')\n .map((s) => s.dictionaryKey)\n );\n\n const remoteDoneKeys = new Set(\n this.statuses\n .filter(\n (s) =>\n s.type === 'remote' &&\n (s.status === 'fetched' ||\n s.status === 'imported' ||\n s.status === 'error')\n )\n .map((s) => s.dictionaryKey)\n );\n\n return {\n localTotal: localKeys.size,\n localDone: localDoneKeys.size,\n remoteTotal: remoteKeys.size,\n remoteDone: remoteDoneKeys.size,\n pluginTotal: this.pluginTotal,\n pluginDone: this.pluginDone,\n } as const;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"log.mjs","names":[],"sources":["../../../src/loadDictionaries/log.ts"],"sourcesContent":["import { log } from '@intlayer/config/built';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n getPrefix,\n spinnerFrames,\n v,\n x,\n} from '@intlayer/config/logger';\nimport { extractErrorMessage } from '@intlayer/config/utils';\nimport type { DictionariesStatus } from './loadDictionaries';\n\nexport class DictionariesLogger {\n private statuses: DictionariesStatus[] = [];\n private spinnerTimer: NodeJS.Timeout | null = null;\n private spinnerIndex = 0;\n private renderedLines = 0;\n private readonly spinnerFrames = spinnerFrames;\n private isFinished = false;\n private readonly prefix: string;\n private lastRenderedState: string = '';\n private remoteCheckInProgress = false;\n private expectRemote = false;\n private remoteError: string | undefined;\n private pluginTotal = 0;\n private pluginDone = 0;\n private pluginNames: string[] = [];\n private pluginError: string | undefined;\n\n constructor() {\n this.prefix = getPrefix(log?.prefix) ?? '';\n }\n\n setExpectRemote(expect: boolean) {\n this.expectRemote = expect;\n }\n\n startRemoteCheck() {\n if (this.isFinished) return;\n this.remoteCheckInProgress = true;\n this.startSpinner();\n this.render();\n }\n\n stopRemoteCheck() {\n this.remoteCheckInProgress = false;\n }\n\n update(newStatuses: DictionariesStatus[]) {\n if (this.isFinished) return;\n for (const status of newStatuses) {\n const index = this.statuses.findIndex(\n (s) =>\n s.dictionaryKey === status.dictionaryKey && s.type === status.type\n );\n if (index >= 0) {\n this.statuses[index] = status;\n } else {\n this.statuses.push(status);\n }\n }\n\n // If we expect remote fetch later, avoid rendering a local-only line first\n const { remoteTotal } = this.computeProgress();\n if (this.expectRemote && !this.remoteCheckInProgress && remoteTotal === 0) {\n // Do not start spinner or render yet; wait until remote check starts\n return;\n }\n\n this.startSpinner();\n this.render();\n }\n\n finish() {\n this.isFinished = true;\n this.stopSpinner();\n // Render final state and keep it visible\n this.render();\n }\n\n private startSpinner() {\n if (this.spinnerTimer || this.isFinished) return;\n this.spinnerTimer = setInterval(() => {\n this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;\n this.render();\n }, 100);\n }\n\n private stopSpinner() {\n if (!this.spinnerTimer) return;\n clearInterval(this.spinnerTimer);\n this.spinnerTimer = null;\n }\n\n public setRemoteError = (error?: Error) => {\n this.remoteError = extractErrorMessage(error);\n // Avoid rendering a transient remote-only line while the remote check flag is still true\n // Ensure local + remote are rendered together after a failure\n this.stopRemoteCheck();\n this.render();\n };\n\n /**\n * Register the names of the plugins providing dictionaries.\n * They are displayed next to the plugin progress counters, e.g.\n * `Plugin content: 6/6 (sync-json, load-po)`.\n *\n * @param names - Names of the plugins exposing a `loadDictionaries` hook\n */\n setPluginNames(names: string[]) {\n if (this.isFinished) return;\n // Guard against plugins declared without a name at runtime\n this.pluginNames = names.filter((name) => Boolean(name));\n this.render();\n }\n\n setPluginTotal(total: number) {\n if (this.isFinished) return;\n this.pluginTotal = total;\n if (total > 0) {\n this.startSpinner();\n }\n this.render();\n }\n\n setPluginDone(done: number) {\n if (this.isFinished) return;\n this.pluginDone = done;\n this.render();\n }\n\n /**\n * Flag the plugin line as errored.\n *\n * @param error - Error thrown while loading the plugin dictionaries\n * @param pluginName - Name of the plugin that threw, prefixed to the message\n */\n setPluginError(error?: Error, pluginName?: string) {\n if (this.isFinished) return;\n const errorMessage = extractErrorMessage(error);\n this.pluginError = pluginName\n ? `${pluginName}: ${errorMessage}`\n : errorMessage;\n this.render();\n }\n\n private render() {\n const {\n localTotal,\n localDone,\n remoteTotal,\n remoteDone,\n pluginTotal,\n pluginDone,\n } = this.computeProgress();\n\n const frame = this.spinnerFrames[this.spinnerIndex];\n const clock = colorize(frame, ANSIColors.BLUE);\n const lines: string[] = [];\n\n const isLocalDone = localDone === localTotal;\n const isRemoteDone = remoteDone === remoteTotal;\n const isPluginDone = pluginDone === pluginTotal;\n\n const suppressLocalWhileCheckingRemote =\n this.expectRemote && this.remoteCheckInProgress && remoteTotal === 0;\n\n if (!suppressLocalWhileCheckingRemote) {\n if (isLocalDone) {\n lines.push(\n `${this.prefix} ${v} Local content: ${colorize(`${localDone}`, ANSIColors.GREEN)}${colorize(`/${localTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Local content: ${colorize(`${localDone}`, ANSIColors.BLUE)}${colorize(`/${localTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Single remote line: show error, check, or progress counts\n if (remoteTotal > 0 || this.remoteCheckInProgress || this.remoteError) {\n if (this.remoteError) {\n lines.push(\n `${this.prefix} ${x} Remote content: ${colorize(\n this.remoteError,\n ANSIColors.RED\n )}`\n );\n } else if (remoteTotal === 0) {\n lines.push(\n `${this.prefix} ${clock} Remote content: ${colorize('Check server', ANSIColors.BLUE)}`\n );\n } else if (isRemoteDone) {\n lines.push(\n `${this.prefix} ${v} Remote content: ${colorize(`${remoteDone}`, ANSIColors.GREEN)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Remote content: ${colorize(`${remoteDone}`, ANSIColors.BLUE)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Plugin line: show error or progress counts, suffixed by the plugin names\n if (pluginTotal > 0 || this.pluginError) {\n const pluginNamesSuffix =\n this.pluginNames.length > 0\n ? colorize(` (${this.pluginNames.join(', ')})`, ANSIColors.GREY)\n : '';\n\n if (this.pluginError) {\n lines.push(\n `${this.prefix} ${x} Plugin content: ${colorize(\n this.pluginError,\n ANSIColors.RED\n )}`\n );\n } else if (isPluginDone) {\n lines.push(\n `${this.prefix} ${v} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.GREEN)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}${pluginNamesSuffix}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.BLUE)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}${pluginNamesSuffix}`\n );\n }\n }\n\n // Check if the state has changed to avoid duplicate rendering\n const currentState = lines.join('\\n');\n if (currentState === this.lastRenderedState) {\n return;\n }\n this.lastRenderedState = currentState;\n\n if (this.renderedLines > 0) {\n process.stdout.write(`\\x1b[${this.renderedLines}F`);\n }\n\n const totalLinesToClear = Math.max(this.renderedLines, lines.length);\n for (let i = 0; i < totalLinesToClear; i++) {\n process.stdout.write('\\x1b[2K');\n const line = lines[i];\n if (line !== undefined) {\n process.stdout.write(line);\n }\n process.stdout.write('\\n');\n }\n\n this.renderedLines = lines.length;\n }\n\n private computeProgress() {\n const localKeys = new Set(\n this.statuses\n .filter((s) => s.type === 'local')\n .map((s) => s.dictionaryKey)\n );\n\n const localDoneKeys = new Set(\n this.statuses\n .filter(\n (s) =>\n s.type === 'local' && (s.status === 'built' || s.status === 'error')\n )\n .map((s) => s.dictionaryKey)\n );\n\n const remoteKeys = new Set(\n this.statuses\n .filter((s) => s.type === 'remote')\n .map((s) => s.dictionaryKey)\n );\n\n const remoteDoneKeys = new Set(\n this.statuses\n .filter(\n (s) =>\n s.type === 'remote' &&\n (s.status === 'fetched' ||\n s.status === 'imported' ||\n s.status === 'error')\n )\n .map((s) => s.dictionaryKey)\n );\n\n return {\n localTotal: localKeys.size,\n localDone: localDoneKeys.size,\n remoteTotal: remoteKeys.size,\n remoteDone: remoteDoneKeys.size,\n pluginTotal: this.pluginTotal,\n pluginDone: this.pluginDone,\n } as const;\n }\n}\n"],"mappings":"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"}
@@ -27,7 +27,7 @@ const prepareIntlayer = async (configuration, options) => {
27
27
  const sentinelPath = join(configuration.system.cacheDir, "intlayer-prepared.lock");
28
28
  const versionCache = cacheDisk(configuration, ["intlayer-version"]);
29
29
  const intlayerCacheVersion = await versionCache.get();
30
- const isCorrectVersion = Boolean(intlayerCacheVersion && intlayerCacheVersion === packageJson.version);
30
+ const isCorrectVersion = Boolean(intlayerCacheVersion === void 0 || intlayerCacheVersion === packageJson.version);
31
31
  const isConfigSimilar = await isCachedConfigurationUpToDate(configuration);
32
32
  const dictionariesWithStats = await listDictionariesWithStats(configuration);
33
33
  let isDictionaryChanged = false;
@@ -41,8 +41,12 @@ const prepareIntlayer = async (configuration, options) => {
41
41
  forceRun: !isCorrectVersion || !isConfigSimilar || isDictionaryChanged || hasPluginLoadDictionaries,
42
42
  ...options ?? {}
43
43
  };
44
- await runOnce(sentinelPath, async () => {
45
- if (clean || !isCorrectVersion) await cleanOutputDir(configuration);
44
+ await runOnce(sentinelPath, async ({ renewLock }) => {
45
+ const isCorrectVersionUnderLock = await versionCache.get() === packageJson.version;
46
+ if (clean || !isCorrectVersionUnderLock) {
47
+ await cleanOutputDir(configuration);
48
+ await renewLock();
49
+ }
46
50
  await versionCache.set(packageJson.version);
47
51
  const preparationStartMs = Date.now();
48
52
  appLogger(["Preparing Intlayer", colorize(`(v${packageJson.version})`, ANSIColors.GREY_DARK)]);
@@ -1 +1 @@
1
- {"version":3,"file":"prepareIntlayer.mjs","names":[],"sources":["../../src/prepareIntlayer.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport packageJson from '@intlayer/config/package.json' with { type: 'json' };\nimport { cacheDisk } from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { buildDictionary } from './buildIntlayerDictionary/buildIntlayerDictionary';\nimport { writeRemoteDictionary } from './buildIntlayerDictionary/writeRemoteDictionary';\nimport { cleanOutputDir } from './cleanOutputDir';\nimport { createDictionaryEntryPoint } from './createDictionaryEntryPoint/createDictionaryEntryPoint';\nimport { createModuleAugmentation, createTypes } from './createType/index';\nimport { listDictionariesWithStats } from './listDictionariesPath';\nimport { loadDictionaries } from './loadDictionaries/loadDictionaries';\nimport { runOnce } from './utils/runOnce';\nimport {\n isCachedConfigurationUpToDate,\n writeConfiguration,\n} from './writeConfiguration';\n\ntype PrepareIntlayerOptions = {\n clean?: boolean;\n env?: 'prod' | 'dev';\n format?: ('cjs' | 'esm')[];\n forceRun?: boolean;\n cacheTimeoutMs?: number;\n onIsCached?: () => void | Promise<void>;\n};\n\nconst DEFAULT_PREPARE_INTLAYER_OPTIONS = {\n clean: false,\n env: 'dev',\n format: ['cjs', 'esm'],\n cacheTimeoutMs: 1000 * 60 * 60, // 1 hour\n} satisfies PrepareIntlayerOptions;\n\nexport const prepareIntlayer = async (\n configuration: IntlayerConfig,\n options?: PrepareIntlayerOptions\n) => {\n const appLogger = getAppLogger(configuration);\n\n const sentinelPath = join(\n configuration.system.cacheDir,\n 'intlayer-prepared.lock'\n );\n // Clean output dir if the intlayer version has changed\n const versionCache = cacheDisk(configuration, ['intlayer-version']);\n const intlayerCacheVersion = await versionCache.get();\n const isCorrectVersion = Boolean(\n intlayerCacheVersion && intlayerCacheVersion === packageJson.version\n );\n\n const isConfigSimilar = await isCachedConfigurationUpToDate(configuration);\n\n // Check if any dictionary has been changed to force a new rebuild\n const dictionariesWithStats = await listDictionariesWithStats(configuration);\n let isDictionaryChanged = false;\n try {\n // Try catch as sentinel file may not exist yet\n const sentinelStats = await stat(sentinelPath);\n isDictionaryChanged = dictionariesWithStats.some(\n (dictionary) =>\n dictionary.stats.mtime.getTime() > sentinelStats.mtime.getTime()\n );\n } catch {}\n\n const resolvedPlugins = await Promise.all(configuration.plugins ?? []);\n const hasPluginLoadDictionaries = resolvedPlugins.some((plugin) =>\n Boolean(plugin.loadDictionaries)\n ); // Disable cache if any plugin because it can have custom behavior\n\n const { clean, format, forceRun, onIsCached, cacheTimeoutMs, env } = {\n ...DEFAULT_PREPARE_INTLAYER_OPTIONS,\n forceRun:\n !isCorrectVersion ||\n !isConfigSimilar ||\n isDictionaryChanged ||\n hasPluginLoadDictionaries,\n ...(options ?? {}),\n };\n\n // Skip preparation if it has already been done recently\n await runOnce(\n sentinelPath,\n async () => {\n // comment because of issue with next and webpack\n // await checkVersionsConsistency(configuration);\n\n if (clean || !isCorrectVersion) {\n await cleanOutputDir(configuration);\n }\n\n await versionCache.set(packageJson.version);\n\n const preparationStartMs = Date.now();\n\n appLogger([\n 'Preparing Intlayer',\n colorize(`(v${packageJson.version})`, ANSIColors.GREY_DARK),\n ]);\n\n await writeConfiguration(configuration);\n\n const configurationWrittenTime = Date.now();\n\n appLogger(\n [\n 'Configuration written',\n colorize(\n `(${configurationWrittenTime - preparationStartMs}ms)`,\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n const contentDeclarationPaths = dictionariesWithStats.map(\n (dictionary) => dictionary.path\n );\n\n const dictionaries = await loadDictionaries(\n contentDeclarationPaths,\n configuration\n );\n\n const dictionariesLoadedTime = Date.now();\n\n appLogger(\n [\n 'Content loaded',\n colorize(\n [\n dictionaries.remoteDictionaries.length +\n dictionaries.pluginDictionaries.length >\n 0\n ? [\n `(Total: ${dictionariesLoadedTime - configurationWrittenTime}ms`,\n dictionaries.localDictionaries.length > 0\n ? ` - Local: ${dictionaries.time.localDictionaries}ms`\n : '',\n dictionaries.remoteDictionaries.length > 0\n ? ` - Remote: ${dictionaries.time.remoteDictionaries}ms`\n : '',\n dictionaries.pluginDictionaries.length > 0\n ? ` - Plugin: ${dictionaries.time.pluginDictionaries}ms`\n : '',\n `)`,\n ].join('')\n : `(${dictionariesLoadedTime - configurationWrittenTime}ms)`,\n ].join(''),\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n // Build local dictionaries\n const dictionariesOutput = await buildDictionary(\n [\n ...dictionaries.localDictionaries,\n ...dictionaries.remoteDictionaries,\n ...dictionaries.pluginDictionaries,\n ],\n configuration,\n { formats: format, importOtherDictionaries: false, env }\n );\n\n // Write remote dictionaries\n // Used as cache for next fetch\n await writeRemoteDictionary(\n dictionaries.remoteDictionaries,\n configuration\n );\n\n const dictionariesToBuild = Object.values(\n dictionariesOutput?.mergedDictionaries ?? {}\n ).map((dictionary) => dictionary.dictionary);\n\n await createTypes(dictionariesToBuild, configuration);\n\n await createDictionaryEntryPoint(configuration, {\n formats: options?.format,\n });\n\n const dictionariesBuiltTime = Date.now();\n\n appLogger([\n 'Dictionaries built',\n colorize(\n `(${dictionariesBuiltTime - preparationStartMs}ms)`,\n ANSIColors.GREY_DARK\n ),\n ]);\n\n await createModuleAugmentation(configuration);\n\n const moduleAugmentationBuiltTime = Date.now();\n\n appLogger(\n [\n 'Module augmentation built',\n colorize(\n `(${moduleAugmentationBuiltTime - dictionariesBuiltTime}ms)`,\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n // Plugin transformation\n // Allow plugins to post-process the final build output (e.g., write back ICU JSON)\n for await (const plugin of configuration.plugins ?? []) {\n const { unmergedDictionaries, mergedDictionaries } = dictionariesOutput;\n\n try {\n await plugin.afterBuild?.({\n dictionaries: {\n unmergedDictionaries,\n mergedDictionaries,\n },\n configuration,\n });\n } catch (error) {\n // A failing write-back (e.g. transient network error on a remote\n // target) must not invalidate the dictionaries already built.\n appLogger(\n [\n `Plugin ${plugin.name} afterBuild failed:`,\n (error as Error).message,\n ],\n { level: 'error' }\n );\n }\n }\n\n const preparationElapsedMs = Date.now() - preparationStartMs;\n appLogger(\n [`Done`, colorize(`${preparationElapsedMs}ms`, ANSIColors.GREEN)],\n {\n level: 'info',\n isVerbose: true,\n }\n );\n },\n {\n forceRun,\n onIsCached,\n cacheTimeoutMs,\n }\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"prepareIntlayer.mjs","names":[],"sources":["../../src/prepareIntlayer.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport packageJson from '@intlayer/config/package.json' with { type: 'json' };\nimport { cacheDisk } from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { buildDictionary } from './buildIntlayerDictionary/buildIntlayerDictionary';\nimport { writeRemoteDictionary } from './buildIntlayerDictionary/writeRemoteDictionary';\nimport { cleanOutputDir } from './cleanOutputDir';\nimport { createDictionaryEntryPoint } from './createDictionaryEntryPoint/createDictionaryEntryPoint';\nimport { createModuleAugmentation, createTypes } from './createType/index';\nimport { listDictionariesWithStats } from './listDictionariesPath';\nimport { loadDictionaries } from './loadDictionaries/loadDictionaries';\nimport { runOnce } from './utils/runOnce';\nimport {\n isCachedConfigurationUpToDate,\n writeConfiguration,\n} from './writeConfiguration';\n\ntype PrepareIntlayerOptions = {\n clean?: boolean;\n env?: 'prod' | 'dev';\n format?: ('cjs' | 'esm')[];\n forceRun?: boolean;\n cacheTimeoutMs?: number;\n onIsCached?: () => void | Promise<void>;\n};\n\nconst DEFAULT_PREPARE_INTLAYER_OPTIONS = {\n clean: false,\n env: 'dev',\n format: ['cjs', 'esm'],\n cacheTimeoutMs: 1000 * 60 * 60, // 1 hour\n} satisfies PrepareIntlayerOptions;\n\nexport const prepareIntlayer = async (\n configuration: IntlayerConfig,\n options?: PrepareIntlayerOptions\n) => {\n const appLogger = getAppLogger(configuration);\n\n const sentinelPath = join(\n configuration.system.cacheDir,\n 'intlayer-prepared.lock'\n );\n // Clean output dir if the intlayer version has changed\n const versionCache = cacheDisk(configuration, ['intlayer-version']);\n const intlayerCacheVersion = await versionCache.get();\n const isCorrectVersion = Boolean(\n intlayerCacheVersion === undefined ||\n intlayerCacheVersion === packageJson.version\n );\n\n const isConfigSimilar = await isCachedConfigurationUpToDate(configuration);\n\n // Check if any dictionary has been changed to force a new rebuild\n const dictionariesWithStats = await listDictionariesWithStats(configuration);\n let isDictionaryChanged = false;\n try {\n // Try catch as sentinel file may not exist yet\n const sentinelStats = await stat(sentinelPath);\n isDictionaryChanged = dictionariesWithStats.some(\n (dictionary) =>\n dictionary.stats.mtime.getTime() > sentinelStats.mtime.getTime()\n );\n } catch {}\n\n const resolvedPlugins = await Promise.all(configuration.plugins ?? []);\n const hasPluginLoadDictionaries = resolvedPlugins.some((plugin) =>\n Boolean(plugin.loadDictionaries)\n ); // Disable cache if any plugin because it can have custom behavior\n\n const { clean, format, forceRun, onIsCached, cacheTimeoutMs, env } = {\n ...DEFAULT_PREPARE_INTLAYER_OPTIONS,\n forceRun:\n !isCorrectVersion ||\n !isConfigSimilar ||\n isDictionaryChanged ||\n hasPluginLoadDictionaries,\n ...(options ?? {}),\n };\n\n // Skip preparation if it has already been done recently\n await runOnce(\n sentinelPath,\n async ({ renewLock }) => {\n // comment because of issue with next and webpack\n // await checkVersionsConsistency(configuration);\n\n // Re-read the cached version now that the lock is held: another process\n // may have upgraded the output directory while this call was queued, in\n // which case cleaning it would throw away a build that just completed.\n const isCorrectVersionUnderLock =\n (await versionCache.get()) === packageJson.version;\n\n if (clean || !isCorrectVersionUnderLock) {\n await cleanOutputDir(configuration);\n // `cleanOutputDir` wipes the cache directory the sentinel lives in.\n // Re-create it immediately, otherwise a concurrent process (e.g. the\n // `intlayer watch` CLI next to a dev server) sees no lock, starts its\n // own run, and cleans the output directory while this one writes.\n await renewLock();\n }\n\n await versionCache.set(packageJson.version);\n\n const preparationStartMs = Date.now();\n\n appLogger([\n 'Preparing Intlayer',\n colorize(`(v${packageJson.version})`, ANSIColors.GREY_DARK),\n ]);\n\n await writeConfiguration(configuration);\n\n const configurationWrittenTime = Date.now();\n\n appLogger(\n [\n 'Configuration written',\n colorize(\n `(${configurationWrittenTime - preparationStartMs}ms)`,\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n const contentDeclarationPaths = dictionariesWithStats.map(\n (dictionary) => dictionary.path\n );\n\n const dictionaries = await loadDictionaries(\n contentDeclarationPaths,\n configuration\n );\n\n const dictionariesLoadedTime = Date.now();\n\n appLogger(\n [\n 'Content loaded',\n colorize(\n [\n dictionaries.remoteDictionaries.length +\n dictionaries.pluginDictionaries.length >\n 0\n ? [\n `(Total: ${dictionariesLoadedTime - configurationWrittenTime}ms`,\n dictionaries.localDictionaries.length > 0\n ? ` - Local: ${dictionaries.time.localDictionaries}ms`\n : '',\n dictionaries.remoteDictionaries.length > 0\n ? ` - Remote: ${dictionaries.time.remoteDictionaries}ms`\n : '',\n dictionaries.pluginDictionaries.length > 0\n ? ` - Plugin: ${dictionaries.time.pluginDictionaries}ms`\n : '',\n `)`,\n ].join('')\n : `(${dictionariesLoadedTime - configurationWrittenTime}ms)`,\n ].join(''),\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n // Build local dictionaries\n const dictionariesOutput = await buildDictionary(\n [\n ...dictionaries.localDictionaries,\n ...dictionaries.remoteDictionaries,\n ...dictionaries.pluginDictionaries,\n ],\n configuration,\n { formats: format, importOtherDictionaries: false, env }\n );\n\n // Write remote dictionaries\n // Used as cache for next fetch\n await writeRemoteDictionary(\n dictionaries.remoteDictionaries,\n configuration\n );\n\n const dictionariesToBuild = Object.values(\n dictionariesOutput?.mergedDictionaries ?? {}\n ).map((dictionary) => dictionary.dictionary);\n\n await createTypes(dictionariesToBuild, configuration);\n\n await createDictionaryEntryPoint(configuration, {\n formats: options?.format,\n });\n\n const dictionariesBuiltTime = Date.now();\n\n appLogger([\n 'Dictionaries built',\n colorize(\n `(${dictionariesBuiltTime - preparationStartMs}ms)`,\n ANSIColors.GREY_DARK\n ),\n ]);\n\n await createModuleAugmentation(configuration);\n\n const moduleAugmentationBuiltTime = Date.now();\n\n appLogger(\n [\n 'Module augmentation built',\n colorize(\n `(${moduleAugmentationBuiltTime - dictionariesBuiltTime}ms)`,\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n // Plugin transformation\n // Allow plugins to post-process the final build output (e.g., write back ICU JSON)\n for await (const plugin of configuration.plugins ?? []) {\n const { unmergedDictionaries, mergedDictionaries } = dictionariesOutput;\n\n try {\n await plugin.afterBuild?.({\n dictionaries: {\n unmergedDictionaries,\n mergedDictionaries,\n },\n configuration,\n });\n } catch (error) {\n // A failing write-back (e.g. transient network error on a remote\n // target) must not invalidate the dictionaries already built.\n appLogger(\n [\n `Plugin ${plugin.name} afterBuild failed:`,\n (error as Error).message,\n ],\n { level: 'error' }\n );\n }\n }\n\n const preparationElapsedMs = Date.now() - preparationStartMs;\n appLogger(\n [`Done`, colorize(`${preparationElapsedMs}ms`, ANSIColors.GREEN)],\n {\n level: 'info',\n isVerbose: true,\n }\n );\n },\n {\n forceRun,\n onIsCached,\n cacheTimeoutMs,\n }\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ //#region src/utils/chunkArray.ts
2
+ /**
3
+ * Splits an array into consecutive chunks of at most `chunkSize` items.
4
+ *
5
+ * The last chunk holds the remainder and may be smaller. An empty input, or a
6
+ * `chunkSize` lower than 1, returns an empty list of chunks.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * chunkArray(['a', 'b', 'c', 'd', 'e'], 2); // [['a', 'b'], ['c', 'd'], ['e']]
11
+ * ```
12
+ */
13
+ const chunkArray = (items, chunkSize) => {
14
+ if (chunkSize < 1) return [];
15
+ const chunks = [];
16
+ for (let index = 0; index < items.length; index += chunkSize) chunks.push(items.slice(index, index + chunkSize));
17
+ return chunks;
18
+ };
19
+
20
+ //#endregion
21
+ export { chunkArray };
22
+ //# sourceMappingURL=chunkArray.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunkArray.mjs","names":[],"sources":["../../../src/utils/chunkArray.ts"],"sourcesContent":["/**\n * Splits an array into consecutive chunks of at most `chunkSize` items.\n *\n * The last chunk holds the remainder and may be smaller. An empty input, or a\n * `chunkSize` lower than 1, returns an empty list of chunks.\n *\n * @example\n * ```ts\n * chunkArray(['a', 'b', 'c', 'd', 'e'], 2); // [['a', 'b'], ['c', 'd'], ['e']]\n * ```\n */\nexport const chunkArray = <T>(items: T[], chunkSize: number): T[][] => {\n if (chunkSize < 1) return [];\n\n const chunks: T[][] = [];\n\n for (let index = 0; index < items.length; index += chunkSize) {\n chunks.push(items.slice(index, index + chunkSize));\n }\n\n return chunks;\n};\n"],"mappings":";;;;;;;;;;;;AAWA,MAAa,cAAiB,OAAY,cAA6B;CACrE,IAAI,YAAY,GAAG,OAAO,CAAC;CAE3B,MAAM,SAAgB,CAAC;CAEvB,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,WACjD,OAAO,KAAK,MAAM,MAAM,OAAO,QAAQ,SAAS,CAAC;CAGnD,OAAO;AACT"}
@@ -4,6 +4,7 @@ import { getPathHash } from "./getPathHash.mjs";
4
4
  import { formatLocale, formatPath } from "./formatter.mjs";
5
5
  import { autoDecorateContent } from "./autoDecorateContent.mjs";
6
6
  import { buildComponentFilesList } from "./buildComponentFilesList.mjs";
7
+ import { chunkArray } from "./chunkArray.mjs";
7
8
  import { splitTextByLines } from "./splitTextByLine.mjs";
8
9
  import { getChunk } from "./getChunk.mjs";
9
10
  import { assembleJSON, chunkJSON, reconstructFromSingleChunk } from "./chunkJSON.mjs";
@@ -19,4 +20,4 @@ import { runParallel } from "./runParallel/index.mjs";
19
20
  import { sortAlphabetically } from "./sortAlphabetically.mjs";
20
21
  import { verifyIdenticObjectFormat } from "./verifyIdenticObjectFormat.mjs";
21
22
 
22
- export { Queue, assembleJSON, autoDecorateContent, buildComponentFilesList, chunkJSON, excludeObjectFormat, formatLocale, formatPath, getChunk, getContentExtension, getExtensionFromFormat, getFormatFromExtension, getGlobalLimiter, getPathHash, getTaskLimiter, mergeChunks, pLimit, parallelize, parallelizeGlobal, reconstructFromSingleChunk, reduceObjectFormat, resolveObjectPromises, resolveRelativePath, runOnce, runParallel, sortAlphabetically, splitTextByLines, verifyIdenticObjectFormat };
23
+ export { Queue, assembleJSON, autoDecorateContent, buildComponentFilesList, chunkArray, chunkJSON, excludeObjectFormat, formatLocale, formatPath, getChunk, getContentExtension, getExtensionFromFormat, getFormatFromExtension, getGlobalLimiter, getPathHash, getTaskLimiter, mergeChunks, pLimit, parallelize, parallelizeGlobal, reconstructFromSingleChunk, reduceObjectFormat, resolveObjectPromises, resolveRelativePath, runOnce, runParallel, sortAlphabetically, splitTextByLines, verifyIdenticObjectFormat };
@@ -1,90 +1,169 @@
1
1
  import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
+ import { rmSync } from "node:fs";
3
4
  import packageJson from "@intlayer/core/package.json" with { type: "json" };
4
5
 
5
6
  //#region src/utils/runOnce.ts
6
- const DEFAULT_RUN_ONCE_OPTIONS = { cacheTimeoutMs: 60 * 1e3 };
7
- const writeSentinelFile = async (sentinelFilePath, currentTimestamp) => {
8
- const data = {
9
- version: packageJson.version,
10
- timestamp: currentTimestamp
11
- };
7
+ const DEFAULT_RUN_ONCE_OPTIONS = {
8
+ cacheTimeoutMs: 60 * 1e3,
9
+ lockWaitTimeoutMs: 300 * 1e3
10
+ };
11
+ /** Delay between two reads of a sentinel owned by another process. */
12
+ const LOCK_POLL_INTERVAL_MS = 50;
13
+ const delay = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
14
+ /**
15
+ * Sentinels owned by this process, released synchronously on exit so a crash or
16
+ * a Ctrl-C never leaves a `running` lock that would stall the next run.
17
+ */
18
+ const ownedSentinelFilePaths = /* @__PURE__ */ new Set();
19
+ process.on("exit", () => {
20
+ for (const sentinelFilePath of ownedSentinelFilePaths) try {
21
+ rmSync(sentinelFilePath, { force: true });
22
+ } catch {}
23
+ });
24
+ /**
25
+ * Reads the sentinel file, returning `undefined` when it does not exist or
26
+ * cannot be parsed.
27
+ *
28
+ * Sentinels written by older versions carry no `status`; they always describe a
29
+ * finished run, so they are reported as `done`.
30
+ */
31
+ const readSentinelState = async (sentinelFilePath) => {
32
+ try {
33
+ const [sentinelStats, raw] = await Promise.all([stat(sentinelFilePath), readFile(sentinelFilePath, "utf8")]);
34
+ const parsed = JSON.parse(raw);
35
+ return {
36
+ version: parsed.version ?? "",
37
+ timestamp: parsed.timestamp ?? 0,
38
+ status: parsed.status ?? "done",
39
+ pid: parsed.pid ?? 0,
40
+ mtimeMs: sentinelStats.mtime.getTime()
41
+ };
42
+ } catch {
43
+ return;
44
+ }
45
+ };
46
+ /**
47
+ * Whether the process that wrote the sentinel is still alive. An unknown PID
48
+ * (legacy sentinel) is assumed alive so the staleness timeout stays the only
49
+ * way to reclaim it.
50
+ */
51
+ const isOwnerProcessAlive = (pid) => {
52
+ if (!pid || pid === process.pid) return true;
12
53
  try {
54
+ process.kill(pid, 0);
55
+ return true;
56
+ } catch (error) {
57
+ return error.code === "EPERM";
58
+ }
59
+ };
60
+ const serializeSentinel = (timestamp, status) => JSON.stringify({
61
+ version: packageJson.version,
62
+ timestamp,
63
+ status,
64
+ pid: process.pid
65
+ });
66
+ /**
67
+ * Attempts to take ownership of the sentinel.
68
+ *
69
+ * `wx` makes the creation atomic, so exactly one process can win even when
70
+ * several start at the same moment.
71
+ *
72
+ * @returns `true` when this process now owns the sentinel, `false` when another
73
+ * process created it first.
74
+ */
75
+ const acquireSentinel = async (sentinelFilePath, timestamp) => {
76
+ const data = serializeSentinel(timestamp, "running");
77
+ for (let attempt = 0; attempt < 2; attempt++) try {
13
78
  await mkdir(dirname(sentinelFilePath), { recursive: true });
14
- await writeFile(sentinelFilePath, JSON.stringify(data), { flag: "wx" });
15
- } catch (err) {
16
- if (err.code === "EEXIST") return;
17
- if (err.code === "ENOENT") try {
18
- await mkdir(dirname(sentinelFilePath), { recursive: true });
19
- await writeFile(sentinelFilePath, JSON.stringify(data), { flag: "wx" });
20
- return;
21
- } catch (retryErr) {
22
- if (retryErr.code === "EEXIST") return;
23
- }
24
- throw err;
79
+ await writeFile(sentinelFilePath, data, { flag: "wx" });
80
+ return true;
81
+ } catch (error) {
82
+ const code = error.code;
83
+ if (code === "EEXIST") return false;
84
+ if (code === "ENOENT" && attempt === 0) continue;
85
+ throw error;
25
86
  }
87
+ return false;
88
+ };
89
+ /**
90
+ * Rewrites the sentinel owned by this process, overwriting any existing file.
91
+ */
92
+ const writeOwnedSentinel = async (sentinelFilePath, timestamp, status) => {
93
+ try {
94
+ await mkdir(dirname(sentinelFilePath), { recursive: true });
95
+ await writeFile(sentinelFilePath, serializeSentinel(timestamp, status));
96
+ } catch {}
97
+ };
98
+ const removeSentinel = async (sentinelFilePath) => {
99
+ try {
100
+ await unlink(sentinelFilePath);
101
+ } catch {}
26
102
  };
27
103
  /**
28
104
  * Ensures a callback function runs only once within a specified time window across multiple processes.
29
105
  * Uses a sentinel file to coordinate execution and prevent duplicate work.
30
106
  *
107
+ * Processes that lose the race for the sentinel wait for the owner to finish
108
+ * rather than running a competing copy of the callback — concurrent runs would
109
+ * otherwise write to (and clean) the same output directory at the same time.
110
+ *
31
111
  * @param sentinelFilePath - Path to the sentinel file used for coordination
32
112
  * @param callback - The function to execute (should be async)
33
113
  * @param options - The options for the runOnce function
34
114
  *
35
115
  * @example
36
116
  * ```typescript
37
- * await runPrepareIntlayerOnce(
117
+ * await runOnce(
38
118
  * '/tmp/intlayer-sentinel',
39
- * async () => {
40
- * // Your initialization logic here
119
+ * async ({ renewLock }) => {
120
+ * await cleanOutputDir(configuration); // may delete the sentinel
121
+ * await renewLock();
41
122
  * await prepareIntlayer();
42
123
  * },
43
- * 30 * 1000 // 30 seconds cache
124
+ * { cacheTimeoutMs: 30 * 1000 } // 30 seconds cache
44
125
  * );
45
126
  * ```
46
127
  *
47
128
  * @throws {Error} When there are unexpected filesystem errors
48
129
  */
49
130
  const runOnce = async (sentinelFilePath, callback, options) => {
50
- const { onIsCached, cacheTimeoutMs, forceRun } = {
131
+ const { onIsCached, cacheTimeoutMs, forceRun, lockWaitTimeoutMs } = {
51
132
  ...DEFAULT_RUN_ONCE_OPTIONS,
52
133
  ...options ?? {}
53
134
  };
54
135
  const currentTimestamp = Date.now();
55
- try {
56
- const sentinelAge = currentTimestamp - (await stat(sentinelFilePath)).mtime.getTime();
57
- let shouldRebuild = Boolean(forceRun) || sentinelAge > cacheTimeoutMs;
58
- if (!shouldRebuild) try {
59
- const raw = await readFile(sentinelFilePath, "utf8");
60
- let cachedVersion;
61
- try {
62
- cachedVersion = JSON.parse(raw).version;
63
- } catch {
64
- cachedVersion = void 0;
136
+ const waitDeadline = currentTimestamp + lockWaitTimeoutMs;
137
+ while (true) {
138
+ const sentinelState = await readSentinelState(sentinelFilePath);
139
+ if (sentinelState) {
140
+ const sentinelAge = Date.now() - sentinelState.mtimeMs;
141
+ if (sentinelState.status === "running") {
142
+ if (!(sentinelAge > lockWaitTimeoutMs || !isOwnerProcessAlive(sentinelState.pid)) && Date.now() < waitDeadline) {
143
+ await delay(LOCK_POLL_INTERVAL_MS);
144
+ continue;
145
+ }
146
+ await removeSentinel(sentinelFilePath);
147
+ continue;
65
148
  }
66
- if (!cachedVersion || cachedVersion !== packageJson.version) shouldRebuild = true;
67
- } catch {
68
- shouldRebuild = true;
69
- }
70
- if (shouldRebuild) try {
71
- await unlink(sentinelFilePath);
72
- } catch {}
73
- else {
74
- await onIsCached?.();
75
- return;
149
+ if (!forceRun && sentinelAge <= cacheTimeoutMs && sentinelState.version === packageJson.version) {
150
+ await onIsCached?.();
151
+ return;
152
+ }
153
+ await removeSentinel(sentinelFilePath);
76
154
  }
77
- } catch (err) {
78
- if (err.code === "ENOENT") {} else throw err;
155
+ if (await acquireSentinel(sentinelFilePath, currentTimestamp)) break;
156
+ await delay(LOCK_POLL_INTERVAL_MS);
79
157
  }
80
- await writeSentinelFile(sentinelFilePath, currentTimestamp);
158
+ ownedSentinelFilePaths.add(sentinelFilePath);
159
+ const renewLock = () => writeOwnedSentinel(sentinelFilePath, currentTimestamp, "running");
81
160
  try {
82
- await callback();
83
- await writeSentinelFile(sentinelFilePath, currentTimestamp);
161
+ await callback({ renewLock });
162
+ await writeOwnedSentinel(sentinelFilePath, currentTimestamp, "done");
84
163
  } catch {
85
- try {
86
- await unlink(sentinelFilePath);
87
- } catch {}
164
+ await removeSentinel(sentinelFilePath);
165
+ } finally {
166
+ ownedSentinelFilePaths.delete(sentinelFilePath);
88
167
  }
89
168
  };
90
169
 
@@ -1 +1 @@
1
- {"version":3,"file":"runOnce.mjs","names":[],"sources":["../../../src/utils/runOnce.ts"],"sourcesContent":["import { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport packageJson from '@intlayer/core/package.json' with { type: 'json' };\n\ntype RunOnceOptions = {\n /**\n * The function to execute when the sentinel is not found or is older than the cache timeout.\n */\n onIsCached?: () => void | Promise<void>;\n /**\n * The time window in milliseconds during which the sentinel is considered valid.\n *\n * @default 60000 = 1 minute\n */\n cacheTimeoutMs?: number;\n /**\n * If true, the callback will always run. If undefined, the callback will run only if the sentinel is older than the cache timeout.\n *\n * @default false\n */\n forceRun?: boolean;\n};\n\nconst DEFAULT_RUN_ONCE_OPTIONS = {\n cacheTimeoutMs: 60 * 1000, // 1 minute in milliseconds,\n} satisfies RunOnceOptions;\n\ntype SentinelData = {\n version: string;\n timestamp: number;\n};\n\nconst writeSentinelFile = async (\n sentinelFilePath: string,\n currentTimestamp: number\n) => {\n // O_EXCL ensures only the *first* process can create the file\n const data: SentinelData = {\n version: packageJson.version,\n timestamp: currentTimestamp,\n };\n\n try {\n // Ensure the directory exists before writing the file\n await mkdir(dirname(sentinelFilePath), { recursive: true });\n\n await writeFile(sentinelFilePath, JSON.stringify(data), { flag: 'wx' });\n } catch (err: any) {\n if (err.code === 'EEXIST') {\n // Another process already created it → we're done\n return;\n }\n // Optimization: If ENOENT occurs on write despite mkdir (race condition with external deletion), retry once.\n if (err.code === 'ENOENT') {\n try {\n await mkdir(dirname(sentinelFilePath), { recursive: true });\n await writeFile(sentinelFilePath, JSON.stringify(data), { flag: 'wx' });\n return;\n } catch (retryErr: any) {\n if (retryErr.code === 'EEXIST') return;\n }\n }\n throw err; // unexpected FS error\n }\n};\n\n/**\n * Ensures a callback function runs only once within a specified time window across multiple processes.\n * Uses a sentinel file to coordinate execution and prevent duplicate work.\n *\n * @param sentinelFilePath - Path to the sentinel file used for coordination\n * @param callback - The function to execute (should be async)\n * @param options - The options for the runOnce function\n *\n * @example\n * ```typescript\n * await runPrepareIntlayerOnce(\n * '/tmp/intlayer-sentinel',\n * async () => {\n * // Your initialization logic here\n * await prepareIntlayer();\n * },\n * 30 * 1000 // 30 seconds cache\n * );\n * ```\n *\n * @throws {Error} When there are unexpected filesystem errors\n */\nexport const runOnce = async (\n sentinelFilePath: string,\n callback: () => void | Promise<void>,\n options?: RunOnceOptions\n) => {\n const { onIsCached, cacheTimeoutMs, forceRun } = {\n ...DEFAULT_RUN_ONCE_OPTIONS,\n ...(options ?? {}),\n };\n const currentTimestamp = Date.now();\n\n try {\n // Check if sentinel file exists and get its stats\n const sentinelStats = await stat(sentinelFilePath);\n const sentinelAge = currentTimestamp - sentinelStats.mtime.getTime();\n\n // Determine if we should rebuild based on cache age, force flag, or version mismatch\n let shouldRebuild = Boolean(forceRun) || sentinelAge > cacheTimeoutMs!;\n\n if (!shouldRebuild) {\n try {\n const raw = await readFile(sentinelFilePath, 'utf8');\n let cachedVersion: string | undefined;\n try {\n const parsed = JSON.parse(raw) as Partial<SentinelData>;\n cachedVersion = parsed.version;\n } catch {\n // Legacy format (timestamp only). Force a rebuild once to write versioned sentinel.\n cachedVersion = undefined;\n }\n\n if (!cachedVersion || cachedVersion !== packageJson.version) {\n shouldRebuild = true;\n }\n } catch {\n // If we cannot read the file, err on the safe side and rebuild\n shouldRebuild = true;\n }\n }\n\n if (shouldRebuild) {\n try {\n await unlink(sentinelFilePath);\n } catch {}\n // Fall through to create new sentinel and rebuild\n } else {\n await onIsCached?.();\n // Sentinel is recent and versions match, no need to rebuild\n return;\n }\n } catch (err: any) {\n if (err.code === 'ENOENT') {\n // File doesn't exist, continue to create it\n } else {\n throw err; // unexpected FS error\n }\n }\n\n // Write sentinel file before to block parallel processes\n // Added await here\n await writeSentinelFile(sentinelFilePath, currentTimestamp);\n\n try {\n await callback();\n\n // Write sentinel file after to ensure the first one has not been removed with cleanOutputDir\n // Added await here\n await writeSentinelFile(sentinelFilePath, currentTimestamp);\n } catch {\n try {\n await unlink(sentinelFilePath); // Remove sentinel file if an error occurs\n } catch {}\n }\n};\n"],"mappings":";;;;;AAuBA,MAAM,2BAA2B,EAC/B,gBAAgB,KAAK,IACvB;AAOA,MAAM,oBAAoB,OACxB,kBACA,qBACG;CAEH,MAAM,OAAqB;EACzB,SAAS,YAAY;EACrB,WAAW;CACb;CAEA,IAAI;EAEF,MAAM,MAAM,QAAQ,gBAAgB,GAAG,EAAE,WAAW,KAAK,CAAC;EAE1D,MAAM,UAAU,kBAAkB,KAAK,UAAU,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;CACxE,SAAS,KAAU;EACjB,IAAI,IAAI,SAAS,UAEf;EAGF,IAAI,IAAI,SAAS,UACf,IAAI;GACF,MAAM,MAAM,QAAQ,gBAAgB,GAAG,EAAE,WAAW,KAAK,CAAC;GAC1D,MAAM,UAAU,kBAAkB,KAAK,UAAU,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;GACtE;EACF,SAAS,UAAe;GACtB,IAAI,SAAS,SAAS,UAAU;EAClC;EAEF,MAAM;CACR;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,UAAU,OACrB,kBACA,UACA,YACG;CACH,MAAM,EAAE,YAAY,gBAAgB,aAAa;EAC/C,GAAG;EACH,GAAI,WAAW,CAAC;CAClB;CACA,MAAM,mBAAmB,KAAK,IAAI;CAElC,IAAI;EAGF,MAAM,cAAc,oBAAmB,MADX,KAAK,gBAAgB,EACG,CAAC,MAAM,QAAQ;EAGnE,IAAI,gBAAgB,QAAQ,QAAQ,KAAK,cAAc;EAEvD,IAAI,CAAC,eACH,IAAI;GACF,MAAM,MAAM,MAAM,SAAS,kBAAkB,MAAM;GACnD,IAAI;GACJ,IAAI;IAEF,gBADe,KAAK,MAAM,GACL,CAAC,CAAC;GACzB,QAAQ;IAEN,gBAAgB;GAClB;GAEA,IAAI,CAAC,iBAAiB,kBAAkB,YAAY,SAClD,gBAAgB;EAEpB,QAAQ;GAEN,gBAAgB;EAClB;EAGF,IAAI,eACF,IAAI;GACF,MAAM,OAAO,gBAAgB;EAC/B,QAAQ,CAAC;OAEJ;GACL,MAAM,aAAa;GAEnB;EACF;CACF,SAAS,KAAU;EACjB,IAAI,IAAI,SAAS,UAAU,CAE3B,OACE,MAAM;CAEV;CAIA,MAAM,kBAAkB,kBAAkB,gBAAgB;CAE1D,IAAI;EACF,MAAM,SAAS;EAIf,MAAM,kBAAkB,kBAAkB,gBAAgB;CAC5D,QAAQ;EACN,IAAI;GACF,MAAM,OAAO,gBAAgB;EAC/B,QAAQ,CAAC;CACX;AACF"}
1
+ {"version":3,"file":"runOnce.mjs","names":[],"sources":["../../../src/utils/runOnce.ts"],"sourcesContent":["import { rmSync } from 'node:fs';\nimport { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport packageJson from '@intlayer/core/package.json' with { type: 'json' };\n\n/**\n * Lifecycle of the run described by a sentinel file.\n *\n * - `running` — a process owns the sentinel and its callback is still in flight.\n * Other processes must wait instead of starting a concurrent run.\n * - `done` — the callback completed; the sentinel is now a plain cache marker.\n */\ntype SentinelStatus = 'running' | 'done';\n\ntype SentinelData = {\n version: string;\n timestamp: number;\n status: SentinelStatus;\n /** PID of the process that owns the sentinel, used to detect abandoned locks. */\n pid: number;\n};\n\n/** Sentinel state as read from disk, enriched with the file's modification time. */\ntype SentinelState = SentinelData & { mtimeMs: number };\n\n/**\n * Context handed to the callback so it can interact with the lock it runs under.\n */\nexport type RunOnceContext = {\n /**\n * Re-create the sentinel file after an operation that may have deleted it —\n * typically cleaning the output directory, which wipes the cache directory the\n * sentinel lives in. Without this, concurrent processes would see no lock and\n * start a competing run while this one is still writing.\n */\n renewLock: () => Promise<void>;\n};\n\ntype RunOnceOptions = {\n /**\n * The function to execute when the sentinel is not found or is older than the cache timeout.\n */\n onIsCached?: () => void | Promise<void>;\n /**\n * The time window in milliseconds during which the sentinel is considered valid.\n *\n * @default 60000 = 1 minute\n */\n cacheTimeoutMs?: number;\n /**\n * If true, the callback will always run. If undefined, the callback will run only if the sentinel is older than the cache timeout.\n *\n * @default false\n */\n forceRun?: boolean;\n /**\n * How long to wait for another process to release the sentinel before\n * considering its run abandoned and taking the lock over.\n *\n * @default 300000 = 5 minutes\n */\n lockWaitTimeoutMs?: number;\n};\n\nconst DEFAULT_RUN_ONCE_OPTIONS = {\n cacheTimeoutMs: 60 * 1000, // 1 minute in milliseconds,\n lockWaitTimeoutMs: 5 * 60 * 1000, // 5 minutes in milliseconds\n} satisfies RunOnceOptions;\n\n/** Delay between two reads of a sentinel owned by another process. */\nconst LOCK_POLL_INTERVAL_MS = 50;\n\nconst delay = (durationMs: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, durationMs));\n\n/**\n * Sentinels owned by this process, released synchronously on exit so a crash or\n * a Ctrl-C never leaves a `running` lock that would stall the next run.\n */\nconst ownedSentinelFilePaths = new Set<string>();\n\nprocess.on('exit', () => {\n for (const sentinelFilePath of ownedSentinelFilePaths) {\n try {\n rmSync(sentinelFilePath, { force: true });\n } catch {}\n }\n});\n\n/**\n * Reads the sentinel file, returning `undefined` when it does not exist or\n * cannot be parsed.\n *\n * Sentinels written by older versions carry no `status`; they always describe a\n * finished run, so they are reported as `done`.\n */\nconst readSentinelState = async (\n sentinelFilePath: string\n): Promise<SentinelState | undefined> => {\n try {\n const [sentinelStats, raw] = await Promise.all([\n stat(sentinelFilePath),\n readFile(sentinelFilePath, 'utf8'),\n ]);\n\n const parsed = JSON.parse(raw) as Partial<SentinelData>;\n\n return {\n version: parsed.version ?? '',\n timestamp: parsed.timestamp ?? 0,\n status: parsed.status ?? 'done',\n pid: parsed.pid ?? 0,\n mtimeMs: sentinelStats.mtime.getTime(),\n };\n } catch {\n return undefined;\n }\n};\n\n/**\n * Whether the process that wrote the sentinel is still alive. An unknown PID\n * (legacy sentinel) is assumed alive so the staleness timeout stays the only\n * way to reclaim it.\n */\nconst isOwnerProcessAlive = (pid: number): boolean => {\n if (!pid || pid === process.pid) return true;\n\n try {\n // Signal 0 performs an existence check 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\nconst serializeSentinel = (timestamp: number, status: SentinelStatus): string =>\n JSON.stringify({\n version: packageJson.version,\n timestamp,\n status,\n pid: process.pid,\n } satisfies SentinelData);\n\n/**\n * Attempts to take ownership of the sentinel.\n *\n * `wx` makes the creation atomic, so exactly one process can win even when\n * several start at the same moment.\n *\n * @returns `true` when this process now owns the sentinel, `false` when another\n * process created it first.\n */\nconst acquireSentinel = async (\n sentinelFilePath: string,\n timestamp: number\n): Promise<boolean> => {\n const data = serializeSentinel(timestamp, 'running');\n\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n // Ensure the directory exists before writing the file\n await mkdir(dirname(sentinelFilePath), { recursive: true });\n\n await writeFile(sentinelFilePath, data, { flag: 'wx' });\n return true;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n\n if (code === 'EEXIST') return false;\n // The directory was removed between the mkdir and the write (e.g. a\n // concurrent output-directory clean); retry once.\n if (code === 'ENOENT' && attempt === 0) continue;\n\n throw error;\n }\n }\n\n return false;\n};\n\n/**\n * Rewrites the sentinel owned by this process, overwriting any existing file.\n */\nconst writeOwnedSentinel = async (\n sentinelFilePath: string,\n timestamp: number,\n status: SentinelStatus\n): Promise<void> => {\n try {\n await mkdir(dirname(sentinelFilePath), { recursive: true });\n await writeFile(sentinelFilePath, serializeSentinel(timestamp, status));\n } catch {}\n};\n\nconst removeSentinel = async (sentinelFilePath: string): Promise<void> => {\n try {\n await unlink(sentinelFilePath);\n } catch {}\n};\n\n/**\n * Ensures a callback function runs only once within a specified time window across multiple processes.\n * Uses a sentinel file to coordinate execution and prevent duplicate work.\n *\n * Processes that lose the race for the sentinel wait for the owner to finish\n * rather than running a competing copy of the callback — concurrent runs would\n * otherwise write to (and clean) the same output directory at the same time.\n *\n * @param sentinelFilePath - Path to the sentinel file used for coordination\n * @param callback - The function to execute (should be async)\n * @param options - The options for the runOnce function\n *\n * @example\n * ```typescript\n * await runOnce(\n * '/tmp/intlayer-sentinel',\n * async ({ renewLock }) => {\n * await cleanOutputDir(configuration); // may delete the sentinel\n * await renewLock();\n * await prepareIntlayer();\n * },\n * { cacheTimeoutMs: 30 * 1000 } // 30 seconds cache\n * );\n * ```\n *\n * @throws {Error} When there are unexpected filesystem errors\n */\nexport const runOnce = async (\n sentinelFilePath: string,\n callback: (context: RunOnceContext) => void | Promise<void>,\n options?: RunOnceOptions\n) => {\n const { onIsCached, cacheTimeoutMs, forceRun, lockWaitTimeoutMs } = {\n ...DEFAULT_RUN_ONCE_OPTIONS,\n ...(options ?? {}),\n };\n const currentTimestamp = Date.now();\n const waitDeadline = currentTimestamp + lockWaitTimeoutMs;\n\n // Acquisition loop: read the sentinel, then either return early (fresh cache),\n // wait for the current owner, or take the lock. Every branch either returns or\n // makes progress, so the loop always terminates.\n while (true) {\n const sentinelState = await readSentinelState(sentinelFilePath);\n\n if (sentinelState) {\n const sentinelAge = Date.now() - sentinelState.mtimeMs;\n\n if (sentinelState.status === 'running') {\n const isAbandoned =\n sentinelAge > lockWaitTimeoutMs ||\n !isOwnerProcessAlive(sentinelState.pid);\n\n if (!isAbandoned && Date.now() < waitDeadline) {\n await delay(LOCK_POLL_INTERVAL_MS);\n continue;\n }\n\n // The owner died or overran the timeout: reclaim the sentinel.\n await removeSentinel(sentinelFilePath);\n continue;\n }\n\n const isCacheValid =\n !forceRun &&\n sentinelAge <= cacheTimeoutMs &&\n sentinelState.version === packageJson.version;\n\n if (isCacheValid) {\n await onIsCached?.();\n return;\n }\n\n await removeSentinel(sentinelFilePath);\n }\n\n const hasAcquiredSentinel = await acquireSentinel(\n sentinelFilePath,\n currentTimestamp\n );\n\n if (hasAcquiredSentinel) break;\n\n // Another process won the race in the meantime: loop back and wait for it.\n // The delay also guarantees the loop yields, so a sentinel being repeatedly\n // created and removed can never turn into a busy wait.\n await delay(LOCK_POLL_INTERVAL_MS);\n }\n\n ownedSentinelFilePaths.add(sentinelFilePath);\n\n const renewLock = () =>\n writeOwnedSentinel(sentinelFilePath, currentTimestamp, 'running');\n\n try {\n await callback({ renewLock });\n\n // Mark the run as finished, re-creating the sentinel if the callback\n // deleted it (e.g. by cleaning the output directory).\n await writeOwnedSentinel(sentinelFilePath, currentTimestamp, 'done');\n } catch {\n await removeSentinel(sentinelFilePath); // Remove sentinel file if an error occurs\n } finally {\n ownedSentinelFilePaths.delete(sentinelFilePath);\n }\n};\n"],"mappings":"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"}