@harperfast/harper 5.2.0-beta.4 → 5.2.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 (320) hide show
  1. package/bin/backup.ts +169 -0
  2. package/bin/cliOperations.ts +92 -73
  3. package/bin/copyDb.ts +21 -4
  4. package/bin/harper.ts +43 -55
  5. package/bin/help.ts +216 -0
  6. package/components/Application.ts +236 -46
  7. package/components/ApplicationScope.ts +26 -0
  8. package/components/EntryHandler.ts +410 -105
  9. package/components/RuntimeModuleTracker.ts +189 -0
  10. package/components/Scope.ts +120 -30
  11. package/components/componentLoader.ts +177 -25
  12. package/components/deployLifecycle.ts +119 -33
  13. package/components/mcp/toolRegistry.ts +10 -0
  14. package/components/mcp/tools/application.ts +12 -5
  15. package/components/mcp/tools/operations.ts +9 -0
  16. package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
  17. package/components/mcp/tools/schemas/operations.ts +9 -0
  18. package/components/operations.js +5 -6
  19. package/components/operationsValidation.js +32 -2
  20. package/components/scopeMount.ts +150 -0
  21. package/config/configUtils.ts +12 -9
  22. package/config-root.schema.json +14 -0
  23. package/dataLayer/backupManifest.ts +102 -0
  24. package/dataLayer/blobBackup.ts +286 -0
  25. package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
  26. package/dataLayer/hdbInfoController.ts +8 -0
  27. package/dataLayer/restoreMarker.ts +276 -0
  28. package/dataLayer/rocksdbBackup.ts +1100 -0
  29. package/dataLayer/schemaDescribe.ts +2 -1
  30. package/dist/bin/backup.d.ts +9 -0
  31. package/dist/bin/backup.js +192 -0
  32. package/dist/bin/backup.js.map +1 -0
  33. package/dist/bin/cliOperations.d.ts +13 -0
  34. package/dist/bin/cliOperations.js +89 -70
  35. package/dist/bin/cliOperations.js.map +1 -1
  36. package/dist/bin/copyDb.js +13 -2
  37. package/dist/bin/copyDb.js.map +1 -1
  38. package/dist/bin/harper.d.ts +13 -0
  39. package/dist/bin/harper.js +45 -56
  40. package/dist/bin/harper.js.map +1 -1
  41. package/dist/bin/help.d.ts +8 -0
  42. package/dist/bin/help.js +192 -0
  43. package/dist/bin/help.js.map +1 -0
  44. package/dist/components/Application.d.ts +16 -1
  45. package/dist/components/Application.js +210 -38
  46. package/dist/components/Application.js.map +1 -1
  47. package/dist/components/ApplicationScope.d.ts +7 -0
  48. package/dist/components/ApplicationScope.js +22 -0
  49. package/dist/components/ApplicationScope.js.map +1 -1
  50. package/dist/components/EntryHandler.d.ts +4 -4
  51. package/dist/components/EntryHandler.js +386 -95
  52. package/dist/components/EntryHandler.js.map +1 -1
  53. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  54. package/dist/components/RuntimeModuleTracker.js +189 -0
  55. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  56. package/dist/components/Scope.d.ts +38 -1
  57. package/dist/components/Scope.js +117 -28
  58. package/dist/components/Scope.js.map +1 -1
  59. package/dist/components/componentLoader.d.ts +2 -6
  60. package/dist/components/componentLoader.js +160 -18
  61. package/dist/components/componentLoader.js.map +1 -1
  62. package/dist/components/deployLifecycle.d.ts +6 -2
  63. package/dist/components/deployLifecycle.js +109 -31
  64. package/dist/components/deployLifecycle.js.map +1 -1
  65. package/dist/components/mcp/toolRegistry.d.ts +7 -0
  66. package/dist/components/mcp/toolRegistry.js +10 -0
  67. package/dist/components/mcp/toolRegistry.js.map +1 -1
  68. package/dist/components/mcp/tools/application.js +11 -5
  69. package/dist/components/mcp/tools/application.js.map +1 -1
  70. package/dist/components/mcp/tools/operations.js +9 -0
  71. package/dist/components/mcp/tools/operations.js.map +1 -1
  72. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
  73. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  74. package/dist/components/mcp/tools/schemas/operations.js +9 -0
  75. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  76. package/dist/components/operations.js +6 -6
  77. package/dist/components/operations.js.map +1 -1
  78. package/dist/components/operationsValidation.js +34 -2
  79. package/dist/components/operationsValidation.js.map +1 -1
  80. package/dist/components/scopeMount.d.ts +86 -0
  81. package/dist/components/scopeMount.js +131 -0
  82. package/dist/components/scopeMount.js.map +1 -0
  83. package/dist/config/configUtils.js +13 -9
  84. package/dist/config/configUtils.js.map +1 -1
  85. package/dist/dataLayer/backupManifest.d.ts +26 -0
  86. package/dist/dataLayer/backupManifest.js +97 -0
  87. package/dist/dataLayer/backupManifest.js.map +1 -0
  88. package/dist/dataLayer/blobBackup.d.ts +87 -0
  89. package/dist/dataLayer/blobBackup.js +282 -0
  90. package/dist/dataLayer/blobBackup.js.map +1 -0
  91. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
  92. package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
  93. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  94. package/dist/dataLayer/hdbInfoController.js +4 -0
  95. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  96. package/dist/dataLayer/restoreMarker.d.ts +122 -0
  97. package/dist/dataLayer/restoreMarker.js +261 -0
  98. package/dist/dataLayer/restoreMarker.js.map +1 -0
  99. package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
  100. package/dist/dataLayer/rocksdbBackup.js +1039 -0
  101. package/dist/dataLayer/rocksdbBackup.js.map +1 -0
  102. package/dist/dataLayer/schemaDescribe.js +2 -1
  103. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  104. package/dist/resources/DatabaseTransaction.d.ts +55 -0
  105. package/dist/resources/DatabaseTransaction.js +282 -109
  106. package/dist/resources/DatabaseTransaction.js.map +1 -1
  107. package/dist/resources/ResourceInterface.d.ts +8 -5
  108. package/dist/resources/ResourceInterface.js.map +1 -1
  109. package/dist/resources/Resources.js +22 -4
  110. package/dist/resources/Resources.js.map +1 -1
  111. package/dist/resources/Table.d.ts +6 -5
  112. package/dist/resources/Table.js +114 -37
  113. package/dist/resources/Table.js.map +1 -1
  114. package/dist/resources/analytics/write.js +6 -6
  115. package/dist/resources/analytics/write.js.map +1 -1
  116. package/dist/resources/blob.d.ts +8 -1
  117. package/dist/resources/blob.js +31 -14
  118. package/dist/resources/blob.js.map +1 -1
  119. package/dist/resources/databases.d.ts +59 -1
  120. package/dist/resources/databases.js +386 -41
  121. package/dist/resources/databases.js.map +1 -1
  122. package/dist/resources/jsResource.d.ts +4 -26
  123. package/dist/resources/jsResource.js +5 -59
  124. package/dist/resources/jsResource.js.map +1 -1
  125. package/dist/resources/models/Models.d.ts +11 -1
  126. package/dist/resources/models/Models.js +10 -1
  127. package/dist/resources/models/Models.js.map +1 -1
  128. package/dist/resources/models/backendRegistry.d.ts +9 -0
  129. package/dist/resources/models/backendRegistry.js +10 -0
  130. package/dist/resources/models/backendRegistry.js.map +1 -1
  131. package/dist/resources/models/openaiStream.d.ts +16 -1
  132. package/dist/resources/models/openaiStream.js +113 -21
  133. package/dist/resources/models/openaiStream.js.map +1 -1
  134. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  135. package/dist/resources/models/v1/chatCompletions.js +115 -0
  136. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  137. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  138. package/dist/resources/models/v1/embeddings.js +71 -0
  139. package/dist/resources/models/v1/embeddings.js.map +1 -0
  140. package/dist/resources/models/v1/errors.d.ts +54 -0
  141. package/dist/resources/models/v1/errors.js +130 -0
  142. package/dist/resources/models/v1/errors.js.map +1 -0
  143. package/dist/resources/models/v1/index.d.ts +36 -0
  144. package/dist/resources/models/v1/index.js +75 -0
  145. package/dist/resources/models/v1/index.js.map +1 -0
  146. package/dist/resources/models/v1/models.d.ts +26 -0
  147. package/dist/resources/models/v1/models.js +44 -0
  148. package/dist/resources/models/v1/models.js.map +1 -0
  149. package/dist/resources/models/v1/translation.d.ts +133 -0
  150. package/dist/resources/models/v1/translation.js +298 -0
  151. package/dist/resources/models/v1/translation.js.map +1 -0
  152. package/dist/resources/roles.d.ts +1 -1
  153. package/dist/resources/roles.js +54 -7
  154. package/dist/resources/roles.js.map +1 -1
  155. package/dist/resources/transaction.js +0 -3
  156. package/dist/resources/transaction.js.map +1 -1
  157. package/dist/security/jsLoader.js +84 -33
  158. package/dist/security/jsLoader.js.map +1 -1
  159. package/dist/security/role.js +4 -0
  160. package/dist/security/role.js.map +1 -1
  161. package/dist/security/superUserGuard.d.ts +7 -0
  162. package/dist/security/superUserGuard.js +23 -0
  163. package/dist/security/superUserGuard.js.map +1 -0
  164. package/dist/security/tokenAuthentication.d.ts +0 -1
  165. package/dist/security/tokenAuthentication.js +6 -2
  166. package/dist/security/tokenAuthentication.js.map +1 -1
  167. package/dist/security/user.d.ts +6 -1
  168. package/dist/security/user.js +23 -1
  169. package/dist/security/user.js.map +1 -1
  170. package/dist/server/REST.js +25 -9
  171. package/dist/server/REST.js.map +1 -1
  172. package/dist/server/fastifyRoutes.js +15 -1
  173. package/dist/server/fastifyRoutes.js.map +1 -1
  174. package/dist/server/http.d.ts +23 -2
  175. package/dist/server/http.js +99 -12
  176. package/dist/server/http.js.map +1 -1
  177. package/dist/server/itc/serverHandlers.js +7 -1
  178. package/dist/server/itc/serverHandlers.js.map +1 -1
  179. package/dist/server/jobs/jobProcess.js +20 -1
  180. package/dist/server/jobs/jobProcess.js.map +1 -1
  181. package/dist/server/jobs/jobRunner.js +10 -0
  182. package/dist/server/jobs/jobRunner.js.map +1 -1
  183. package/dist/server/jobs/jobs.js +11 -0
  184. package/dist/server/jobs/jobs.js.map +1 -1
  185. package/dist/server/middlewareChain.d.ts +10 -1
  186. package/dist/server/middlewareChain.js +81 -21
  187. package/dist/server/middlewareChain.js.map +1 -1
  188. package/dist/server/operationsServer.d.ts +0 -2
  189. package/dist/server/operationsServer.js.map +1 -1
  190. package/dist/server/serverHelpers/multipartParser.js +9 -0
  191. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  192. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  193. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  194. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  195. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  196. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  197. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  198. package/dist/server/serverHelpers/serverHandlers.js +27 -7
  199. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  200. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  201. package/dist/server/serverHelpers/serverUtilities.js +21 -7
  202. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  203. package/dist/server/static.js +68 -21
  204. package/dist/server/static.js.map +1 -1
  205. package/dist/server/storageReclamation.d.ts +17 -0
  206. package/dist/server/storageReclamation.js +90 -6
  207. package/dist/server/storageReclamation.js.map +1 -1
  208. package/dist/server/threads/socketRouter.js +20 -0
  209. package/dist/server/threads/socketRouter.js.map +1 -1
  210. package/dist/server/threads/threadServer.js +12 -0
  211. package/dist/server/threads/threadServer.js.map +1 -1
  212. package/dist/sqlEngine/diff/differential.js +7 -3
  213. package/dist/sqlEngine/diff/differential.js.map +1 -1
  214. package/dist/sqlTranslator/index.js +6 -1
  215. package/dist/sqlTranslator/index.js.map +1 -1
  216. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  217. package/dist/upgrade/upgradePrompt.js +22 -3
  218. package/dist/upgrade/upgradePrompt.js.map +1 -1
  219. package/dist/utility/OperationFunctionCaller.js +25 -6
  220. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  221. package/dist/utility/errors/commonErrors.d.ts +1 -0
  222. package/dist/utility/errors/commonErrors.js +1 -0
  223. package/dist/utility/errors/commonErrors.js.map +1 -1
  224. package/dist/utility/hdbTerms.d.ts +13 -1
  225. package/dist/utility/hdbTerms.js +13 -1
  226. package/dist/utility/hdbTerms.js.map +1 -1
  227. package/dist/utility/install/installer.js +32 -0
  228. package/dist/utility/install/installer.js.map +1 -1
  229. package/dist/utility/logging/harper_logger.d.ts +47 -0
  230. package/dist/utility/logging/harper_logger.js +803 -38
  231. package/dist/utility/logging/harper_logger.js.map +1 -1
  232. package/dist/utility/operation_authorization.js +13 -3
  233. package/dist/utility/operation_authorization.js.map +1 -1
  234. package/npm-shrinkwrap.json +198 -193
  235. package/package.json +12 -7
  236. package/resources/DESIGN.md +2 -0
  237. package/resources/DatabaseTransaction.ts +310 -104
  238. package/resources/ResourceInterface.ts +8 -5
  239. package/resources/Resources.ts +22 -4
  240. package/resources/Table.ts +341 -263
  241. package/resources/analytics/write.ts +22 -20
  242. package/resources/blob.ts +31 -14
  243. package/resources/databases.ts +387 -46
  244. package/resources/jsResource.ts +5 -62
  245. package/resources/models/Models.ts +14 -1
  246. package/resources/models/backendRegistry.ts +10 -0
  247. package/resources/models/openaiStream.ts +131 -19
  248. package/resources/models/v1/chatCompletions.ts +128 -0
  249. package/resources/models/v1/embeddings.ts +70 -0
  250. package/resources/models/v1/errors.ts +141 -0
  251. package/resources/models/v1/index.ts +72 -0
  252. package/resources/models/v1/models.ts +53 -0
  253. package/resources/models/v1/translation.ts +362 -0
  254. package/resources/roles.ts +67 -7
  255. package/resources/transaction.ts +0 -3
  256. package/security/jsLoader.ts +84 -30
  257. package/security/role.ts +7 -0
  258. package/security/superUserGuard.ts +20 -0
  259. package/security/tokenAuthentication.ts +6 -3
  260. package/security/user.ts +26 -1
  261. package/server/DESIGN.md +61 -34
  262. package/server/REST.ts +25 -9
  263. package/server/fastifyRoutes.ts +20 -1
  264. package/server/http.ts +100 -13
  265. package/server/itc/serverHandlers.js +7 -1
  266. package/server/jobs/jobProcess.ts +18 -1
  267. package/server/jobs/jobRunner.ts +10 -0
  268. package/server/jobs/jobs.ts +11 -0
  269. package/server/middlewareChain.ts +79 -20
  270. package/server/operationsServer.ts +0 -2
  271. package/server/serverHelpers/multipartParser.ts +9 -0
  272. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  273. package/server/serverHelpers/registeredOperations.ts +19 -15
  274. package/server/serverHelpers/serverHandlers.js +28 -7
  275. package/server/serverHelpers/serverUtilities.ts +29 -7
  276. package/server/static.ts +87 -23
  277. package/server/storageReclamation.ts +104 -8
  278. package/server/threads/socketRouter.ts +20 -0
  279. package/server/threads/threadServer.js +11 -0
  280. package/sqlTranslator/index.ts +6 -1
  281. package/static/defaultConfig.yaml +3 -0
  282. package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
  283. package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
  284. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
  285. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
  286. package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
  287. package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
  288. package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
  289. package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
  290. package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
  291. package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
  292. package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
  293. package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
  294. package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
  295. package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
  296. package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
  297. package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
  298. package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
  299. package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
  300. package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
  301. package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
  302. package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
  303. package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
  304. package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
  305. package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
  306. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
  307. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
  308. package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
  309. package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
  310. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
  311. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
  312. package/studio/web/index.html +1 -1
  313. package/upgrade/upgradePrompt.ts +22 -3
  314. package/utility/OperationFunctionCaller.ts +24 -3
  315. package/utility/errors/commonErrors.ts +2 -0
  316. package/utility/hdbTerms.ts +13 -1
  317. package/utility/install/installer.ts +37 -0
  318. package/utility/logging/harper_logger.ts +786 -41
  319. package/utility/operation_authorization.ts +31 -3
  320. package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.EntryHandler = void 0;
