@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.1

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 (291) hide show
  1. package/bin/cliOperations.ts +12 -0
  2. package/bin/restart.ts +66 -6
  3. package/components/Application.ts +1134 -112
  4. package/components/OptionsWatcher.ts +368 -102
  5. package/components/Scope.ts +20 -1
  6. package/components/componentLoader.ts +43 -4
  7. package/components/deploymentOperations.ts +4 -1
  8. package/components/deploymentRecorder.ts +9 -2
  9. package/components/operations.js +284 -52
  10. package/components/operationsValidation.js +49 -2
  11. package/components/packageComponent.ts +25 -1
  12. package/components/requestRestart.ts +11 -0
  13. package/config/RootConfigWatcher.ts +191 -37
  14. package/config/configReadRetry.ts +62 -0
  15. package/config/configUtils.ts +78 -26
  16. package/config/parseConfigFile.ts +34 -0
  17. package/config/readConfigFileSync.ts +44 -0
  18. package/config/watcherArming.ts +59 -0
  19. package/config-root.schema.json +4 -0
  20. package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
  21. package/dist/bin/cliOperations.js +13 -0
  22. package/dist/bin/cliOperations.js.map +1 -1
  23. package/dist/bin/restart.js +42 -6
  24. package/dist/bin/restart.js.map +1 -1
  25. package/dist/components/Application.d.ts +104 -9
  26. package/dist/components/Application.js +954 -102
  27. package/dist/components/Application.js.map +1 -1
  28. package/dist/components/OptionsWatcher.d.ts +4 -1
  29. package/dist/components/OptionsWatcher.js +378 -104
  30. package/dist/components/OptionsWatcher.js.map +1 -1
  31. package/dist/components/Scope.js +15 -1
  32. package/dist/components/Scope.js.map +1 -1
  33. package/dist/components/componentLoader.js +35 -3
  34. package/dist/components/componentLoader.js.map +1 -1
  35. package/dist/components/deploymentOperations.js +4 -1
  36. package/dist/components/deploymentOperations.js.map +1 -1
  37. package/dist/components/deploymentRecorder.d.ts +4 -2
  38. package/dist/components/deploymentRecorder.js +1 -0
  39. package/dist/components/deploymentRecorder.js.map +1 -1
  40. package/dist/components/operations.d.ts +28 -0
  41. package/dist/components/operations.js +263 -54
  42. package/dist/components/operations.js.map +1 -1
  43. package/dist/components/operationsValidation.js +48 -2
  44. package/dist/components/operationsValidation.js.map +1 -1
  45. package/dist/components/packageComponent.js +24 -0
  46. package/dist/components/packageComponent.js.map +1 -1
  47. package/dist/components/requestRestart.d.ts +1 -0
  48. package/dist/components/requestRestart.js +7 -0
  49. package/dist/components/requestRestart.js.map +1 -1
  50. package/dist/config/RootConfigWatcher.d.ts +2 -0
  51. package/dist/config/RootConfigWatcher.js +189 -35
  52. package/dist/config/RootConfigWatcher.js.map +1 -1
  53. package/dist/config/configReadRetry.d.ts +8 -0
  54. package/dist/config/configReadRetry.js +62 -0
  55. package/dist/config/configReadRetry.js.map +1 -0
  56. package/dist/config/configUtils.d.ts +10 -9
  57. package/dist/config/configUtils.js +63 -27
  58. package/dist/config/configUtils.js.map +1 -1
  59. package/dist/config/parseConfigFile.d.ts +4 -0
  60. package/dist/config/parseConfigFile.js +35 -0
  61. package/dist/config/parseConfigFile.js.map +1 -0
  62. package/dist/config/readConfigFileSync.d.ts +1 -0
  63. package/dist/config/readConfigFileSync.js +47 -0
  64. package/dist/config/readConfigFileSync.js.map +1 -0
  65. package/dist/config/watcherArming.d.ts +15 -0
  66. package/dist/config/watcherArming.js +59 -0
  67. package/dist/config/watcherArming.js.map +1 -0
  68. package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
  69. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.js +4 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/json/systemSchema.json +3 -0
  74. package/dist/resources/DatabaseTransaction.d.ts +25 -0
  75. package/dist/resources/DatabaseTransaction.js +224 -18
  76. package/dist/resources/DatabaseTransaction.js.map +1 -1
  77. package/dist/resources/LMDBTransaction.d.ts +2 -1
  78. package/dist/resources/LMDBTransaction.js +22 -3
  79. package/dist/resources/LMDBTransaction.js.map +1 -1
  80. package/dist/resources/PrimaryRocksDatabase.js +22 -6
  81. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  82. package/dist/resources/RecordEncoder.d.ts +1 -1
  83. package/dist/resources/RecordEncoder.js +21 -5
  84. package/dist/resources/RecordEncoder.js.map +1 -1
  85. package/dist/resources/Resource.js +97 -13
  86. package/dist/resources/Resource.js.map +1 -1
  87. package/dist/resources/ResourceInterface.d.ts +8 -0
  88. package/dist/resources/RocksIndexStore.js +2 -1
  89. package/dist/resources/RocksIndexStore.js.map +1 -1
  90. package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
  91. package/dist/resources/RocksTransactionLogStore.js +104 -33
  92. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  93. package/dist/resources/Table.d.ts +58 -7
  94. package/dist/resources/Table.js +1095 -346
  95. package/dist/resources/Table.js.map +1 -1
  96. package/dist/resources/analytics/write.js +10 -3
  97. package/dist/resources/analytics/write.js.map +1 -1
  98. package/dist/resources/auditStore.d.ts +170 -0
  99. package/dist/resources/auditStore.js +457 -11
  100. package/dist/resources/auditStore.js.map +1 -1
  101. package/dist/resources/dataLoader.js +3 -4
  102. package/dist/resources/dataLoader.js.map +1 -1
  103. package/dist/resources/databases.d.ts +16 -13
  104. package/dist/resources/databases.js +624 -176
  105. package/dist/resources/databases.js.map +1 -1
  106. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  107. package/dist/resources/derivedIndexRegistry.js +68 -0
  108. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  109. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  110. package/dist/resources/derivedIndexRuntime.js +2027 -0
  111. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  112. package/dist/resources/graphql.js +3 -2
  113. package/dist/resources/graphql.js.map +1 -1
  114. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
  115. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
  116. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  117. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  118. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  119. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  120. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  121. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  122. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  123. package/dist/resources/nodeIdMapping.d.ts +5 -0
  124. package/dist/resources/nodeIdMapping.js +49 -0
  125. package/dist/resources/nodeIdMapping.js.map +1 -1
  126. package/dist/resources/recordLock.d.ts +47 -4
  127. package/dist/resources/recordLock.js +138 -7
  128. package/dist/resources/recordLock.js.map +1 -1
  129. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  130. package/dist/resources/recordLockCoordinator.js +2565 -0
  131. package/dist/resources/recordLockCoordinator.js.map +1 -0
  132. package/dist/resources/replayLogs.js +5 -0
  133. package/dist/resources/replayLogs.js.map +1 -1
  134. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  135. package/dist/resources/replicatedApplyFailure.js +63 -0
  136. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  137. package/dist/resources/scheduler/scheduler.js +3 -3
  138. package/dist/resources/scheduler/scheduler.js.map +1 -1
  139. package/dist/resources/search.d.ts +10 -4
  140. package/dist/resources/search.js +160 -40
  141. package/dist/resources/search.js.map +1 -1
  142. package/dist/resources/tracked.d.ts +5 -1
  143. package/dist/resources/tracked.js +74 -23
  144. package/dist/resources/tracked.js.map +1 -1
  145. package/dist/security/jsLoader.js +6 -4
  146. package/dist/security/jsLoader.js.map +1 -1
  147. package/dist/server/REST.js +33 -2
  148. package/dist/server/REST.js.map +1 -1
  149. package/dist/server/http.d.ts +5 -1
  150. package/dist/server/http.js +34 -2
  151. package/dist/server/http.js.map +1 -1
  152. package/dist/server/serverHelpers/Headers.d.ts +2 -0
  153. package/dist/server/serverHelpers/Headers.js +6 -0
  154. package/dist/server/serverHelpers/Headers.js.map +1 -1
  155. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  156. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  157. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  158. package/dist/server/serverHelpers/Request.d.ts +5 -10
  159. package/dist/server/serverHelpers/Request.js +38 -136
  160. package/dist/server/serverHelpers/Request.js.map +1 -1
  161. package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
  162. package/dist/server/serverHelpers/contentTypes.js +189 -15
  163. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  164. package/dist/server/serverHelpers/serverUtilities.js +96 -17
  165. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  166. package/dist/server/storageReclamation.js +1 -1
  167. package/dist/server/storageReclamation.js.map +1 -1
  168. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  169. package/dist/server/threads/isolatedApplications.js +171 -0
  170. package/dist/server/threads/isolatedApplications.js.map +1 -0
  171. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  172. package/dist/server/threads/logRotationTransport.js +33 -0
  173. package/dist/server/threads/logRotationTransport.js.map +1 -0
  174. package/dist/server/threads/manageThreads.d.ts +64 -6
  175. package/dist/server/threads/manageThreads.js +261 -12
  176. package/dist/server/threads/manageThreads.js.map +1 -1
  177. package/dist/server/threads/socketRouter.d.ts +1 -0
  178. package/dist/server/threads/socketRouter.js +196 -13
  179. package/dist/server/threads/socketRouter.js.map +1 -1
  180. package/dist/server/threads/threadServer.js +30 -7
  181. package/dist/server/threads/threadServer.js.map +1 -1
  182. package/dist/utility/errors/hdbError.d.ts +24 -0
  183. package/dist/utility/errors/hdbError.js +58 -1
  184. package/dist/utility/errors/hdbError.js.map +1 -1
  185. package/dist/utility/hdbTerms.d.ts +2 -0
  186. package/dist/utility/hdbTerms.js +2 -0
  187. package/dist/utility/hdbTerms.js.map +1 -1
  188. package/dist/utility/logging/harper_logger.js +217 -38
  189. package/dist/utility/logging/harper_logger.js.map +1 -1
  190. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  191. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  192. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  193. package/dist/utility/logging/logRotation.d.ts +46 -0
  194. package/dist/utility/logging/logRotation.js +365 -0
  195. package/dist/utility/logging/logRotation.js.map +1 -0
  196. package/dist/utility/logging/logRotator.d.ts +1 -1
  197. package/dist/utility/logging/logRotator.js +172 -92
  198. package/dist/utility/logging/logRotator.js.map +1 -1
  199. package/dist/utility/npmUtilities.js +6 -4
  200. package/dist/utility/npmUtilities.js.map +1 -1
  201. package/dist/utility/watcherFallback.d.ts +0 -45
  202. package/dist/utility/watcherFallback.js +1 -125
  203. package/dist/utility/watcherFallback.js.map +1 -1
  204. package/dist/validation/configValidator.js +6 -3
  205. package/dist/validation/configValidator.js.map +1 -1
  206. package/index.ts +6 -0
  207. package/json/systemSchema.json +3 -0
  208. package/npm-shrinkwrap.json +131 -41
  209. package/package.json +10 -3
  210. package/resources/DESIGN.md +124 -19
  211. package/resources/DatabaseTransaction.ts +230 -17
  212. package/resources/LMDBTransaction.ts +21 -3
  213. package/resources/PrimaryRocksDatabase.ts +20 -7
  214. package/resources/RecordEncoder.ts +25 -5
  215. package/resources/Resource.ts +97 -13
  216. package/resources/ResourceInterface.ts +8 -0
  217. package/resources/RocksIndexStore.ts +2 -1
  218. package/resources/RocksTransactionLogStore.ts +111 -31
  219. package/resources/Table.ts +1224 -393
  220. package/resources/analytics/write.ts +10 -3
  221. package/resources/auditStore.ts +460 -11
  222. package/resources/dataLoader.ts +3 -4
  223. package/resources/databases.ts +610 -146
  224. package/resources/derivedIndexRegistry.ts +56 -0
  225. package/resources/derivedIndexRuntime.ts +2292 -0
  226. package/resources/graphql.ts +3 -2
  227. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
  228. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  229. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  230. package/resources/nodeIdMapping.ts +50 -0
  231. package/resources/recordLock.ts +173 -7
  232. package/resources/recordLockCoordinator.ts +3043 -0
  233. package/resources/replayLogs.ts +5 -0
  234. package/resources/replicatedApplyFailure.ts +77 -0
  235. package/resources/scheduler/scheduler.ts +4 -4
  236. package/resources/search.ts +169 -49
  237. package/resources/tracked.ts +73 -22
  238. package/security/jsLoader.ts +6 -4
  239. package/server/DESIGN.md +11 -0
  240. package/server/REST.ts +36 -3
  241. package/server/http.ts +34 -2
  242. package/server/serverHelpers/Headers.ts +5 -1
  243. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  244. package/server/serverHelpers/Request.ts +33 -131
  245. package/server/serverHelpers/contentTypes.ts +188 -15
  246. package/server/serverHelpers/serverUtilities.ts +143 -24
  247. package/server/storageReclamation.ts +2 -2
  248. package/server/threads/isolatedApplications.ts +157 -0
  249. package/server/threads/logRotationTransport.ts +40 -0
  250. package/server/threads/manageThreads.js +254 -12
  251. package/server/threads/socketRouter.ts +217 -11
  252. package/server/threads/threadServer.js +30 -7
  253. package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
  254. package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
  255. package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
  256. package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
  257. package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
  258. package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
  259. package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
  260. package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
  261. package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
  262. package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
  263. package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
  264. package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
  265. package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
  266. package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
  267. package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
  268. package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
  269. package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
  270. package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
  271. package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
  272. package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
  273. package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
  274. package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
  275. package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
  276. package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
  277. package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
  278. package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
  279. package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
  280. package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
  281. package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
  282. package/studio/web/index.html +1 -1
  283. package/utility/errors/hdbError.ts +54 -0
  284. package/utility/hdbTerms.ts +2 -0
  285. package/utility/logging/harper_logger.ts +209 -30
  286. package/utility/logging/logGenerationCoordinator.ts +196 -0
  287. package/utility/logging/logRotation.ts +367 -0
  288. package/utility/logging/logRotator.ts +196 -91
  289. package/utility/npmUtilities.ts +6 -4
  290. package/utility/watcherFallback.ts +0 -122
  291. package/validation/configValidator.ts +6 -3
