@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
@@ -0,0 +1,367 @@
1
+ 'use strict';
2
+
3
+ // The rotation primitives the log write path needs. Kept free of every Harper import so
4
+ // harper_logger.ts can pull it in eagerly: logRotator.ts reaches server/storageReclamation.ts ->
5
+ // manageThreads.js -> harper_logger.ts, which is why it may only ever be required lazily, and a
6
+ // guard installed on a timer is a guard the first megabytes of a burst are written without.
7
+
8
+ import {
9
+ createReadStream,
10
+ createWriteStream,
11
+ existsSync,
12
+ mkdirSync,
13
+ renameSync,
14
+ statSync,
15
+ promises as fsProm,
16
+ } from 'node:fs';
17
+ import { createGzip } from 'node:zlib';
18
+ import { pipeline } from 'node:stream/promises';
19
+ import { basename, dirname, extname, join, resolve } from 'node:path';
20
+ import { createHash } from 'node:crypto';
21
+ import { isMainThread, threadId } from 'node:worker_threads';
22
+ import { nextGenerationId, requestGenerationClose } from './logGenerationCoordinator.ts';
23
+
24
+ // Bounds each writer's blind window at one quantum plus the flush that crosses it, so the file is
25
+ // bounded by maxBytes + T * (quantum + batch) rather than by elapsed time. Fixed, not a remaining
26
+ // budget: a budget assumes one thread's stat accounts for the other threads' future bytes.
27
+ const CHECK_QUANTUM_DIVISOR = 16;
28
+ const ROTATION_RETRY_COOLDOWN = 5000;
29
+ const SIZE_UNIT_MULTIPLIERS = { K: 1e3, M: 1e6, G: 1e9 };
30
+
31
+ export const INVALID_MAX_SIZE_MSG = "'maxSize' must be a positive size with a K, M or G unit (for example '64M')";
32
+
33
+ /**
34
+ * Convert `logging.rotation.maxSize` to bytes, or undefined if it cannot be a byte limit. One
35
+ * definition, shared with validation/configValidator.ts.
36
+ */
37
+ export function parseMaxSize(maxSize: any) {
38
+ if (typeof maxSize !== 'string') return undefined;
39
+ const multiplier = SIZE_UNIT_MULTIPLIERS[maxSize.slice(-1)];
40
+ if (!multiplier) return undefined;
41
+ const size = maxSize.slice(0, -1);
42
+ // Number(), not a stricter grammar, so every mantissa that yields a usable cap today keeps working.
43
+ if (size.trim() === '') return undefined;
44
+ const bytes = Number(size) * multiplier;
45
+ return Number.isSafeInteger(bytes) && bytes > 0 ? bytes : undefined;
46
+ }
47
+
48
+ export function resolveRotatedLogDir(logPath: string, configuredPath?: string) {
49
+ return configuredPath || join(dirname(logPath), 'rotated');
50
+ }
51
+
52
+ // threadId is load-bearing in the suffix below: worker threads share the process pid, and this
53
+ // counter is per-isolate, so pid+seq alone cannot keep two threads' archives apart.
54
+ let rotationSequence = 0;
55
+
56
+ // A configured rotation path may also contain live logs, so destructive scans must identify archives.
57
+ const ARCHIVE_NAME = /-[0-9a-f]{8}-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z-\d+-\d+-\d+\.log(\.gz)?$/;
58
+
59
+ export function isArchiveName(file: string) {
60
+ return ARCHIVE_NAME.test(file);
61
+ }
62
+
63
+ export function archivePathFor(logPath: string, rotatedLogDir: string) {
64
+ // Name the archive after its source log (hdb, external, a component name, ...), not a fixed
65
+ // "HDB" literal — external/component loggers inherit rotation from the main logger (#1877) and
66
+ // default to the same rotated directory. A basename alone is not enough either: two distinct
67
+ // source paths can share one (`/logs/a/hdb.log`, `/logs/b/hdb.log`), so a hash of the resolved
68
+ // source path plus the unique suffix give every archive a name rename() can never clobber.
69
+ const sourceName = basename(logPath, extname(logPath)) || 'HDB';
70
+ // sha256, not sha1: this only needs a stable identifier, but a FIPS-mode OpenSSL provider
71
+ // disables sha1 and throws synchronously, which would crash every rotation.
72
+ const sourceId = createHash('sha256').update(resolve(logPath)).digest('hex').slice(0, 8);
73
+ const uniqueSuffix = `${process.pid}-${threadId}-${rotationSequence++}`;
74
+ const timestamp = new Date().toISOString().replaceAll(':', '-');
75
+ return join(rotatedLogDir, `${sourceName}-${sourceId}-${timestamp}-${uniqueSuffix}.log`);
76
+ }
77
+
78
+ /**
79
+ * Move the active log aside and release this isolate's descriptor, with nothing awaited in between:
80
+ * a rotation that yields to the event loop lets the logging loop that triggered it keep appending,
81
+ * which is the rate-dependent overshoot this whole change exists to remove.
82
+ */
83
+ export function rotateLogFileSync(logPath: string, rotatedLogDir: string, closeLogFile: () => void, activeStats?: any) {
84
+ const active = activeStats ?? statSync(logPath);
85
+ const archivePath = archivePathFor(logPath, rotatedLogDir);
86
+ renameSync(logPath, archivePath);
87
+ closeLogFile();
88
+ // From the archive, not from the stat above: another isolate can rotate this generation away and
89
+ // its sink recreate the pathname between the two, and then this rename moves an inode the earlier
90
+ // stat never saw. Announcing that stat's identity would name a generation nobody holds, so every
91
+ // peer would answer "released" while still appending to the one about to be destroyed.
92
+ let { ino, dev } = active;
93
+ try {
94
+ ({ ino, dev } = statSync(archivePath));
95
+ } catch {
96
+ // Already claimed by retention or another pass; the pre-rename identity is the best available.
97
+ }
98
+ return { logPath, archivePath, ino, dev, generation: nextGenerationId() };
99
+ }
100
+
101
+ /**
102
+ * Publish an archived generation: ask every enumerable in-process peer to release it, then compress
103
+ * it if requested. The plain archive is only unlinked once that release is proven.
104
+ */
105
+ export async function publishArchivedGeneration(
106
+ generation: any,
107
+ compress?: boolean,
108
+ reportCompressionError?: (error: any) => void
109
+ ) {
110
+ if (!(await requestGenerationClose(generation))) {
111
+ rememberUnprovenArchive(generation.archivePath, { generation, compress });
112
+ return generation.archivePath;
113
+ }
114
+ // A worker's peer set is eventually consistent while replacements join. Its release request can
115
+ // make peers reopen promptly, but only the main thread has an authoritative worker list and may
116
+ // destroy the plain archive; its audit will obtain a fresh proof for the archived snapshot.
117
+ if (compress && isMainThread) {
118
+ try {
119
+ return await compressArchive(generation.archivePath);
120
+ } catch (error) {
121
+ reportCompressionError?.(error);
122
+ }
123
+ }
124
+ return generation.archivePath;
125
+ }
126
+
127
+ // A compression retry queue, not the safety mechanism — safety is the release the tick proves before
128
+ // anything is destroyed. Bounded because rotation happens mostly in workers, which run no tick to
129
+ // drain it, and because the archives are found on disk anyway.
130
+ const MAX_UNPROVEN_ARCHIVES = 64;
131
+ const unprovenArchives = new Map<string, any>();
132
+
133
+ function rememberUnprovenArchive(archivePath: string, pending: any) {
134
+ unprovenArchives.set(archivePath, pending);
135
+ while (unprovenArchives.size > MAX_UNPROVEN_ARCHIVES) {
136
+ unprovenArchives.delete(unprovenArchives.keys().next().value);
137
+ }
138
+ }
139
+
140
+ export function isArchivePendingQuiescence(archivePath: string) {
141
+ return unprovenArchives.has(archivePath);
142
+ }
143
+
144
+ // Bounded per pass: a peer that never answers must not let the retry queue starve the audit work.
145
+ const MAX_RETRIES_PER_PASS = 4;
146
+
147
+ export async function retryPendingGenerations(options: any = {}) {
148
+ const { deadline = Infinity, shouldStop = () => false } = options;
149
+ let attempts = 0;
150
+ for (const [archivePath, pending] of [...unprovenArchives]) {
151
+ if (shouldStop() || Date.now() >= deadline) return;
152
+ if (attempts++ >= MAX_RETRIES_PER_PASS) return;
153
+ if (!existsSync(archivePath)) {
154
+ unprovenArchives.delete(archivePath);
155
+ continue;
156
+ }
157
+ if (!(await requestGenerationClose(pending.generation, deadline))) continue;
158
+ unprovenArchives.delete(archivePath);
159
+ // One archive that will not compress must not stop the rest of the queue.
160
+ if (pending.compress) await compressArchive(archivePath).catch(() => {});
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Compress archives left plain by any isolate, from a listing taken before quiescence was proven.
166
+ * Write-path rotations happen mostly in the HTTP workers, whose unproven-archive bookkeeping the
167
+ * main thread cannot see, so without this a worker's archive would silently stay uncompressed for
168
+ * the life of the process however the operator configured `compress`.
169
+ * Only safe to call once quiescence has been proven for exactly this listing.
170
+ */
171
+ export async function compressPendingArchives(
172
+ rotatedLogDir: string,
173
+ files: string[],
174
+ liveLogPaths: Set<string>,
175
+ options: any = {}
176
+ ) {
177
+ const { deadline = Infinity, shouldStop = () => false } = options;
178
+ const present = new Set(files);
179
+ let firstError;
180
+ for (const file of files) {
181
+ if (shouldStop() || Date.now() >= deadline) break;
182
+ if (!file.endsWith('.log') || !isArchiveName(file)) continue;
183
+ const archivePath = join(rotatedLogDir, file);
184
+ if (liveLogPaths.has(resolve(archivePath)) || unprovenArchives.has(archivePath)) continue;
185
+ try {
186
+ if (present.has(`${file}.gz`)) {
187
+ // Both representations of one generation: a crash between the .gz rename and the source
188
+ // unlink leaves the plain copy behind, and every later pass skipped it as already done, so
189
+ // it survived until retention aged it out — or forever, retention being unset by default.
190
+ // The .gz is renamed into place whole, and this pass already proved every peer released it.
191
+ await fsProm.unlink(archivePath);
192
+ continue;
193
+ }
194
+ const result = await tryCompressArchive(archivePath);
195
+ if (!result.compressed) break;
196
+ } catch (error) {
197
+ if (error.code !== 'ENOENT' && !firstError) firstError = { error, file };
198
+ }
199
+ }
200
+ return firstError;
201
+ }
202
+
203
+ // One compression at a time per rotated directory. A small maxSize on a busy instance rotates
204
+ // hundreds of times a second, and a pipeline per rotation would exhaust descriptors and memory long
205
+ // before retention could run.
206
+ const compressionByDirectory = new Map<string, Promise<any>>();
207
+
208
+ export async function compressArchive(archivePath: string) {
209
+ return (await tryCompressArchive(archivePath)).path;
210
+ }
211
+
212
+ async function tryCompressArchive(archivePath: string) {
213
+ const directory = dirname(archivePath);
214
+ // Declined rather than queued: chaining would grow an unbounded list of pending jobs whenever
215
+ // rotation outruns gzip. The archive is left for the tick's bounded sweep, which is where an
216
+ // uncompressed rotation leaves it anyway. The slot is released by the promise this returns, not by
217
+ // a detached continuation, because that sweep awaits each call before making the next.
218
+ if (compressionByDirectory.has(directory)) return { path: archivePath, compressed: false };
219
+ const chain: Promise<any> = compressOneArchive(archivePath).finally(() => {
220
+ if (compressionByDirectory.get(directory) === chain) compressionByDirectory.delete(directory);
221
+ });
222
+ compressionByDirectory.set(directory, chain);
223
+ return { path: await chain, compressed: true };
224
+ }
225
+
226
+ async function compressOneArchive(archivePath: string) {
227
+ const compressedPath = `${archivePath}.gz`;
228
+ // Written to a temp file and renamed into place, so a crash mid-gzip can never leave a truncated
229
+ // `.gz` looking like the authoritative copy of a generation.
230
+ const temporaryPath = `${compressedPath}.${process.pid}-${threadId}-${rotationSequence++}.tmp`;
231
+ try {
232
+ await pipeline(createReadStream(archivePath), createGzip(), createWriteStream(temporaryPath));
233
+ await fsProm.rename(temporaryPath, compressedPath);
234
+ } catch (error) {
235
+ await fsProm.unlink(temporaryPath).catch(() => {});
236
+ throw error;
237
+ }
238
+ await fsProm.unlink(archivePath);
239
+ return compressedPath;
240
+ }
241
+
242
+ /**
243
+ * The write-path size guard. One subtraction and one branch per flush; one pathname stat once per
244
+ * quantum of this writer's own output.
245
+ */
246
+ export function createRotationGuard(options: any) {
247
+ const { logPath, maxBytes, rotatedLogDir, compress, getLogIdentity, closeLogFile, report, onRotated } = options;
248
+ const checkQuantum = Math.max(1, Math.floor(maxBytes / CHECK_QUANTUM_DIVISOR));
249
+ const logDir = dirname(logPath);
250
+ mkdirSync(rotatedLogDir, { recursive: true });
251
+ // The sink creates this one lazily, on the first append that gets ENOENT, and rotatedLogDir is not
252
+ // always under it — so without this the stat below throws ENOENT on a directory that is about to
253
+ // exist, and rotation stays off for the life of the process.
254
+ mkdirSync(logDir, { recursive: true });
255
+ // Rotation is a rename, and a rename across devices can never succeed. Refusing to build the guard
256
+ // here turns a misconfiguration that would otherwise fail closed on every write — ending file
257
+ // logging for the life of the process — into one startup error and today's unrotated behavior.
258
+ if (statSync(rotatedLogDir).dev !== statSync(logDir).dev) {
259
+ throw new Error(`the rotation directory ${rotatedLogDir} is on a different filesystem than ${logPath}`);
260
+ }
261
+ let bytesUntilCheck = checkQuantum;
262
+ let retryAfter = 0;
263
+ let rotationPending = false;
264
+ let rotating = false;
265
+ try {
266
+ // Seeded from the file as it actually is, so an instance restarted onto an already-oversized
267
+ // log rotates on its first write rather than on the first audit tick a minute later.
268
+ bytesUntilCheck = Math.min(checkQuantum, Math.max(0, maxBytes - statSync(logPath).size));
269
+ } catch {
270
+ // No log file yet.
271
+ }
272
+ return { beforeAppend, recordWrite, checkQuantum };
273
+
274
+ /**
275
+ * Try a pending rotation before the next append. A failed rotation leaves the active file
276
+ * writable: daemonized services discard stdio, so refusing the append would silently lose logs.
277
+ */
278
+ function beforeAppend() {
279
+ // `rotating` first: the rotation notice is written back through this same sink, and it must
280
+ // not re-enter a rotation that has not finished setting its own state.
281
+ if (rotating || !rotationPending) return true;
282
+ if (retryAfter > performance.now()) return true;
283
+ attemptRotation();
284
+ // If rotation is still unavailable, preserving records in the writable active log is safer
285
+ // than silently dropping them (scripted services have no stdio fallback).
286
+ return true;
287
+ }
288
+
289
+ function recordWrite(byteLength: number) {
290
+ bytesUntilCheck -= byteLength;
291
+ if (bytesUntilCheck > 0) return;
292
+ bytesUntilCheck = checkQuantum;
293
+ // `rotating` covers the rotation notice, which is written back through this same sink.
294
+ if (rotating || retryAfter > performance.now()) return;
295
+ attemptRotation();
296
+ }
297
+
298
+ function attemptRotation() {
299
+ rotating = true;
300
+ try {
301
+ checkAndRotate();
302
+ rotationPending = false;
303
+ } catch (error) {
304
+ // rename() reports ENOENT for a missing source AND for a missing destination directory, and
305
+ // only the first is benign. A missing source is the normal multi-writer outcome: another
306
+ // thread (or the audit tick) renamed the generation between this thread's stat and its
307
+ // rename, so the file is under the cap now, which is all this check wanted. A missing
308
+ // destination means no rotation can ever succeed, and treating that as a race would clear
309
+ // the cap check on every pass and let the log grow without a bound or a diagnostic. The
310
+ // destination is what is asked about: a peer can recreate the source pathname between the
311
+ // failed rename and this check, which would make the benign case look like the fatal one.
312
+ if (error.code === 'ENOENT' && existsSync(rotatedLogDir)) {
313
+ closeLogFile();
314
+ rotationPending = false;
315
+ return;
316
+ }
317
+ rotationPending = true;
318
+ retryAfter = performance.now() + ROTATION_RETRY_COOLDOWN;
319
+ closeLogFile();
320
+ report(`Harper cannot rotate its log file: ${error}`);
321
+ // A rotation target removed under a running instance is recoverable; recreate it here rather
322
+ // than on every rotation, so the common path keeps costing one stat and one rename.
323
+ if (error.code === 'ENOENT') {
324
+ try {
325
+ mkdirSync(rotatedLogDir, { recursive: true });
326
+ } catch {
327
+ // Reported above already; the retry will fail the same way and report again.
328
+ }
329
+ }
330
+ } finally {
331
+ rotating = false;
332
+ }
333
+ }
334
+
335
+ function checkAndRotate() {
336
+ let active;
337
+ try {
338
+ active = statSync(logPath);
339
+ } catch (error) {
340
+ if (error.code !== 'ENOENT') throw error;
341
+ // The generation this descriptor belongs to has already been rotated away by someone else.
342
+ closeLogFile();
343
+ return;
344
+ }
345
+ // The descriptor's identity is cached when it is opened, so the common checkpoint costs this
346
+ // one pathname stat rather than a stat plus an fstat.
347
+ if (!holdsGeneration(active)) {
348
+ closeLogFile();
349
+ return;
350
+ }
351
+ if (active.size < maxBytes) return;
352
+ const generation = rotateLogFileSync(logPath, rotatedLogDir, closeLogFile, active);
353
+ onRotated?.(generation.archivePath);
354
+ publishArchivedGeneration(generation, compress, (error) =>
355
+ report(`Harper could not compress a rotated log file: ${error}`)
356
+ ).catch((error) => report(`Harper could not publish a rotated log file: ${error}`));
357
+ }
358
+
359
+ function holdsGeneration(active: any) {
360
+ const identity = getLogIdentity();
361
+ if (!identity) return true;
362
+ // Some Windows filesystems report an unstable or zero ino, where identity cannot distinguish
363
+ // generations; there this defers to the size check rather than closing a descriptor at random.
364
+ if (identity.ino === 0 || active.ino === 0) return true;
365
+ return identity.ino === active.ino && identity.dev === active.dev;
366
+ }
367
+ }