7
7
  const harper_logger_ts_1 = require("../utility/logging/harper_logger.js");
8
+ const node_crypto_1 = require("node:crypto");
8
9
  const node_events_1 = require("node:events");
9
10
  const Component_ts_1 = require("./Component.js");
10
11
  const chokidar_1 = __importDefault(require("chokidar"));
@@ -18,29 +19,63 @@ class EntryHandler extends node_events_1.EventEmitter {
18
19
  #watcher;
19
20
  #logger;
20
21
  #pendingFileReads;
22
+ #pendingFileReadsByGeneration;
21
23
  #isInitialScanComplete;
22
- // When true, #watch() short-circuits without creating a chokidar watcher.
23
- // pause() sets it, resume() clears it. Lets a deploy quiesce the watcher
24
- // without losing the EntryHandler instance (and therefore listener
25
- // attachments registered by plugins via scope.handleEntry(handler)).
24
+ #readyPending = false;
25
+ #readyGeneration = 0;
26
+ #watchGeneration = 0;
27
+ #eventSequence = 0;
28
+ #latestPathSequence = new Map();
29
+ #entries = new Map();
30
+ #redeployScan;
26
31
  #paused = false;
27
- // Tracks the in-flight close() promise from pause() so resume() can await
28
- // the old watcher's inotify handles releasing before installing a fresh
29
- // chokidar instance — otherwise a rapid pause→resume can overlap teardown
30
- // and setup, which under inotify pressure can produce spurious EMFILE.
31
32
  #pausedClose;
33
+ // Serializes replacements so concurrent update/recovery calls cannot orphan an intervening watcher.
34
+ #watchInstall;
32
35
  #usingPolling = false;
33
36
  #closed = false;
34
37
  #openCount = 0;
38
+ #liveWatchers = new Set();
35
39
  ready;
36
40
  constructor(name, directory, config, logger) {
37
41
  super();
38
42
  this.#component = new Component_ts_1.Component(name, directory, castConfig(config));
39
43
  this.#logger = logger || (0, harper_logger_ts_1.loggerWithTag)(name);
40
44
  this.#pendingFileReads = new Set();
45
+ this.#pendingFileReadsByGeneration = new Map();
41
46
  this.#isInitialScanComplete = false;
42
- this.ready = (0, node_events_1.once)(this, 'ready');
43
- this.#watch();
47
+ this.ready = this.#createReadyLatch();
48
+ this.#watch().catch(() => { });
49
+ }
50
+ #createReadyLatch() {
51
+ if (this.#readyPending)
52
+ return this.ready;
53
+ this.#readyPending = true;
54
+ const ready = new Promise((resolve, reject) => {
55
+ const cleanup = () => {
56
+ this.#readyPending = false;
57
+ this.off('ready', handleReady);
58
+ this.off('error', handleError);
59
+ this.off('close', handleClose);
60
+ };
61
+ const handleReady = () => {
62
+ cleanup();
63
+ resolve([]);
64
+ };
65
+ const handleError = (error) => {
66
+ cleanup();
67
+ reject(error);
68
+ };
69
+ const handleClose = () => {
70
+ cleanup();
71
+ reject(new Error(`Entry handler for ${this.name} closed before becoming ready`));
72
+ };
73
+ this.once('ready', handleReady);
74
+ this.once('error', handleError);
75
+ this.once('close', handleClose);
76
+ });
77
+ void ready.catch(() => { });
78
+ return ready;
44
79
  }
45
80
  get name() {
46
81
  return this.#component.name;
@@ -48,7 +83,9 @@ class EntryHandler extends node_events_1.EventEmitter {
48
83
  get directory() {
49
84
  return this.#component.directory;
50
85
  }
51
- #handleAll(...[event, path, stats]) {
86
+ #handleAll(generation, ...[event, path, stats]) {
87
+ if (generation !== this.#watchGeneration)
88
+ return;
52
89
  if (path === '')
53
90
  path = '/';
54
91
  if (!(0, micromatch_1.isMatch)(path, this.#component.globOptions.source, { ignore: this.#component.globOptions.ignore }))
@@ -57,25 +94,7 @@ class EntryHandler extends node_events_1.EventEmitter {
57
94
  switch (event) {
58
95
  case 'add':
59
96
  case 'change': {
60
- const urlPath = (0, deriveURLPath_ts_1.deriveURLPath)(this.#component, path, 'file');
61
- const fileReadPromise = (0, promises_1.readFile)(absolutePath)
62
- .then((contents) => {
63
- const entry = {
64
- eventType: event,
65
- entryType: 'file',
66
- contents,
67
- stats,
68
- absolutePath,
69
- urlPath,
70
- };
71
- this.emit('all', entry);
72
- this.emit(event, entry);
73
- })
74
- .finally(() => {
75
- this.#pendingFileReads.delete(fileReadPromise);
76
- this.#checkIfAllComplete();
77
- });
78
- this.#pendingFileReads.add(fileReadPromise);
97
+ this.#queueFileRead(generation, event, path, absolutePath, stats, (0, promises_1.readFile)(absolutePath));
79
98
  break;
80
99
  }
81
100
  case 'unlink': {
@@ -87,27 +106,200 @@ class EntryHandler extends node_events_1.EventEmitter {
87
106
  absolutePath,
88
107
  urlPath,
89
108
  };
90
- this.emit('all', entry);
91
- this.emit(event, entry);
109
+ this.#handleRemovedEntry(generation, entry);
92
110
  break;
93
111
  }
94
112
  case 'addDir':
95
113
  case 'unlinkDir': {
96
114
  const urlPath = (0, deriveURLPath_ts_1.deriveURLPath)(this.#component, path, 'directory');
97
- const entry = {
98
- eventType: event,
99
- entryType: 'directory',
100
- stats,
101
- absolutePath,
102
- urlPath,
103
- };
104
- this.emit('all', entry);
105
- this.emit(event, entry);
115
+ if (event === 'addDir') {
116
+ const entry = {
117
+ eventType: event,
118
+ entryType: 'directory',
119
+ stats,
120
+ absolutePath,
121
+ urlPath,
122
+ };
123
+ this.#handleAddedEntry(generation, entry, this.#snapshot(entry));
124
+ }
125
+ else {
126
+ const entry = {
127
+ eventType: event,
128
+ entryType: 'directory',
129
+ stats,
130
+ absolutePath,
131
+ urlPath,
132
+ };
133
+ this.#handleRemovedEntry(generation, entry);
134
+ }
106
135
  break;
107
136
  }
108
137
  }
109
138
  }
110
- #handleError(error) {
139
+ #queueFileRead(generation, event, path, absolutePath, stats, contentsPromise) {
140
+ const sequence = ++this.#eventSequence;
141
+ this.#latestPathSequence.set(absolutePath, sequence);
142
+ const fileReadPromise = contentsPromise
143
+ .then((contents) => {
144
+ if (generation !== this.#watchGeneration || this.#latestPathSequence.get(absolutePath) !== sequence)
145
+ return;
146
+ if (this.#redeployScan?.generation === generation)
147
+ this.#redeployScan.readFailures.delete(absolutePath);
148
+ const entry = {
149
+ eventType: event,
150
+ entryType: 'file',
151
+ contents,
152
+ stats,
153
+ absolutePath,
154
+ urlPath: (0, deriveURLPath_ts_1.deriveURLPath)(this.#component, path, 'file'),
155
+ };
156
+ try {
157
+ this.#handleAddedEntry(generation, entry, this.#snapshot(entry));
158
+ }
159
+ catch (error) {
160
+ this.#reportConsumerError(error);
161
+ }
162
+ }, (error) => {
163
+ if (generation !== this.#watchGeneration ||
164
+ this.#latestPathSequence.get(absolutePath) !== sequence ||
165
+ error?.code === 'ENOENT')
166
+ return;
167
+ if (this.#redeployScan?.generation === generation) {
168
+ this.#redeployScan.readFailures.set(absolutePath, error);
169
+ }
170
+ else {
171
+ this.#handleWatcherError(error);
172
+ }
173
+ })
174
+ .finally(() => {
175
+ if (this.#latestPathSequence.get(absolutePath) === sequence)
176
+ this.#latestPathSequence.delete(absolutePath);
177
+ this.#pendingFileReads.delete(fileReadPromise);
178
+ const generationReads = this.#pendingFileReadsByGeneration.get(generation);
179
+ generationReads?.delete(fileReadPromise);
180
+ if (generationReads?.size === 0)
181
+ this.#pendingFileReadsByGeneration.delete(generation);
182
+ this.#checkIfAllComplete(generation);
183
+ });
184
+ this.#pendingFileReads.add(fileReadPromise);
185
+ let generationReads = this.#pendingFileReadsByGeneration.get(generation);
186
+ if (!generationReads)
187
+ this.#pendingFileReadsByGeneration.set(generation, (generationReads = new Set()));
188
+ generationReads.add(fileReadPromise);
189
+ return fileReadPromise;
190
+ }
191
+ #snapshot(entry) {
192
+ if (entry.entryType === 'file')
193
+ return {
194
+ entryType: entry.entryType,
195
+ urlPath: entry.urlPath,
196
+ digest: (0, node_crypto_1.createHash)('sha256').update(entry.contents).digest(),
197
+ };
198
+ return { entryType: entry.entryType, urlPath: entry.urlPath };
199
+ }
200
+ #handleAddedEntry(generation, entry, snapshot) {
201
+ if (generation !== this.#watchGeneration)
202
+ return;
203
+ const scan = this.#redeployScan?.generation === generation ? this.#redeployScan : undefined;
204
+ if (!scan) {
205
+ const wasKnown = this.#entries.has(entry.absolutePath);
206
+ this.#entries.set(entry.absolutePath, snapshot);
207
+ // A newer change read can supersede an initial add read for the same path. The consumer still
208
+ // needs its first event to be an add so it can load the entry before handling later changes.
209
+ if (entry.entryType === 'file' && entry.eventType === 'change' && !wasKnown)
210
+ this.#emitEntrySafely({ ...entry, eventType: 'add' });
211
+ else
212
+ this.#emitEntrySafely(entry);
213
+ return;
214
+ }
215
+ const wasRemovedDuringScan = scan.removalsEmitted.delete(entry.absolutePath);
216
+ const currentEntry = scan.currentEntries.get(entry.absolutePath);
217
+ const previousEntry = wasRemovedDuringScan
218
+ ? undefined
219
+ : (currentEntry ?? scan.previousEntries.get(entry.absolutePath));
220
+ scan.currentEntries.set(entry.absolutePath, snapshot);
221
+ if (!previousEntry) {
222
+ this.#emitAddition(entry);
223
+ return;
224
+ }
225
+ if (previousEntry.entryType !== snapshot.entryType || previousEntry.urlPath !== snapshot.urlPath) {
226
+ try {
227
+ this.#emitRemoval(previousEntry, entry.absolutePath);
228
+ }
229
+ catch (error) {
230
+ this.#reportConsumerError(error);
231
+ }
232
+ this.#emitAddition(entry);
233
+ return;
234
+ }
235
+ if (snapshot.entryType === 'file' &&
236
+ previousEntry.entryType === 'file' &&
237
+ !previousEntry.digest.equals(snapshot.digest)) {
238
+ this.#emitEntrySafely({ ...entry, eventType: 'change' });
239
+ }
240
+ }
241
+ #emitAddition(entry) {
242
+ if (entry.entryType === 'file')
243
+ this.#emitEntrySafely({ ...entry, eventType: 'add' });
244
+ else
245
+ this.#emitEntrySafely({ ...entry, eventType: 'addDir' });
246
+ }
247
+ #handleRemovedEntry(generation, entry) {
248
+ if (generation !== this.#watchGeneration)
249
+ return;
250
+ this.#latestPathSequence.delete(entry.absolutePath);
251
+ const scan = this.#redeployScan?.generation === generation ? this.#redeployScan : undefined;
252
+ if (!scan) {
253
+ this.#entries.delete(entry.absolutePath);
254
+ this.#emitEntrySafely(entry);
255
+ return;
256
+ }
257
+ const knownEntry = scan.currentEntries.get(entry.absolutePath) ?? scan.previousEntries.get(entry.absolutePath);
258
+ scan.currentEntries.delete(entry.absolutePath);
259
+ if (!knownEntry || scan.removalsEmitted.has(entry.absolutePath))
260
+ return;
261
+ try {
262
+ this.#emitRemoval(knownEntry, entry.absolutePath);
263
+ }
264
+ catch (error) {
265
+ this.#reportConsumerError(error);
266
+ }
267
+ scan.removalsEmitted.add(entry.absolutePath);
268
+ }
269
+ #emitRemoval(snapshot, absolutePath) {
270
+ if (snapshot.entryType === 'file')
271
+ this.#emitEntry({ eventType: 'unlink', entryType: 'file', absolutePath, urlPath: snapshot.urlPath });
272
+ else
273
+ this.#emitEntry({ eventType: 'unlinkDir', entryType: 'directory', absolutePath, urlPath: snapshot.urlPath });
274
+ }
275
+ #emitEntry(entry) {
276
+ this.emit('all', entry);
277
+ switch (entry.eventType) {
278
+ case 'add':
279
+ this.emit('add', entry);
280
+ break;
281
+ case 'change':
282
+ this.emit('change', entry);
283
+ break;
284
+ case 'unlink':
285
+ this.emit('unlink', entry);
286
+ break;
287
+ case 'addDir':
288
+ this.emit('addDir', entry);
289
+ break;
290
+ case 'unlinkDir':
291
+ this.emit('unlinkDir', entry);
292
+ }
293
+ }
294
+ #emitEntrySafely(entry) {
295
+ try {
296
+ this.#emitEntry(entry);
297
+ }
298
+ catch (error) {
299
+ this.#reportConsumerError(error);
300
+ }
301
+ }
302
+ #handleWatcherError(error) {
111
303
  if ((0, watcherFallback_ts_1.isWatcherExhaustionError)(error)) {
112
304
  // Swallow every exhaustion error — chokidar can emit several before the
113
305
  // failed native watcher closes, and we don't want a flurry of ENOSPC to
@@ -127,46 +319,133 @@ class EntryHandler extends node_events_1.EventEmitter {
127
319
  }
128
320
  return;
129
321
  }
130
- this.emit('error', error);
322
+ this.#reportConsumerError(error);
323
+ }
324
+ #reportConsumerError(error) {
325
+ if (this.listenerCount('error') > 0)
326
+ this.emit('error', error);
327
+ else
328
+ this.#logger.error?.('Error in entry listener:', error);
131
329
  }
132
- #handleReady() {
330
+ #handleReady(generation) {
331
+ if (generation !== this.#watchGeneration)
332
+ return;
133
333
  this.#isInitialScanComplete = true;
134
- if (this.#pendingFileReads.size > 0) {
135
- this.#logger.debug?.(`Initial scan complete, still waiting for ${this.#pendingFileReads.size} pending file reads`);
334
+ const pendingReads = this.#pendingFileReadsByGeneration.get(generation)?.size ?? 0;
335
+ if (pendingReads > 0) {
336
+ this.#logger.debug?.(`Initial scan complete, still waiting for ${pendingReads} pending file reads`);
136
337
  }
137
- this.#checkIfAllComplete();
338
+ this.#checkIfAllComplete(generation);
138
339
  }
139
- #checkIfAllComplete() {
140
- // Only emit 'ready' once the initial scan is complete AND all file reads are done
141
- if (this.#isInitialScanComplete && this.#pendingFileReads.size === 0) {
340
+ #checkIfAllComplete(generation) {
341
+ if (generation === this.#watchGeneration &&
342
+ this.#readyGeneration !== generation &&
343
+ this.#isInitialScanComplete &&
344
+ !this.#pendingFileReadsByGeneration.has(generation)) {
345
+ this.#readyGeneration = generation;
346
+ const { listenerErrors, readErrors } = this.#finishRedeployScan(generation);
142
347
  this.emit('ready');
348
+ // The readiness latch rejects if `error` is emitted first. Surface consumer failures only
349
+ // after resolving the generation's ready latch so one bad removal listener cannot strand startup.
350
+ for (const error of listenerErrors)
351
+ this.#reportConsumerError(error);
352
+ for (const error of readErrors)
353
+ this.#handleWatcherError(error);
143
354
  }
144
355
  }
356
+ #finishRedeployScan(generation) {
357
+ const scan = this.#redeployScan;
358
+ if (!scan || scan.generation !== generation)
359
+ return { listenerErrors: [], readErrors: [] };
360
+ const listenerErrors = [];
361
+ for (const absolutePath of scan.readFailures.keys()) {
362
+ const previousEntry = scan.previousEntries.get(absolutePath);
363
+ if (previousEntry && !scan.currentEntries.has(absolutePath))
364
+ scan.currentEntries.set(absolutePath, previousEntry);
365
+ }
366
+ const missingEntries = [...scan.previousEntries].filter(([absolutePath]) => !scan.currentEntries.has(absolutePath) && !scan.removalsEmitted.has(absolutePath));
367
+ // Match watcher deletion semantics: descendants disappear before the directory that contained them.
368
+ missingEntries.sort(([a], [b]) => b.length - a.length);
369
+ // Commit the generation before invoking consumer code. A synchronously throwing removal listener must
370
+ // not leave the old snapshot installed or keep the readiness latch permanently unresolved.
371
+ this.#entries = scan.currentEntries;
372
+ this.#redeployScan = undefined;
373
+ for (const [absolutePath, snapshot] of missingEntries) {
374
+ try {
375
+ this.#emitRemoval(snapshot, absolutePath);
376
+ }
377
+ catch (error) {
378
+ listenerErrors.push(error);
379
+ }
380
+ }
381
+ return { listenerErrors, readErrors: [...scan.readFailures.values()] };
382
+ }
383
+ #observedEntries() {
384
+ const scan = this.#redeployScan;
385
+ if (!scan)
386
+ return this.#entries;
387
+ // A generation can be replaced before `ready`, after some events have already reached consumers.
388
+ // Carry exactly that observed state forward: retain previously known paths that this partial scan
389
+ // has not reached, apply additions/changes it emitted, and remove paths whose unlink was emitted.
390
+ const observedEntries = new Map(scan.previousEntries);
391
+ for (const absolutePath of scan.removalsEmitted)
392
+ observedEntries.delete(absolutePath);
393
+ for (const [absolutePath, snapshot] of scan.currentEntries)
394
+ observedEntries.set(absolutePath, snapshot);
395
+ return observedEntries;
396
+ }
145
397
  async #watch() {
146
- // If pause() retained an in-flight close, wait for it to release inotify
147
- // handles before we install a new watcher. Otherwise a fast pause→resume
148
- // can overlap teardown and setup under inotify pressure.
398
+ const install = this.#watchInstall ? this.#watchInstall.then(() => this.#installWatcher()) : this.#installWatcher();
399
+ const settled = install.then(() => {
400
+ if (this.#watchInstall === settled)
401
+ this.#watchInstall = undefined;
402
+ }, () => {
403
+ if (this.#watchInstall === settled)
404
+ this.#watchInstall = undefined;
405
+ });
406
+ this.#watchInstall = settled;
407
+ await install;
408
+ return this.ready;
409
+ }
410
+ async #installWatcher() {
149
411
  if (this.#pausedClose) {
150
412
  await this.#pausedClose;
151
413
  this.#pausedClose = undefined;
152
414
  }
153
- await this.#watcher?.close();
154
- this.#watcher = undefined;
155
- // If close() landed while a previous close()/recreate was awaiting, don't
156
- // install a fresh watcher it would outlive the EntryHandler.
415
+ const outgoing = this.#watcher;
416
+ if (outgoing) {
417
+ // Stays installed across the await so a close() landing in the gap still includes this
418
+ // teardown in its own settle set; only clear it if close() didn't already replace it.
419
+ try {
420
+ await outgoing.close();
421
+ }
422
+ catch (error) {
423
+ this.#logger.warn?.(`Failed to close the previous watcher for ${this.name}; replacing it anyway:`, error);
424
+ }
425
+ finally {
426
+ this.#liveWatchers.delete(outgoing);
427
+ }
428
+ if (this.#watcher === outgoing)
429
+ this.#watcher = undefined;
430
+ }
157
431
  if (this.#closed)
158
- return this.ready;
159
- // pause() may have landed in the gap before our async close resolved.
160
- // If so, do not install a replacement watcher — resume() will.
432
+ return;
161
433
  if (this.#paused)
162
- return this.ready;
163
- // When a fresh watcher is installed (after pause+resume, or update), the
164
- // initial scan emits add events anew, so reset the readiness latch so
165
- // `ready` resolves after the new scan completes.
434
+ return;
435
+ const generation = ++this.#watchGeneration;
166
436
  this.#isInitialScanComplete = false;
437
+ const previousEntries = this.#observedEntries();
438
+ this.#entries = previousEntries;
439
+ this.#redeployScan = {
440
+ generation,
441
+ previousEntries,
442
+ currentEntries: new Map(),
443
+ removalsEmitted: new Set(),
444
+ readFailures: new Map(),
445
+ };
167
446
  const allowedBases = this.#component.patternBases.map((base) => (0, node_path_1.join)(this.#component.directory, base));
168
447
  this.#openCount++;
169
- this.#watcher = chokidar_1.default
448
+ const watcher = (this.#watcher = chokidar_1.default
170
449
  .watch(this.#component.commonPatternBase, {
171
450
  cwd: this.#component.directory,
172
451
  persistent: false,
@@ -200,34 +479,43 @@ class EntryHandler extends node_events_1.EventEmitter {
200
479
  return (normalizedPath !== normalizedDirectory && normalizedBases.every((base) => !normalizedPath.startsWith(base)));
201
480
  },
202
481
  })
203
- .on('all', this.#handleAll.bind(this))
204
- .on('error', this.#handleError.bind(this))
205
- .on('ready', this.#handleReady.bind(this));
206
- return this.ready;
482
+ .on('all', (...args) => this.#handleAll(generation, ...args))
483
+ .on('error', (error) => {
484
+ if (generation === this.#watchGeneration)
485
+ this.#handleWatcherError(error);
486
+ })
487
+ .on('ready', () => this.#handleReady(generation)));
488
+ this.#liveWatchers.add(watcher);
207
489
  }
208
- // Test-only: simulate the underlying chokidar watcher emitting an error.
209
- // Exposed so the polling-fallback path can be exercised without triggering a
210
- // real ENOSPC/EMFILE on the host.
211
490
  _simulateWatcherErrorForTests(error) {
212
- this.#handleError(error);
491
+ this.#handleWatcherError(error);
492
+ }
493
+ _simulateFileReadForTests(event, path, contents) {
494
+ const absolutePath = (0, node_path_1.join)(this.directory, path);
495
+ return this.#queueFileRead(this.#watchGeneration, event, path, absolutePath, undefined, contents);
213
496
  }
214
- // Test-only: whether the watcher has fallen back to polling.
215
497
  get _usingPollingForTests() {
216
498
  return this.#usingPolling;
217
499
  }
218
- // Test-only: number of times the underlying watcher has been (re)opened.
219
- // Used to assert that a close()-during-fallback race didn't install a
220
- // replacement watcher.
221
500
  get _openCountForTests() {
222
501
  return this.#openCount;
223
502
  }
503
+ get _liveWatcherCountForTests() {
504
+ return this.#liveWatchers.size;
505
+ }
224
506
  close() {
507
+ this.#watchGeneration++;
225
508
  this.#closed = true;
226
509
  const pendingReads = [...this.#pendingFileReads];
227
- const watcherClose = this.#watcher ? Promise.resolve(this.#watcher.close()).catch(() => { }) : Promise.resolve();
510
+ const outgoing = this.#watcher;
511
+ const watcherClose = outgoing
512
+ ? Promise.resolve(outgoing.close())
513
+ .catch(() => { })
514
+ .then(() => {
515
+ this.#liveWatchers.delete(outgoing);
516
+ })
517
+ : Promise.resolve();
228
518
  this.#watcher = undefined;
229
- // If paused, there may be an in-flight close from pause() that hasn't settled yet.
230
- // Include it so close() doesn't resolve while inotify handles are still releasing.
231
519
  const pausedClose = this.#pausedClose ?? Promise.resolve();
232
520
  this.#pausedClose = undefined;
233
521
  this.emit('close');
@@ -244,27 +532,25 @@ class EntryHandler extends node_events_1.EventEmitter {
244
532
  * Idempotent. Awaiting `ready` while paused will not resolve until resume().
245
533
  */
246
534
  pause() {
535
+ if (this.#paused || this.#closed)
536
+ return;
537
+ this.#watchGeneration++;
247
538
  this.#paused = true;
248
- // Reset `ready` to a fresh pending promise so the documented "awaiting
249
- // ready while paused will not resolve until resume()" contract holds even
250
- // when the watcher had already become ready before pause(). The next
251
- // 'ready' emit will come from the chokidar instance installed by resume().
252
- this.ready = (0, node_events_1.once)(this, 'ready');
539
+ this.ready = this.#createReadyLatch();
253
540
  if (this.#watcher) {
254
- // Retain the close promise so resume()→#watch() can await full
255
- // teardown before opening a new watcher.
256
- this.#pausedClose = Promise.resolve(this.#watcher.close()).catch(() => {
257
- // Teardown errors aren't actionable; swallow so resume can proceed.
541
+ const outgoing = this.#watcher;
542
+ this.#pausedClose = Promise.resolve(outgoing.close())
543
+ .catch(() => { })
544
+ .then(() => {
545
+ this.#liveWatchers.delete(outgoing);
258
546
  });
259
547
  this.#watcher = undefined;
260
548
  }
261
549
  }
262
550
  /**
263
551
  * Reinstate the watcher previously stopped by pause(). The fresh chokidar
264
- * instance does an initial scan and emits add events for every file
265
- * currently matching the configured globs by design, since the typical
266
- * caller (Scope, on deploy:end) wants plugins to see the post-deploy tree
267
- * as if loading cold.
552
+ * instance scans the post-pause tree, compares it with the retained snapshot,
553
+ * and emits only the logical add, change, unlink, addDir, and unlinkDir events.
268
554
  *
269
555
  * No-op if not currently paused.
270
556
  */
@@ -272,12 +558,17 @@ class EntryHandler extends node_events_1.EventEmitter {
272
558
  if (!this.#paused)
273
559
  return this.ready;
274
560
  this.#paused = false;
275
- // `this.ready` was already reset to a pending promise in pause(); just
276
- // trigger the watcher recreation and let its 'ready' emit resolve it.
277
561
  return this.#watch();
278
562
  }
279
563
  update(config) {
564
+ if (this.#closed)
565
+ return this.ready;
566
+ this.#watchGeneration++;
280
567
  this.#component = new Component_ts_1.Component(this.name, this.directory, castConfig(config));
568
+ // Re-arm the readiness latch for parity with pause(). Without this, awaiting update() on an
569
+ // already-ready handler resolves against the outgoing generation's 'ready' — before the
570
+ // replacement generation has scanned, digested, and emitted its logical events.
571
+ this.ready = this.#createReadyLatch();
281
572
  return this.#watch();
282
573
  }
283
574
  }