@@ -13,6 +13,8 @@ import { PACKAGE_ROOT } from '../../utility/packageUtils.js';
13
13
  import { _assignPackageExport } from '../../globals.js';
14
14
  import { Console } from 'console';
15
15
  import { inspect, types } from 'node:util';
16
+ import { createRotationGuard, INVALID_MAX_SIZE_MSG, parseMaxSize, resolveRotatedLogDir } from './logRotation.ts';
17
+ import { registerLogSink } from './logGenerationCoordinator.ts';
16
18
 
17
19
  const { isNativeError } = types;
18
20
  // store the native write function so we can call it after we write to the log file (and store it on process.stdout
@@ -47,6 +49,18 @@ const DEFAULT_CONFIG_FILE = join(PACKAGE_ROOT, 'static', hdbTerms.HDB_DEFAULT_CO
47
49
 
48
50
  const CLOSE_LOG_FD_TIMEOUT = 10000;
49
51
 
52
+ // Above the module-scope initLogSettings() call below, which reaches getFileLogger: a const
53
+ // declared after it is still in its temporal dead zone when the file sink first reads one.
54
+ const LOG_TIME_USAGE_THRESHOLD = 100;
55
+ // How long to write straight to stdio after the log file refuses an append.
56
+ const APPEND_RETRY_COOLDOWN = 5000;
57
+ // Ceiling on how often a failing rotation may report itself; the failure repeats every check point.
58
+ const ROTATION_REPORT_INTERVAL = 60000;
59
+ const SET_ROTATION_POLICY = Symbol('setRotationPolicy');
60
+ const GET_ROTATION_POLICY = Symbol('getRotationPolicy');
61
+ const FILE_ROTATION_SOURCE = Symbol('fileRotationSource');
62
+ const SERVICE_NAME = workerData?.name?.replace(/ /g, '-') || 'main';
63
+
50
64
  let logConsole;
51
65
  let log_to_file;
52
66
  let logToStdstreams;
@@ -109,7 +123,20 @@ export function updateLogger(logger: any, logOptions: any, name?: string, mainLo
109
123
  // rotation entirely (#1877). Excluded when `logger` IS mainLoggerRef: the fallback would just
110
124
  // reassign mainLogger.rotation to itself, making it impossible to ever clear main's rotation.
111
125
  const inheritedRotation = logOptions.rotation ?? (logger === mainLoggerRef ? undefined : mainLoggerRef?.rotation);
112
- if (inheritedRotation && inheritedRotation === mainLoggerRef?.rotation) {
126
+ const loggerPolicy = logger[GET_ROTATION_POLICY]?.();
127
+ const mainPolicy = mainLoggerRef?.[GET_ROTATION_POLICY]?.();
128
+ const inheritsRotation = logOptions.rotation == null && logger !== mainLoggerRef;
129
+ const rotationSource = inheritsRotation ? mainPolicy?.source : loggerPolicy?.ownSource;
130
+ let path = logOptions.path;
131
+ if (path) {
132
+ if (!logOptions.root) logOptions.root = pathModule.dirname(path);
133
+ } else if (logOptions.root) {
134
+ path = join(logOptions.root, logName);
135
+ } else {
136
+ path = mainLoggerRef.path;
137
+ if (!logOptions.root) logOptions.root = pathModule.dirname(path);
138
+ }
139
+ if (inheritedRotation && inheritedRotation === mainLoggerRef?.rotation && path !== mainLoggerRef?.path) {
113
140
  // This logger has no rotation block of its own and is inheriting main's wholesale — but
114
141
  // inheriting `rotation.path` too would point this logger's archives at wherever main
115
142
  // archives to. If this logger's file lives on a different filesystem/volume than that
@@ -120,20 +147,17 @@ export function updateLogger(logger: any, logOptions: any, name?: string, mainLo
120
147
  // goes through `logOptions.rotation` above, not this fallback. Clone rather than mutate:
121
148
  // `inheritedRotation` is the literal object shared by mainLogger.rotation and every other
122
149
  // component that inherited it.
150
+ //
151
+ // Only when the paths differ. A logger writing to main's own file shares main's rotation
152
+ // state — file loggers are cached per path — so stripping there discards the operator's
153
+ // configured rotation.path for the main log itself, which no EXDEV risk justifies.
123
154
  const { path: _inheritedPath, ...rotationWithoutPath } = inheritedRotation;
124
- logger.rotation = rotationWithoutPath;
125
- } else {
126
- logger.rotation = inheritedRotation;
127
- }
128
- let path = logOptions.path;
129
- if (path) {
130
- if (!logOptions.root) logOptions.root = pathModule.dirname(path);
131
- } else if (logOptions.root) {
132
- path = join(logOptions.root, logName);
155
+ setRotationPolicy(rotationWithoutPath);
133
156
  } else {
134
- path = mainLoggerRef.path;
135
- if (!logOptions.root) logOptions.root = pathModule.dirname(path);
157
+ setRotationPolicy(inheritedRotation);
136
158
  }
159
+ // After the rotation above: assigning `path` goes through the setter that rebuilds this path's
160
+ // file logger, and it reads `logger.rotation` as it stands at that moment.
137
161
  if (path) logger.path = path;
138
162
  else console.error('No path for logger', logOptions);
139
163
  logger.level = LOG_LEVEL_HIERARCHY[logOptions.level] ?? mainLoggerRef?.level ?? LOG_LEVEL_HIERARCHY.info;
@@ -142,6 +166,11 @@ export function updateLogger(logger: any, logOptions: any, name?: string, mainLo
142
166
  // if there is a configured tag or if a component is logging to default/main log path, use the component name as the tag
143
167
  // to differentiate it
144
168
  logger.tag = logOptions.tag ?? ((mainLoggerRef.path === logger.path || externalLogger.path === logger.path) && name);
169
+
170
+ function setRotationPolicy(rotation) {
171
+ if (logger[SET_ROTATION_POLICY]) logger[SET_ROTATION_POLICY](rotation, rotationSource, inheritsRotation);
172
+ else logger.rotation = rotation;
173
+ }
145
174
  }
146
175
  // creates a logger where the methods are only defined if they are within the log level.
147
176
  // Using this conditional logger means that every method call must be optional like log.trace?.('message),
@@ -174,7 +203,19 @@ async function updateLogSettings() {
174
203
  // TODO: Any way to differentiate changes that we can and can't handle?
175
204
  rootConfig.on('change', updateLogSettings);
176
205
  }
177
- let rootConfigObject = rootConfig.config;
206
+ applyLogSettings(rootConfig.config);
207
+ }
208
+
209
+ // Separate from the watcher wiring above because the watcher it builds reads a process-wide path,
210
+ // which leaves no way to exercise these settings against a given config.
211
+ function applyLogSettings(rootConfigObject: any) {
212
+ // `ready` settles on every terminal read outcome, including ones that carry no config at all —
213
+ // see DESIGN.md, "Every terminal read outcome settles the barrier". Applying that as settings
214
+ // would not be "the defaults": `updateLogger` reads an absent `rotation` as rotation off and an
215
+ // absent `console` as console off, so an unreadable config would silently disable logging.
216
+ // What `initLogSettings()` established at startup stands until a real config arrives, which
217
+ // is why the watcher settles those outcomes with no config rather than an empty one.
218
+ if (!rootConfigObject || typeof rootConfigObject !== 'object') return;
178
219
  const logOptions = rootConfigObject.logging ?? {};
179
220
  // Resolve relative paths against rootPath from the same config
180
221
  const rootPath = rootConfigObject.rootPath;
@@ -197,7 +238,9 @@ async function updateLogSettings() {
197
238
  for (const name in rootConfigObject) {
198
239
  // we now scan each component to see if it has logging individual configured
199
240
  const component = rootConfigObject[name];
200
- if (component.logging) {
241
+ // `myComponent:` with nothing under it parses to null, and an async listener's TypeError
242
+ // escapes the watcher's emit guard as an unhandled rejection.
243
+ if (component?.logging) {
201
244
  updateLogger(mainLogger.forComponent(name), component.logging, name);
202
245
  } else if (mainLogger.hasComponent(name)) {
203
246
  const componentLogger = mainLogger.forComponent(name);
@@ -399,6 +442,8 @@ module.exports = {
399
442
  // we can start using the RootConfigWatcher
400
443
  start: updateLogSettings,
401
444
  startOnMainThread: updateLogSettings,
445
+ // Test-only: applies a config without the process-wide watcher `updateLogSettings` builds.
446
+ _applyLogSettingsForTests: applyLogSettings,
402
447
  errorToString,
403
448
  errorForLog,
404
449
  inspectForLog,
@@ -463,6 +508,14 @@ export function logsAtLevel(level: any) {
463
508
  export function initLogSettings(forceInit = false) {
464
509
  try {
465
510
  if (hdbProperties === undefined || forceInit) {
511
+ if (forceInit && mainLogger?.[SET_ROTATION_POLICY]) {
512
+ const currentPath = mainLogger.path;
513
+ if (currentPath) {
514
+ const { ownSource } = mainLogger[GET_ROTATION_POLICY]();
515
+ mainLogger[SET_ROTATION_POLICY](undefined, ownSource, false);
516
+ mainLogger.path = currentPath;
517
+ }
518
+ }
466
519
  closeLogFile();
467
520
  const bootPropsFilePath = getPropsFilePath();
468
521
  let properties = assignCMDENVVariables(['ROOTPATH']);
@@ -643,7 +696,6 @@ export function suppressLogging(callback) {
643
696
  }
644
697
  }
645
698
 
646
- const SERVICE_NAME = workerData?.name?.replace(/ /g, '-') || 'main';
647
699
  // these are used to store information about the current service and tag so we can prepend them to the log during
648
700
  // the writes, without having to pass the information through the Console instance
649
701
  let currentLevel = 'info'; // default is info
@@ -658,7 +710,14 @@ export function createLogger(options: any = {} as any) {
658
710
  isExternalInstance,
659
711
  writeToLog,
660
712
  component,
713
+ rotationSource,
714
+ rotationInherited = false,
661
715
  }: any = options;
716
+ const ownRotationSource = Symbol('rotationPolicySource');
717
+ let currentRotationSource = rotationSource ?? ownRotationSource;
718
+ let previousRotationSource;
719
+ let currentRotationInherited = rotationInherited;
720
+ let rotationChangeIsAuthoritative = rotation !== undefined;
662
721
  if (!logLevel) logLevel = 'info';
663
722
  let level = typeof logLevel === 'number' ? logLevel : LOG_LEVEL_HIERARCHY[logLevel];
664
723
  let logger;
@@ -702,7 +761,8 @@ export function createLogger(options: any = {} as any) {
702
761
  }
703
762
  } else if (logToStdstreams) process.stderr.write(log);
704
763
  }
705
- let logToFile = logFilePath && getFileLogger(logFilePath, rotation, isExternalInstance);
764
+ let logToFile = logFilePath && getFileLogger(logFilePath, rotation, isExternalInstance, rotationPolicy());
765
+ rotationPolicyApplied();
706
766
  function logPrepend(write) {
707
767
  return {
708
768
  write(log) {
@@ -725,6 +785,14 @@ export function createLogger(options: any = {} as any) {
725
785
  },
726
786
  level
727
787
  );
788
+ logger[SET_ROTATION_POLICY] = function (newRotation, source = ownRotationSource, inherited = false) {
789
+ previousRotationSource = currentRotationSource;
790
+ currentRotationSource = source ?? ownRotationSource;
791
+ currentRotationInherited = inherited;
792
+ rotationChangeIsAuthoritative = true;
793
+ logger.rotation = newRotation;
794
+ };
795
+ logger[GET_ROTATION_POLICY] = () => ({ ownSource: ownRotationSource, source: currentRotationSource });
728
796
  updateConditional(logger);
729
797
  logger.path = logFilePath;
730
798
  Object.defineProperty(logger, 'path', {
@@ -733,7 +801,9 @@ export function createLogger(options: any = {} as any) {
733
801
  },
734
802
  set(path) {
735
803
  logFilePath = path;
736
- logToFile = getFileLogger(logFilePath, logger.rotation, isExternalInstance);
804
+ if (!logFilePath) return;
805
+ logToFile = getFileLogger(logFilePath, logger.rotation, isExternalInstance, rotationPolicy());
806
+ rotationPolicyApplied();
737
807
  if (isExternalInstance) writeToLogFile = logToFile;
738
808
  },
739
809
  enumerable: true,
@@ -746,12 +816,15 @@ export function createLogger(options: any = {} as any) {
746
816
  let componentLogger = components.get(name);
747
817
  if (!componentLogger) {
748
818
  const protoLogger = isExternal ? externalLogger : logger;
819
+ const protoRotationPolicy = protoLogger[GET_ROTATION_POLICY]?.();
749
820
  componentLogger = createLogger({
750
821
  path: protoLogger.path,
751
822
  level: protoLogger.level,
752
823
  stdStreams: protoLogger.logToStdstreams,
753
824
  isExternalInstance: isExternal || name === 'external',
754
825
  rotation: protoLogger.rotation,
826
+ rotationSource: protoRotationPolicy?.source,
827
+ rotationInherited: true,
755
828
  writeToLog,
756
829
  component: true,
757
830
  });
@@ -767,34 +840,83 @@ export function createLogger(options: any = {} as any) {
767
840
  logger.components = components;
768
841
  }
769
842
  return logger;
843
+
844
+ function rotationPolicy() {
845
+ return {
846
+ source: currentRotationSource,
847
+ previousSource: previousRotationSource,
848
+ inherited: currentRotationInherited,
849
+ authoritative: rotationChangeIsAuthoritative,
850
+ };
851
+ }
852
+
853
+ function rotationPolicyApplied() {
854
+ previousRotationSource = currentRotationSource;
855
+ rotationChangeIsAuthoritative = false;
856
+ }
770
857
  }
771
- const LOG_TIME_USAGE_THRESHOLD = 100;
772
- // How long to write straight to stdio after the log file refuses an append.
773
- const APPEND_RETRY_COOLDOWN = 5000;
774
858
  /**
775
859
  * Get the file logger for the given path. If it doesn't exist, create it.
776
860
  * @param path
777
861
  * @param isExternalInstance
778
862
  * @return {any}
779
863
  */
780
- function getFileLogger(path, rotation, isExternalInstance) {
864
+ function getFileLogger(path, rotation, isExternalInstance, rotationPolicy) {
781
865
  let logger = fileLoggers.get(path);
782
866
  let logFD, loggedFDError, loggedAppendError, logTimer, retryAppendAfter;
783
867
  let logBuffer;
868
+ let rotationProblemNotice;
784
869
  let logTimeUsage = 0;
870
+ let rotationGuard,
871
+ logFDIdentity,
872
+ nextRotationReport = 0;
785
873
  if (!logger) {
786
874
  logger = logToFile;
787
875
  logger.closeLogFile = closeLogFile;
876
+ // The guard has to be installed through the live closure: later calls for this path get the
877
+ // cached logger back, and their own logFD/logBuffer are dead.
878
+ logger.installRotationGuard = installRotationGuard;
788
879
  logger.path = path;
789
880
  fileLoggers.set(path, logger);
881
+ // Registered by the sink, not by the size guard: a thread that writes this file but has no
882
+ // guard (no maxSize, or rotation driven only by interval) still holds a descriptor, and an
883
+ // answer from it has to mean "released" rather than only "handler ran".
884
+ registerLogSink(path, { identity: () => logFDIdentity, close: closeLogFile });
885
+ }
886
+ const currentSource = logger[FILE_ROTATION_SOURCE];
887
+ const sameSource = currentSource === rotationPolicy.source;
888
+ const replacesPreviousSource = currentSource !== undefined && currentSource === rotationPolicy.previousSource;
889
+ const canInstall =
890
+ rotation !== undefined &&
891
+ (!rotationPolicy.inherited || currentSource === undefined || sameSource || replacesPreviousSource);
892
+ const canClear = rotation === undefined && rotationPolicy.authoritative && (sameSource || replacesPreviousSource);
893
+ let reconfigured = false;
894
+ if (canInstall) {
895
+ logger[FILE_ROTATION_SOURCE] = rotationPolicy.source;
896
+ reconfigured = JSON.stringify(rotation) !== JSON.stringify(logger.rotation);
790
897
  }
791
- if (isMainThread && JSON.stringify(rotation) !== JSON.stringify(logger.rotation)) {
898
+ if (reconfigured) {
792
899
  logger.rotation = rotation;
900
+ // Every writing thread, and synchronously: request logging is produced by the HTTP workers, and
901
+ // at the rates that make maxSize matter a deferred guard misses megabytes before it exists.
902
+ logger.installRotationGuard(rotation);
903
+ } else if (canClear) {
904
+ logger.rotation = undefined;
905
+ logger[FILE_ROTATION_SOURCE] = undefined;
906
+ logger.installRotationGuard(undefined);
907
+ reconfigured = true;
908
+ }
909
+ if (isMainThread && reconfigured) {
793
910
  setTimeout(() => {
794
- logger.rotator?.end();
795
- if (!rotation) return;
796
- const { logRotator } = require('./logRotator');
911
+ // Everything inside the try: a throw from a timer callback is unhandled, and neither
912
+ // require('./logRotator') (which reaches environmentManager's synchronous init) nor a
913
+ // rotator teardown may take the process down over log rotation (#847).
797
914
  try {
915
+ const previousRotator = logger.rotator;
916
+ logger.rotator = undefined;
917
+ previousRotator?.end();
918
+ if (!rotation) return;
919
+ const { logRotator } = require('./logRotator');
798
920
  logger.rotator = logRotator({
799
921
  logger,
800
922
  ...rotation,
@@ -805,6 +927,46 @@ function getFileLogger(path, rotation, isExternalInstance) {
805
927
  }, 100);
806
928
  }
807
929
  return logger;
930
+ function installRotationGuard(newRotation) {
931
+ rotationGuard = undefined;
932
+ if (!newRotation || newRotation.enabled === false) return;
933
+ const maxBytes = parseMaxSize(newRotation.maxSize);
934
+ if (!maxBytes) {
935
+ if (newRotation.maxSize != null) reportRotationProblem(`logging.rotation.maxSize: ${INVALID_MAX_SIZE_MSG}`);
936
+ return;
937
+ }
938
+ try {
939
+ rotationGuard = createRotationGuard({
940
+ logPath: path,
941
+ maxBytes,
942
+ rotatedLogDir: resolveRotatedLogDir(path, newRotation.path),
943
+ // From the rotation block rather than environmentManager (which logRotator still reads):
944
+ // environmentManager imports this module, so it cannot be reached from the write path.
945
+ compress: newRotation.compress,
946
+ getLogIdentity: () => logFDIdentity,
947
+ closeLogFile,
948
+ report: reportRotationProblem,
949
+ onRotated(archivePath) {
950
+ // Same shape as every other line in the file: written straight to the sink, this
951
+ // bypasses the prefix createLogger's logPrepend adds, and readLog parses by level.
952
+ logToFile(`[${SERVICE_NAME}/${threadId}] [notify]: hdb.log rotated, old log moved to ${archivePath}`);
953
+ },
954
+ });
955
+ } catch (error) {
956
+ reportRotationProblem(`log rotation is disabled for ${path}: ${error}`);
957
+ }
958
+ }
959
+ function reportRotationProblem(text) {
960
+ // Straight to stdio, never back through this sink: the sink is what rotation is failing on.
961
+ // Rate-limited rather than once-only, so a later, different failure is still reported.
962
+ if (nextRotationReport > performance.now()) return;
963
+ nextRotationReport = performance.now() + ROTATION_REPORT_INTERVAL;
964
+ const message = `Harper log rotation problem — ${text}`;
965
+ // Scripted services discard stdio. Preserve the warning in the active file on the next
966
+ // successful append as well, without recursing through the rotation guard.
967
+ rotationProblemNotice = `${new Date().toISOString()} [${SERVICE_NAME}/${threadId}] [error]: ${message}\n`;
968
+ writeToStdioDirectly(process.stderr, `${message}\n`);
969
+ }
808
970
  function logToFile(log) {
809
971
  let entry = `${new Date().toISOString()} ${log}${log.endsWith('\n') ? '' : '\n'}`;
810
972
  if (logBuffer) {
@@ -831,18 +993,28 @@ function getFileLogger(path, rotation, isExternalInstance) {
831
993
  }
832
994
  // this is called on a timer, and will write the log buffer to the file
833
995
  function logQueuedData(entry?: any) {
834
- openLogFile(undefined);
835
996
  const payload = logBuffer ? logBuffer.join('') : entry;
997
+ // Released before anything can re-enter: the rotation notice is written from inside the append
998
+ // below, and a re-entrant flush still holding this batch would write it twice.
999
+ logBuffer = null;
1000
+ if (payload === undefined) return;
836
1001
  // A file that just refused a write will refuse the next one too, and every attempt costs a
837
1002
  // failed syscall plus a thrown error on whatever path is logging - which, on a full volume,
838
1003
  // is the request path at full rate. Go straight to stdio until the cooldown expires.
839
- if (logFD && !(retryAppendAfter > performance.now())) {
1004
+ // Opened after the guard: a guard recovering by rotation closes this descriptor first.
1005
+ const mayAppend = !(retryAppendAfter > performance.now()) && (rotationGuard?.beforeAppend() ?? true);
1006
+ openLogFile(undefined);
1007
+ if (logFD && mayAppend) {
840
1008
  let startTime = performance.now();
841
1009
  try {
842
- fs.appendFileSync(logFD, payload);
1010
+ const appendPayload = rotationProblemNotice ? rotationProblemNotice + payload : payload;
1011
+ const appendBuffer = Buffer.from(appendPayload);
1012
+ fs.appendFileSync(logFD, appendBuffer);
1013
+ rotationProblemNotice = undefined;
843
1014
  // Both cleared, so a volume that fills again months later reports itself again
844
1015
  retryAppendAfter = undefined;
845
1016
  loggedAppendError = false;
1017
+ rotationGuard?.recordWrite(appendBuffer.length);
846
1018
  } catch (error) {
847
1019
  retryAppendAfter = performance.now() + APPEND_RETRY_COOLDOWN;
848
1020
  // A log write must never take the process down: on an exhausted volume this throws from
@@ -855,7 +1027,6 @@ function getFileLogger(path, rotation, isExternalInstance) {
855
1027
  writeToStdioDirectly(process.stderr, `Harper cannot write to its log file: ${error}\n`);
856
1028
  }
857
1029
  writeToStdioDirectly(process.stdout, payload);
858
- logBuffer = null;
859
1030
  return;
860
1031
  }
861
1032
  let endTime = performance.now();
@@ -863,7 +1034,6 @@ function getFileLogger(path, rotation, isExternalInstance) {
863
1034
  // will start buffering
864
1035
  logTimeUsage = Math.max(endTime, logTimeUsage) + (endTime - startTime) * 50;
865
1036
  } else writeToStdioDirectly(process.stdout, payload);
866
- if (logBuffer) logBuffer = null;
867
1037
  }
868
1038
 
869
1039
  function closeLogFile(_unused?: any) {
@@ -871,6 +1041,7 @@ function getFileLogger(path, rotation, isExternalInstance) {
871
1041
  fs.closeSync(logFD);
872
1042
  } catch {}
873
1043
  logFD = null;
1044
+ logFDIdentity = null;
874
1045
  if (isExternalInstance) mainLogFd = null;
875
1046
  }
876
1047
 
@@ -878,6 +1049,14 @@ function getFileLogger(path, rotation, isExternalInstance) {
878
1049
  if (!logFD) {
879
1050
  try {
880
1051
  logFD = fs.openSync(path, 'a');
1052
+ // Which generation this descriptor belongs to, recorded once here so the size guard's
1053
+ // checkpoint needs a single pathname stat to tell whether the file has moved under it.
1054
+ try {
1055
+ const opened = fs.fstatSync(logFD);
1056
+ logFDIdentity = { ino: opened.ino, dev: opened.dev };
1057
+ } catch {
1058
+ logFDIdentity = null;
1059
+ }
881
1060
  if (isExternalInstance) mainLogFd = logFD;
882
1061
  } catch (error) {
883
1062
  if (error.code === 'ENOENT' && !isRetry) {
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ // Every isolate holds its own descriptor on the shared log file, so a rename leaves the others
4
+ // appending to the archived inode and destroying it loses whatever they write next. No size
5
+ // comparison can rule out a write that has not happened yet; enumerated in-process peers must answer.
6
+ //
7
+ // The thread layer injects the transport; harper_logger must never reach manageThreads.
8
+
9
+ import { statSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ import { isMainThread } from 'node:worker_threads';
12
+
13
+ export const LOG_GENERATION_ROTATED = 'log_generation_rotated';
14
+ export const LOG_GENERATION_CLOSED = 'log_generation_closed';
15
+
16
+ // Under manageThreads' 30s ITC default, so an unanswered generation is decided here (retain the
17
+ // plain archive) rather than by a transport backstop that cannot express "unproven".
18
+ const DEFAULT_QUIESCENCE_TIMEOUT = 15000;
19
+ // A tiny maxSize can rotate faster than a slow peer answers. Overflow is left as a plain archive
20
+ // for the audit sweep instead of retaining one timer and one mesh request per generation.
21
+ const MAX_PENDING_RELEASE_REQUESTS = 64;
22
+
23
+ interface RotationTransport {
24
+ broadcast(message: any): void;
25
+ sendToThread(threadId: number, message: any): void;
26
+ onMessage(type: string, handler: (message: any) => void): void;
27
+ onThreadExit(handler: (threadId: number) => void): void;
28
+ peerThreadIds(): Iterable<number>;
29
+ threadId: number;
30
+ quiescenceTimeout?: number;
31
+ }
32
+
33
+ let transport: RotationTransport | undefined;
34
+ type LogSink = { identity(): any; close(): void };
35
+ // A set per path, not one sink: harper_logger caches its file loggers by the raw configured path, so
36
+ // two spellings of one file are two sinks holding two descriptors on it, and a release has to close
37
+ // both. The key is resolved because retention compares resolved paths.
38
+ const sinksByPath = new Map<string, Set<LogSink>>();
39
+ const pendingByRequest = new Map<
40
+ string,
41
+ { expected: Set<number>; liveLogPaths: Set<string>; settle: (released: boolean) => void }
42
+ >();
43
+ let requestCounter = 0;
44
+
45
+ export function setRotationTransport(newTransport?: RotationTransport) {
46
+ transport = newTransport;
47
+ if (!newTransport) return;
48
+ newTransport.onMessage(LOG_GENERATION_ROTATED, (message) => {
49
+ releaseLocally(message);
50
+ newTransport.sendToThread(message.originator, {
51
+ type: LOG_GENERATION_CLOSED,
52
+ request: message.request,
53
+ threadId: newTransport.threadId,
54
+ // The paths this isolate writes. Components load in the workers, so a component's own log
55
+ // is registered only there, and the thread that runs retention would otherwise see a live
56
+ // file it has never heard of and delete it by age.
57
+ logPaths: message.stale ? [...sinksByPath.keys()] : undefined,
58
+ });
59
+ });
60
+ newTransport.onMessage(LOG_GENERATION_CLOSED, (message) => {
61
+ recordPeerResponse(message.request, message.threadId, message.logPaths);
62
+ });
63
+ newTransport.onThreadExit((threadId) => {
64
+ // An exited thread cannot hold a descriptor, so its exit answers everything it owed.
65
+ for (const request of [...pendingByRequest.keys()]) recordPeerResponse(request, threadId);
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Registered by the file sink, not the size guard: a thread with no guard still holds a descriptor,
71
+ * and its answer has to mean the descriptor is gone.
72
+ */
73
+ export function registerLogSink(logPath: string, sink: LogSink) {
74
+ const key = resolve(logPath);
75
+ const sinks = sinksByPath.get(key) ?? new Set<LogSink>();
76
+ sinks.add(sink);
77
+ sinksByPath.set(key, sinks);
78
+ }
79
+
80
+ export function unregisterLogSink(logPath: string, sink?: LogSink) {
81
+ const key = resolve(logPath);
82
+ if (!sink) return void sinksByPath.delete(key);
83
+ const sinks = sinksByPath.get(key);
84
+ if (!sinks?.delete(sink)) return;
85
+ if (sinks.size === 0) sinksByPath.delete(key);
86
+ }
87
+
88
+ export function nextGenerationId() {
89
+ return `${process.pid}-${transport?.threadId ?? 0}-${requestCounter++}`;
90
+ }
91
+
92
+ /**
93
+ * Ask every in-process peer this isolate can enumerate to release the descriptors this request
94
+ * names. `false` means the caller must leave those archives in place — it is never safe to unlink
95
+ * an inode a peer may still be appending to.
96
+ */
97
+ function requestRelease(message: any, deadline?: number): Promise<{ released: boolean; liveLogPaths: Set<string> }> {
98
+ releaseLocally(message);
99
+ const liveLogPaths = new Set<string>(sinksByPath.keys());
100
+ // No transport in a worker isolate means the mesh cannot be enumerated yet, not that there is
101
+ // nobody to ask: harper_logger installs the size guard at its own module load and manageThreads
102
+ // installs the transport at the end of its, so a worker rotating in that window would otherwise
103
+ // call an empty peer set a proof and unlink the generation its siblings are appending to. On the
104
+ // main thread the same state means no worker has been spawned, where zero peers is the truth.
105
+ if (!transport) return Promise.resolve({ released: isMainThread, liveLogPaths });
106
+ const expected = new Set<number>(transport.peerThreadIds());
107
+ if (expected.size === 0) return Promise.resolve({ released: true, liveLogPaths });
108
+ if (pendingByRequest.size >= MAX_PENDING_RELEASE_REQUESTS) return Promise.resolve({ released: false, liveLogPaths });
109
+ const quiescenceTimeout = transport.quiescenceTimeout ?? DEFAULT_QUIESCENCE_TIMEOUT;
110
+ if (deadline !== undefined && Date.now() + quiescenceTimeout > deadline)
111
+ return Promise.resolve({ released: false, liveLogPaths });
112
+ const request = message.request;
113
+ return new Promise((resolve) => {
114
+ let timer;
115
+ const settle = (released) => {
116
+ clearTimeout(timer);
117
+ pendingByRequest.delete(request);
118
+ resolve({ released, liveLogPaths });
119
+ };
120
+ pendingByRequest.set(request, { expected, liveLogPaths, settle });
121
+ timer = setTimeout(() => settle(false), quiescenceTimeout);
122
+ timer.unref?.();
123
+ transport.broadcast({ ...message, originator: transport.threadId });
124
+ });
125
+ }
126
+
127
+ /** One archived generation: release any descriptor still pointing at the inode that was renamed. */
128
+ export async function requestGenerationClose(generation: any, deadline?: number): Promise<boolean> {
129
+ return (
130
+ await requestRelease(
131
+ {
132
+ type: LOG_GENERATION_ROTATED,
133
+ request: generation.generation,
134
+ logPath: generation.logPath,
135
+ ino: generation.ino,
136
+ dev: generation.dev,
137
+ },
138
+ deadline
139
+ )
140
+ ).released;
141
+ }
142
+
143
+ /**
144
+ * Every generation but the live one, for every log this process writes, in a single round trip.
145
+ * Not scoped to one path: the archive directory holds the archives of every component and external
146
+ * log sharing it, and retention destroys those too.
147
+ */
148
+ export function requestStaleDescriptorRelease(
149
+ deadline?: number
150
+ ): Promise<{ released: boolean; liveLogPaths: Set<string> }> {
151
+ return requestRelease({ type: LOG_GENERATION_ROTATED, request: nextGenerationId(), stale: true }, deadline);
152
+ }
153
+
154
+ function recordPeerResponse(request: string, threadId: number, logPaths?: string[]) {
155
+ const pending = pendingByRequest.get(request);
156
+ if (!pending) return;
157
+ if (logPaths) for (const logPath of logPaths) pending.liveLogPaths.add(logPath);
158
+ pending.expected.delete(threadId);
159
+ if (pending.expected.size === 0) pending.settle(true);
160
+ }
161
+
162
+ function releaseLocally(message: any) {
163
+ if (message.stale) return releaseStaleDescriptors();
164
+ for (const sink of sinksByPath.get(resolve(message.logPath)) ?? []) {
165
+ const identity = sink.identity();
166
+ // No identity to compare (no descriptor open, or a filesystem that cannot report one) means
167
+ // closing is the only answer that can still be called a release.
168
+ if (!identity || !identity.ino || !message.ino || (identity.ino === message.ino && identity.dev === message.dev))
169
+ sink.close();
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Release every descriptor this isolate holds that is not on the live generation of its own path.
175
+ * Each sink decides for itself, so nothing has to name the live inode of a log it does not own.
176
+ */
177
+ function releaseStaleDescriptors() {
178
+ for (const [logPath, sinks] of sinksByPath) {
179
+ let live;
180
+ try {
181
+ live = statSync(logPath);
182
+ } catch {
183
+ for (const sink of sinks) sink.close();
184
+ continue;
185
+ }
186
+ for (const sink of sinks) {
187
+ const identity = sink.identity();
188
+ // No identity is not the same as no descriptor: openLogFile() leaves the descriptor open when
189
+ // its fstat fails. Skipping the sink and still answering "released" is what lets an archive be
190
+ // unlinked under it, so close it here as the per-generation path already does.
191
+ // `ino === 0` (some Windows filesystems) proves nothing for the same reason.
192
+ if (!identity || !identity.ino || !live.ino || identity.ino !== live.ino || identity.dev !== live.dev)
193
+ sink.close();
194
+ }
195
+ }
196
+ }