@harperfast/harper 5.2.0 → 5.2.2

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 (308) hide show
  1. package/bin/copyDb.ts +40 -11
  2. package/bin/harper.ts +20 -51
  3. package/bin/help.ts +216 -0
  4. package/components/Application.ts +236 -46
  5. package/components/ApplicationScope.ts +26 -0
  6. package/components/EntryHandler.ts +410 -105
  7. package/components/RuntimeModuleTracker.ts +189 -0
  8. package/components/Scope.ts +68 -24
  9. package/components/componentLoader.ts +70 -16
  10. package/components/deployLifecycle.ts +119 -33
  11. package/components/deploymentRecorder.ts +102 -11
  12. package/components/mcp/tools/operations.ts +6 -0
  13. package/components/operations.js +8 -6
  14. package/config/configUtils.ts +78 -20
  15. package/config/harperConfigEnvVars.ts +114 -17
  16. package/config-root.schema.json +10 -0
  17. package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
  18. package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
  19. package/dataLayer/hdbInfoController.ts +8 -0
  20. package/dataLayer/schemaDescribe.ts +2 -1
  21. package/dist/bin/copyDb.js +33 -9
  22. package/dist/bin/copyDb.js.map +1 -1
  23. package/dist/bin/harper.d.ts +6 -0
  24. package/dist/bin/harper.js +18 -50
  25. package/dist/bin/harper.js.map +1 -1
  26. package/dist/bin/help.d.ts +8 -0
  27. package/dist/bin/help.js +192 -0
  28. package/dist/bin/help.js.map +1 -0
  29. package/dist/components/Application.d.ts +16 -1
  30. package/dist/components/Application.js +210 -38
  31. package/dist/components/Application.js.map +1 -1
  32. package/dist/components/ApplicationScope.d.ts +7 -0
  33. package/dist/components/ApplicationScope.js +22 -0
  34. package/dist/components/ApplicationScope.js.map +1 -1
  35. package/dist/components/EntryHandler.d.ts +4 -4
  36. package/dist/components/EntryHandler.js +386 -95
  37. package/dist/components/EntryHandler.js.map +1 -1
  38. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  39. package/dist/components/RuntimeModuleTracker.js +189 -0
  40. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  41. package/dist/components/Scope.d.ts +1 -0
  42. package/dist/components/Scope.js +69 -22
  43. package/dist/components/Scope.js.map +1 -1
  44. package/dist/components/componentLoader.js +69 -10
  45. package/dist/components/componentLoader.js.map +1 -1
  46. package/dist/components/deployLifecycle.d.ts +6 -2
  47. package/dist/components/deployLifecycle.js +109 -31
  48. package/dist/components/deployLifecycle.js.map +1 -1
  49. package/dist/components/deploymentRecorder.d.ts +6 -0
  50. package/dist/components/deploymentRecorder.js +94 -12
  51. package/dist/components/deploymentRecorder.js.map +1 -1
  52. package/dist/components/mcp/tools/operations.js +6 -0
  53. package/dist/components/mcp/tools/operations.js.map +1 -1
  54. package/dist/components/operations.js +8 -6
  55. package/dist/components/operations.js.map +1 -1
  56. package/dist/config/configUtils.d.ts +6 -0
  57. package/dist/config/configUtils.js +78 -18
  58. package/dist/config/configUtils.js.map +1 -1
  59. package/dist/config/harperConfigEnvVars.js +108 -16
  60. package/dist/config/harperConfigEnvVars.js.map +1 -1
  61. package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
  62. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  63. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
  64. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
  65. package/dist/dataLayer/hdbInfoController.js +4 -0
  66. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  67. package/dist/dataLayer/schemaDescribe.js +2 -1
  68. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  69. package/dist/resources/DatabaseTransaction.d.ts +57 -0
  70. package/dist/resources/DatabaseTransaction.js +284 -104
  71. package/dist/resources/DatabaseTransaction.js.map +1 -1
  72. package/dist/resources/ResourceInterface.d.ts +8 -2
  73. package/dist/resources/ResourceInterface.js.map +1 -1
  74. package/dist/resources/Resources.js +22 -4
  75. package/dist/resources/Resources.js.map +1 -1
  76. package/dist/resources/Table.d.ts +8 -6
  77. package/dist/resources/Table.js +158 -31
  78. package/dist/resources/Table.js.map +1 -1
  79. package/dist/resources/analytics/write.js +6 -6
  80. package/dist/resources/analytics/write.js.map +1 -1
  81. package/dist/resources/auditStore.js +27 -1
  82. package/dist/resources/auditStore.js.map +1 -1
  83. package/dist/resources/blob.d.ts +0 -1
  84. package/dist/resources/blob.js +15 -7
  85. package/dist/resources/blob.js.map +1 -1
  86. package/dist/resources/databases.d.ts +22 -5
  87. package/dist/resources/databases.js +118 -9
  88. package/dist/resources/databases.js.map +1 -1
  89. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
  90. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
  91. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  92. package/dist/resources/jsResource.d.ts +4 -26
  93. package/dist/resources/jsResource.js +5 -59
  94. package/dist/resources/jsResource.js.map +1 -1
  95. package/dist/resources/models/Models.d.ts +11 -1
  96. package/dist/resources/models/Models.js +10 -1
  97. package/dist/resources/models/Models.js.map +1 -1
  98. package/dist/resources/models/backendRegistry.d.ts +9 -0
  99. package/dist/resources/models/backendRegistry.js +10 -0
  100. package/dist/resources/models/backendRegistry.js.map +1 -1
  101. package/dist/resources/models/openaiStream.d.ts +16 -1
  102. package/dist/resources/models/openaiStream.js +113 -21
  103. package/dist/resources/models/openaiStream.js.map +1 -1
  104. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  105. package/dist/resources/models/v1/chatCompletions.js +115 -0
  106. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  107. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  108. package/dist/resources/models/v1/embeddings.js +71 -0
  109. package/dist/resources/models/v1/embeddings.js.map +1 -0
  110. package/dist/resources/models/v1/errors.d.ts +54 -0
  111. package/dist/resources/models/v1/errors.js +130 -0
  112. package/dist/resources/models/v1/errors.js.map +1 -0
  113. package/dist/resources/models/v1/index.d.ts +36 -0
  114. package/dist/resources/models/v1/index.js +75 -0
  115. package/dist/resources/models/v1/index.js.map +1 -0
  116. package/dist/resources/models/v1/models.d.ts +26 -0
  117. package/dist/resources/models/v1/models.js +44 -0
  118. package/dist/resources/models/v1/models.js.map +1 -0
  119. package/dist/resources/models/v1/translation.d.ts +133 -0
  120. package/dist/resources/models/v1/translation.js +298 -0
  121. package/dist/resources/models/v1/translation.js.map +1 -0
  122. package/dist/resources/roles.d.ts +1 -1
  123. package/dist/resources/roles.js +54 -7
  124. package/dist/resources/roles.js.map +1 -1
  125. package/dist/resources/search.d.ts +1 -1
  126. package/dist/resources/search.js +7 -3
  127. package/dist/resources/search.js.map +1 -1
  128. package/dist/security/jsLoader.js +84 -33
  129. package/dist/security/jsLoader.js.map +1 -1
  130. package/dist/security/role.js +4 -0
  131. package/dist/security/role.js.map +1 -1
  132. package/dist/security/superUserGuard.d.ts +7 -0
  133. package/dist/security/superUserGuard.js +23 -0
  134. package/dist/security/superUserGuard.js.map +1 -0
  135. package/dist/security/tokenAuthentication.d.ts +0 -1
  136. package/dist/security/tokenAuthentication.js +6 -2
  137. package/dist/security/tokenAuthentication.js.map +1 -1
  138. package/dist/security/user.d.ts +6 -1
  139. package/dist/security/user.js +23 -1
  140. package/dist/security/user.js.map +1 -1
  141. package/dist/server/http.d.ts +23 -2
  142. package/dist/server/http.js +99 -12
  143. package/dist/server/http.js.map +1 -1
  144. package/dist/server/operationsServer.d.ts +0 -2
  145. package/dist/server/operationsServer.js.map +1 -1
  146. package/dist/server/serverHelpers/multipartParser.js +9 -0
  147. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  148. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  149. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  150. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  151. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  152. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  153. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  154. package/dist/server/serverHelpers/serverHandlers.js +19 -3
  155. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  156. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  157. package/dist/server/serverHelpers/serverUtilities.js +10 -7
  158. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  159. package/dist/server/static.js +57 -18
  160. package/dist/server/static.js.map +1 -1
  161. package/dist/server/storageReclamation.d.ts +17 -0
  162. package/dist/server/storageReclamation.js +90 -6
  163. package/dist/server/storageReclamation.js.map +1 -1
  164. package/dist/server/threads/manageThreads.js +8 -0
  165. package/dist/server/threads/manageThreads.js.map +1 -1
  166. package/dist/server/threads/socketRouter.js +20 -0
  167. package/dist/server/threads/socketRouter.js.map +1 -1
  168. package/dist/server/threads/threadServer.js +12 -0
  169. package/dist/server/threads/threadServer.js.map +1 -1
  170. package/dist/sqlEngine/diff/differential.js +7 -3
  171. package/dist/sqlEngine/diff/differential.js.map +1 -1
  172. package/dist/sqlEngine/parser/normalizer.js +37 -9
  173. package/dist/sqlEngine/parser/normalizer.js.map +1 -1
  174. package/dist/sqlTranslator/index.js +6 -1
  175. package/dist/sqlTranslator/index.js.map +1 -1
  176. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  177. package/dist/upgrade/upgradePrompt.js +22 -3
  178. package/dist/upgrade/upgradePrompt.js.map +1 -1
  179. package/dist/utility/environment/environmentManager.d.ts +11 -0
  180. package/dist/utility/environment/environmentManager.js +154 -0
  181. package/dist/utility/environment/environmentManager.js.map +1 -1
  182. package/dist/utility/errors/commonErrors.d.ts +1 -0
  183. package/dist/utility/errors/commonErrors.js +1 -0
  184. package/dist/utility/errors/commonErrors.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/install/installer.js +32 -0
  189. package/dist/utility/install/installer.js.map +1 -1
  190. package/dist/utility/lmdb/environmentUtility.js +2 -1
  191. package/dist/utility/lmdb/environmentUtility.js.map +1 -1
  192. package/dist/utility/logging/harper_logger.d.ts +7 -0
  193. package/dist/utility/logging/harper_logger.js +57 -24
  194. package/dist/utility/logging/harper_logger.js.map +1 -1
  195. package/npm-shrinkwrap.json +208 -198
  196. package/package.json +10 -8
  197. package/resources/DESIGN.md +1 -0
  198. package/resources/DatabaseTransaction.ts +313 -102
  199. package/resources/ResourceInterface.ts +8 -2
  200. package/resources/Resources.ts +22 -4
  201. package/resources/Table.ts +398 -259
  202. package/resources/analytics/write.ts +22 -20
  203. package/resources/auditStore.ts +32 -1
  204. package/resources/blob.ts +15 -8
  205. package/resources/databases.ts +123 -12
  206. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
  207. package/resources/jsResource.ts +5 -62
  208. package/resources/models/Models.ts +14 -1
  209. package/resources/models/backendRegistry.ts +10 -0
  210. package/resources/models/openaiStream.ts +131 -19
  211. package/resources/models/v1/chatCompletions.ts +128 -0
  212. package/resources/models/v1/embeddings.ts +70 -0
  213. package/resources/models/v1/errors.ts +141 -0
  214. package/resources/models/v1/index.ts +72 -0
  215. package/resources/models/v1/models.ts +53 -0
  216. package/resources/models/v1/translation.ts +362 -0
  217. package/resources/roles.ts +67 -7
  218. package/resources/search.ts +8 -3
  219. package/security/jsLoader.ts +84 -30
  220. package/security/role.ts +7 -0
  221. package/security/superUserGuard.ts +20 -0
  222. package/security/tokenAuthentication.ts +6 -3
  223. package/security/user.ts +26 -1
  224. package/server/DESIGN.md +45 -34
  225. package/server/http.ts +100 -13
  226. package/server/operationsServer.ts +0 -2
  227. package/server/serverHelpers/multipartParser.ts +9 -0
  228. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  229. package/server/serverHelpers/registeredOperations.ts +19 -15
  230. package/server/serverHelpers/serverHandlers.js +20 -3
  231. package/server/serverHelpers/serverUtilities.ts +10 -7
  232. package/server/static.ts +75 -20
  233. package/server/storageReclamation.ts +104 -8
  234. package/server/threads/manageThreads.js +8 -0
  235. package/server/threads/socketRouter.ts +20 -0
  236. package/server/threads/threadServer.js +11 -0
  237. package/sqlTranslator/index.ts +6 -1
  238. package/static/defaultConfig.yaml +2 -0
  239. package/studio/web/assets/{Chat-DoVWScmq.js → Chat-BMUjPklt.js} +5 -5
  240. package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-BMUjPklt.js.map} +1 -1
  241. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-Cbghuxe5.js} +5 -5
  242. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
  243. package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-CkgTQAqc.js} +2 -2
  244. package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-CkgTQAqc.js.map} +1 -1
  245. package/studio/web/assets/{applications-D03NA7wW.js → applications-WvF4NOTN.js} +13 -13
  246. package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
  247. package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
  248. package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
  249. package/studio/web/assets/index-CDeqVx71.css +1 -0
  250. package/studio/web/assets/index-Sevt3MVx.js +819 -0
  251. package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
  252. package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
  253. package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
  254. package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
  255. package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
  256. package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
  257. package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
  258. package/studio/web/assets/{notifications-0edoFTsb.js → notifications-Ap84YyQp.js} +2 -2
  259. package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-Ap84YyQp.js.map} +1 -1
  260. package/studio/web/assets/notifications-vehqyOLM.js +2 -0
  261. package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-vehqyOLM.js.map} +1 -1
  262. package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
  263. package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
  264. package/studio/web/assets/{profile-DUfEPQtx.js → profile-DzDpRLHY.js} +2 -2
  265. package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-DzDpRLHY.js.map} +1 -1
  266. package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
  267. package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
  268. package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
  269. package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
  270. package/studio/web/assets/register-BDy01-q3.js +3 -0
  271. package/studio/web/assets/register-BDy01-q3.js.map +1 -0
  272. package/studio/web/assets/register-CQHRwNP4.js +3 -0
  273. package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
  274. package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-D0aPcI3W.js} +2 -2
  275. package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
  276. package/studio/web/assets/setup-I15FAF9H.js +3 -0
  277. package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-I15FAF9H.js.map} +1 -1
  278. package/studio/web/assets/status-BF1eJshJ.js +62 -0
  279. package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
  280. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-DwSPXXhl.js} +2 -2
  281. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
  282. package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
  283. package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
  284. package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-Bu51A-VG.js} +2 -2
  285. package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-Bu51A-VG.js.map} +1 -1
  286. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-DrV1zMrN.js} +2 -2
  287. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
  288. package/studio/web/index.html +6 -7
  289. package/upgrade/upgradePrompt.ts +22 -3
  290. package/utility/environment/environmentManager.ts +152 -0
  291. package/utility/errors/commonErrors.ts +2 -0
  292. package/utility/hdbTerms.ts +2 -0
  293. package/utility/install/installer.ts +37 -0
  294. package/utility/lmdb/environmentUtility.ts +2 -1
  295. package/utility/logging/harper_logger.ts +57 -26
  296. package/studio/web/assets/applications-D03NA7wW.js.map +0 -1
  297. package/studio/web/assets/index-Bh_CNAHr.js +0 -809
  298. package/studio/web/assets/index-Bh_CNAHr.js.map +0 -1
  299. package/studio/web/assets/index-C8Mv9kHV.css +0 -1
  300. package/studio/web/assets/index.lazy-Dx3MpyDC.js +0 -14
  301. package/studio/web/assets/notifications-CwKhipK7.js +0 -2
  302. package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
  303. package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
  304. package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
  305. package/studio/web/assets/setup-B56Oz1_u.js +0 -3
  306. package/studio/web/assets/status-BAod7p3o.js +0 -62
  307. package/studio/web/assets/status-BAod7p3o.js.map +0 -1
  308. package/studio/web/assets/textarea-DhLMZ3PA.js.map +0 -1
@@ -483,10 +483,106 @@ const COMPONENT_PREPARATION_WAIT_MARGIN_MS = 30000;
483
483
  const MAX_GIT_EXTRACTION_COMMANDS = 4;
484
484
  const MAX_INSTALL_COMMANDS = 2;
485
485
 
486
+ type ExtractionTransaction = {
487
+ commit(): Promise<void>;
488
+ rollback(): Promise<void>;
489
+ };
490
+
486
491
  // The credential helper git executes for a private git-reference deploy. It ships alongside this
487
492
  // module (both in source and in dist), holds no secret, and is inert without a live session.
488
493
  export const GIT_CREDENTIAL_HELPER_PATH = join(__dirname, 'gitCredentialHelper.js');
489
494
 
495
+ const PACKAGE_LOCK_FILES = [
496
+ 'package-lock.json',
497
+ 'npm-shrinkwrap.json',
498
+ 'pnpm-lock.yaml',
499
+ 'yarn.lock',
500
+ 'bun.lock',
501
+ 'bun.lockb',
502
+ ];
503
+
504
+ type InstalledPackageMetadata = {
505
+ files: Map<string, Buffer>;
506
+ readable: boolean;
507
+ hasLockfile: boolean;
508
+ hasInstallableDependencies: boolean;
509
+ };
510
+
511
+ export async function readInstalledPackageMetadata(directory: string): Promise<InstalledPackageMetadata> {
512
+ const files = new Map<string, Buffer>();
513
+ let readable = true;
514
+ let packageJSON: any;
515
+ await Promise.all([
516
+ (async () => {
517
+ try {
518
+ const contents = await readFile(join(directory, 'package.json'));
519
+ try {
520
+ packageJSON = JSON.parse(contents.toString());
521
+ files.set('package.json', Buffer.from(JSON.stringify(canonicalizeJSON(packageJSON))));
522
+ } catch {
523
+ files.set('package.json', contents);
524
+ }
525
+ } catch (error) {
526
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') readable = false;
527
+ }
528
+ })(),
529
+ ...PACKAGE_LOCK_FILES.map(async (filename) => {
530
+ try {
531
+ files.set(filename, await readFile(join(directory, filename)));
532
+ } catch (error) {
533
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') readable = false;
534
+ }
535
+ }),
536
+ ]);
537
+ return {
538
+ files,
539
+ readable,
540
+ hasLockfile: PACKAGE_LOCK_FILES.some((filename) => files.has(filename)),
541
+ hasInstallableDependencies: ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].some(
542
+ (field) => {
543
+ const dependencies = packageJSON?.[field];
544
+ return (
545
+ typeof dependencies === 'object' &&
546
+ dependencies !== null &&
547
+ !Array.isArray(dependencies) &&
548
+ Object.keys(dependencies).length > 0
549
+ );
550
+ }
551
+ ),
552
+ };
553
+ }
554
+
555
+ export function installedPackageMetadataEqual(
556
+ previous: InstalledPackageMetadata,
557
+ current: InstalledPackageMetadata
558
+ ): boolean {
559
+ if (!previous.readable || !current.readable || previous.files.size !== current.files.size) return false;
560
+ for (const [filename, contents] of previous.files) {
561
+ if (!current.files.get(filename)?.equals(contents)) return false;
562
+ }
563
+ return true;
564
+ }
565
+
566
+ export function installedRuntimeChanged(
567
+ previous: InstalledPackageMetadata,
568
+ current: InstalledPackageMetadata,
569
+ installationIsOpaque: boolean
570
+ ): boolean {
571
+ return (
572
+ installationIsOpaque ||
573
+ (current.hasInstallableDependencies && !current.hasLockfile) ||
574
+ !installedPackageMetadataEqual(previous, current)
575
+ );
576
+ }
577
+
578
+ function canonicalizeJSON(value: any): any {
579
+ if (Array.isArray(value)) return value.map(canonicalizeJSON);
580
+ if (!value || typeof value !== 'object') return value;
581
+ const canonical: Record<string, any> = Object.create(null);
582
+ for (const key of Object.keys(value).sort()) canonical[key] = canonicalizeJSON(value[key]);
583
+ return canonical;
584
+ }
585
+
490
586
  /**
491
587
  * Extract an application given payload (content of the application) or package (npm-compatible identifier to the application).
492
588
  *
@@ -497,7 +593,10 @@ export const GIT_CREDENTIAL_HELPER_PATH = join(__dirname, 'gitCredentialHelper.j
497
593
  * This method may be called from any Harper thread. Same-component calls are serialized across
498
594
  * threads by the preparation lock below.
499
595
  */
500
- export async function extractApplication(application: Application) {
596
+ export async function extractApplication(
597
+ application: Application,
598
+ deferCommit = false
599
+ ): Promise<ExtractionTransaction | undefined> {
501
600
  // Can't specify neither
502
601
  if (!application.payload && !application.packageIdentifier) {
503
602
  throw new Error('Either payload or package must be provided');
@@ -507,7 +606,6 @@ export async function extractApplication(application: Application) {
507
606
  if (application.payload && application.packageIdentifier) {
508
607
  throw new Error('Both payload and package cannot be provided');
509
608
  }
510
-
511
609
  // Resolve the tarball from the input
512
610
  let tarballPath: string;
513
611
  let tarball: Readable;
@@ -576,6 +674,7 @@ export async function extractApplication(application: Application) {
576
674
  // not have, so this gates the pack step regardless of whether a credential happens to be in
577
675
  // play.
578
676
  const allowScripts = !!application.install?.allowInstallScripts;
677
+ if (allowScripts) application.installationIsOpaque = true;
579
678
  // `--ignore-scripts` alone isn't a reliable way to enforce that: pacote's DirFetcher runs a
580
679
  // git source's `prepare` unconditionally on npm versions before 11.0.0 (see
581
680
  // packGitReferenceWithoutScripts), which is exactly what Node 22's bundled npm ships. For a
@@ -630,12 +729,13 @@ export async function extractApplication(application: Application) {
630
729
  // component, and the per-component path means a sibling component never collides
631
730
  // with (or sweeps) another's aside.
632
731
  const asideStagingDir = join(dirname(application.dirPath), ASIDE_STAGING_DIR, basename(application.dirPath));
633
- let didRenameAside = false;
732
+ let asidePath: string | undefined;
634
733
  try {
635
734
  await access(application.dirPath, constants.F_OK);
636
735
  await mkdir(asideStagingDir, { recursive: true });
637
- await rename(application.dirPath, join(asideStagingDir, `${process.pid}-${Date.now()}-${randomUUID()}`));
638
- didRenameAside = true;
736
+ const candidateAsidePath = join(asideStagingDir, `${process.pid}-${Date.now()}-${randomUUID()}`);
737
+ await rename(application.dirPath, candidateAsidePath);
738
+ asidePath = candidateAsidePath;
639
739
  } catch (err) {
640
740
  // Ignore does not exist error
641
741
  if (err.code !== 'ENOENT') {
@@ -644,35 +744,37 @@ export async function extractApplication(application: Application) {
644
744
  }
645
745
  // A directory existed for this component name prior to this deploy, so this is a redeploy of
646
746
  // an already-active component rather than a first-time deploy. See `isNewComponent` above.
647
- if (didRenameAside) application.isNewComponent = false;
648
- // Finally, create the application directory fresh
649
- await mkdir(application.dirPath, { recursive: true });
650
-
651
- // Now pipeline the tarball into maybe-gunzip then tar-fs to reliably decompress and extract the contents
652
- await pipeline(tarball, gunzip(), extract(application.dirPath));
653
-
654
- // If the extracted directory contains a single folder, move the contents up one level
655
- // The `npm pack` command does this (the top-level folder is called "package")
656
- // Other packing tools may have similar behavior, but the directory name is not guaranteed.
657
- const extracted = await readdir(application.dirPath, { withFileTypes: true });
658
- if (extracted.length === 1 && extracted[0].isDirectory()) {
659
- const topLevelDirPath = join(application.dirPath, extracted[0].name);
660
-
661
- const tempDirPath = await mkdtemp(application.dirPath);
662
-
663
- // Copy contents of top-level directory to temp directory (in order to avoid collisions of top-level directory name and one of the contents)
664
- await cp(topLevelDirPath, tempDirPath, { recursive: true });
665
- // Remove top-level directory
666
- await rm(topLevelDirPath, { recursive: true, force: true });
667
- // Copy contents of temp directory to application directory
668
- await cp(tempDirPath, application.dirPath, { recursive: true });
669
- // Finally, remove the temp dir
670
- await rm(tempDirPath, { recursive: true, force: true });
747
+ if (asidePath) application.isNewComponent = false;
748
+ try {
749
+ await mkdir(application.dirPath, { recursive: true });
750
+ await pipeline(tarball, gunzip(), extract(application.dirPath));
751
+
752
+ const extracted = await readdir(application.dirPath, { withFileTypes: true });
753
+ if (extracted.length === 1 && extracted[0].isDirectory()) {
754
+ const topLevelDirPath = join(application.dirPath, extracted[0].name);
755
+ await mkdir(asideStagingDir, { recursive: true });
756
+ const tempDirPath = await mkdtemp(join(asideStagingDir, '.normalize-'));
757
+ await cp(topLevelDirPath, tempDirPath, { recursive: true });
758
+ await rm(topLevelDirPath, { recursive: true, force: true });
759
+ await cp(tempDirPath, application.dirPath, { recursive: true });
760
+ await rm(tempDirPath, { recursive: true, force: true });
761
+ }
762
+ } catch (error) {
763
+ try {
764
+ await rollbackExtractedDirectory(application, asideStagingDir, asidePath);
765
+ } catch (rollbackError) {
766
+ throw new AggregateError(
767
+ [error, rollbackError],
768
+ `Failed to extract ${application.name} and restore its previous component directory`
769
+ );
770
+ }
771
+ throw error;
671
772
  }
672
-
673
773
  // Clean up the original tarball
674
774
  if (shouldDeleteTarball && tarballPath) {
675
- await rm(tarballPath, { force: true });
775
+ await rm(tarballPath, { force: true }).catch((error) =>
776
+ application.logger.warn(`Failed to remove temporary package ${tarballPath}:`, error)
777
+ );
676
778
  }
677
779
 
678
780
  // Remove this component's aside copies. The old worker may still hold files open
@@ -681,11 +783,76 @@ export async function extractApplication(application: Application) {
681
783
  // earlier deploys whose workers have since exited, and a copy that survives because
682
784
  // its worker is still live is swept by the next deploy. The failure is expected in
683
785
  // the live-worker case, so it's logged at trace rather than as a warning.
684
- if (didRenameAside) {
685
- rm(asideStagingDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((err) =>
686
- logger.trace?.(`Deferred cleanup of previous ${application.name} component directory: ${err.message}`)
687
- );
786
+ let settled = false;
787
+ const transaction: ExtractionTransaction = {
788
+ async commit() {
789
+ if (settled) return;
790
+ settled = true;
791
+ await cleanupExtractionStaging(application, asideStagingDir);
792
+ },
793
+ async rollback() {
794
+ if (settled) return;
795
+ settled = true;
796
+ await rollbackExtractedDirectory(application, asideStagingDir, asidePath);
797
+ },
798
+ };
799
+ if (deferCommit) return transaction;
800
+ await transaction.commit();
801
+ }
802
+
803
+ async function cleanupExtractionStaging(application: Application, asideStagingDir: string): Promise<void> {
804
+ try {
805
+ await rm(asideStagingDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
806
+ } catch (error) {
807
+ logger.trace?.(`Cleanup of previous ${application.name} component directory deferred: ${error.message}`);
808
+ }
809
+ }
810
+
811
+ async function rollbackExtractedDirectory(
812
+ application: Application,
813
+ asideStagingDir: string,
814
+ asidePath: string | undefined
815
+ ): Promise<void> {
816
+ await mkdir(asideStagingDir, { recursive: true });
817
+ const retryableRenameCodes = new Set(['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES', 'EBUSY']);
818
+ const displaceCurrentDirectory = async () => {
819
+ const displacedPath = join(asideStagingDir, `.failed-${process.pid}-${Date.now()}-${randomUUID()}`);
820
+ let lastError: unknown;
821
+ for (let attempt = 0; attempt < 100; attempt++) {
822
+ try {
823
+ await rename(application.dirPath, displacedPath);
824
+ return;
825
+ } catch (error) {
826
+ const code = (error as NodeJS.ErrnoException).code;
827
+ if (code === 'ENOENT') return;
828
+ if (!retryableRenameCodes.has(code ?? '')) throw error;
829
+ lastError = error;
830
+ await delay(10);
831
+ }
832
+ }
833
+ throw lastError;
834
+ };
835
+
836
+ await displaceCurrentDirectory();
837
+ if (asidePath) {
838
+ let restored = false;
839
+ let restoreError: unknown;
840
+ for (let attempt = 0; attempt < 100; attempt++) {
841
+ try {
842
+ await rename(asidePath, application.dirPath);
843
+ restored = true;
844
+ break;
845
+ } catch (error) {
846
+ restoreError = error;
847
+ if (!retryableRenameCodes.has((error as NodeJS.ErrnoException).code ?? '')) break;
848
+ await displaceCurrentDirectory();
849
+ await delay(10);
850
+ }
851
+ }
852
+ if (!restored) throw restoreError;
688
853
  }
854
+
855
+ await cleanupExtractionStaging(application, asideStagingDir);
689
856
  }
690
857
 
691
858
  /**
@@ -711,7 +878,10 @@ export async function installApplication(application: Application) {
711
878
  try {
712
879
  // Does node_modules exist?
713
880
  await access(join(application.dirPath, 'node_modules'), constants.F_OK);
714
- application.logger.info(`Application ${application.name} already has node_modules; skipping install`);
881
+ application.logger.info(
882
+ `Application ${application.name} already has node_modules; skipping install and treating the runtime as opaque for redeploy comparison`
883
+ );
884
+ application.installationIsOpaque = true;
715
885
  return;
716
886
  } catch (err) {
717
887
  if (err.code !== 'ENOENT') throw err;
@@ -735,6 +905,7 @@ export async function installApplication(application: Application) {
735
905
  );
736
906
  // if it succeeds, return
737
907
  if (code === 0) {
908
+ application.installationIsOpaque = true;
738
909
  return;
739
910
  }
740
911
  if (stdout) {
@@ -797,6 +968,7 @@ export async function installApplication(application: Application) {
797
968
 
798
969
  // if it succeeds, return
799
970
  if (code === 0) {
971
+ if (application.install?.allowInstallScripts) application.installationIsOpaque = true;
800
972
  return;
801
973
  }
802
974
 
@@ -854,6 +1026,7 @@ export async function installApplication(application: Application) {
854
1026
 
855
1027
  // if it succeeds, return
856
1028
  if (code === 0) {
1029
+ if (application.install?.allowInstallScripts) application.installationIsOpaque = true;
857
1030
  return;
858
1031
  }
859
1032
 
@@ -911,14 +1084,10 @@ export class Application {
911
1084
  npmUserconfigPath?: string;
912
1085
  #npmrcTempDir?: string;
913
1086
  #gitCredentialSession?: GitCredentialSession;
914
- // Whether this component's directory did not already exist when extractApplication ran
915
- // i.e. this deploy is the component's first, as opposed to a redeploy of something already
916
- // active. Defaults true and is flipped to false by extractApplication when it finds (and
917
- // renames aside) a pre-existing directory for this component name. Used by deployComponent to
918
- // scope its unconditional requestRestart() call to genuinely new components (harper#1806):
919
- // an existing, already-loaded component already has a live file watcher (Scope/EntryHandler)
920
- // that independently requests a restart if the redeploy actually needs one.
1087
+ // Existing components rely on their runtime-equivalence checks; only a first deploy restarts unconditionally.
921
1088
  isNewComponent: boolean = true;
1089
+ packageMetadataChanged: boolean = false;
1090
+ installationIsOpaque: boolean = false;
922
1091
 
923
1092
  constructor({ name, payload, packageIdentifier, install, onInstallLine, credentials }: ApplicationOptions) {
924
1093
  this.name = name;
@@ -1064,12 +1233,14 @@ export function derivePackageIdentifier(packageIdentifier: string) {
1064
1233
  * @returns A promise that resolves when all preparation steps complete.
1065
1234
  */
1066
1235
  export async function prepareApplication(application: Application) {
1067
- await broadcastDeployStart(application.name);
1236
+ const deploymentId = await broadcastDeployStart(application.name);
1068
1237
  try {
1069
1238
  const commandTimeoutMs = application.install?.timeout ?? DEFAULT_COMMAND_TIMEOUT_MS;
1070
1239
  await withComponentPreparationLock(
1071
1240
  application.dirPath,
1072
1241
  async () => {
1242
+ const previousPackageMetadata = await readInstalledPackageMetadata(application.dirPath);
1243
+ let extraction: ExtractionTransaction | undefined;
1073
1244
  try {
1074
1245
  // Materialize the per-deploy `.npmrc` before extraction so both `npm pack` (extract) and
1075
1246
  // `npm install` authenticate against the private registry; always remove it afterward.
@@ -1080,11 +1251,30 @@ export async function prepareApplication(application: Application) {
1080
1251
  // already gone by the time the component's dependency tree (and any install script it is
1081
1252
  // allowed to run) executes.
1082
1253
  await application.startGitCredentialSession();
1083
- await extractApplication(application);
1254
+ extraction = await extractApplication(application, true);
1084
1255
  } finally {
1085
1256
  await application.cleanupGitCredentialSession();
1086
1257
  }
1087
1258
  await installApplication(application);
1259
+ if (!application.isNewComponent) {
1260
+ const currentPackageMetadata = await readInstalledPackageMetadata(application.dirPath);
1261
+ application.packageMetadataChanged = installedRuntimeChanged(
1262
+ previousPackageMetadata,
1263
+ currentPackageMetadata,
1264
+ application.installationIsOpaque
1265
+ );
1266
+ }
1267
+ await extraction?.commit();
1268
+ } catch (error) {
1269
+ try {
1270
+ await extraction?.rollback();
1271
+ } catch (rollbackError) {
1272
+ throw new AggregateError(
1273
+ [error, rollbackError],
1274
+ `Failed to prepare ${application.name} and restore its previous component directory`
1275
+ );
1276
+ }
1277
+ throw error;
1088
1278
  } finally {
1089
1279
  await application.cleanupTransientNpmrc();
1090
1280
  }
@@ -1111,7 +1301,7 @@ export async function prepareApplication(application: Application) {
1111
1301
  }
1112
1302
  );
1113
1303
  } finally {
1114
- broadcastDeployEnd(application.name);
1304
+ broadcastDeployEnd(application.name, deploymentId);
1115
1305
  }
1116
1306
  }
1117
1307
 
@@ -4,6 +4,8 @@ import { forComponent } from '../utility/logging/harper_logger.ts';
4
4
  import { scopedImport } from '../security/jsLoader.ts';
5
5
  import * as env from '../utility/environment/environmentManager.ts';
6
6
  import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
7
+ import { RuntimeModuleTracker } from './RuntimeModuleTracker.ts';
8
+ import { deployLifecycle } from './deployLifecycle.ts';
7
9
 
8
10
  export class MissingDefaultFilesOptionError extends Error {
9
11
  constructor() {
@@ -24,14 +26,18 @@ export class ApplicationScope {
24
26
  mode?: 'native' | 'vm' | 'vm-current-context' | 'compartment'; // option to set this from the scope
25
27
  dependencyLoader?: 'native' | 'app' | 'auto'; // option to set this from the scope
26
28
  allowedPath?: string;
29
+ runtimeRoot?: string;
27
30
  config: any;
28
31
  moduleCache: any; // used by the loader to retain a cache of modules, type is an internal detail of the loader
32
+ #runtimeModules: RuntimeModuleTracker;
29
33
  constructor(name: string, resources: Resources, server: Server, isInternal = false) {
30
34
  this.name = name;
31
35
  this.logger = forComponent(name, !isInternal);
32
36
 
33
37
  this.resources = resources;
34
38
  this.server = server;
39
+ this.#runtimeModules = new RuntimeModuleTracker(() => this.runtimeRoot);
40
+ if (deployLifecycle.isDeployInFlight(name)) this.#runtimeModules.beginDeploy();
35
41
 
36
42
  this.mode = env.get(CONFIG_PARAMS.APPLICATIONS_MODULELOADER) ?? 'vm-current-context';
37
43
  this.dependencyLoader = env.get(CONFIG_PARAMS.APPLICATIONS_DEPENDENCYLOADER);
@@ -50,4 +56,24 @@ export class ApplicationScope {
50
56
  async import(filePath: string): Promise<unknown> {
51
57
  return scopedImport(filePath, this);
52
58
  }
59
+
60
+ recordLoadedModule(moduleUrl: string, source: string | Buffer): void {
61
+ this.#runtimeModules.recordModule(moduleUrl, source);
62
+ }
63
+
64
+ recordModuleResolution(specifier: string, referrer: string, resolvedUrl: string): void {
65
+ this.#runtimeModules.recordResolution(specifier, referrer, resolvedUrl);
66
+ }
67
+
68
+ markNativeRuntime(): void {
69
+ this.#runtimeModules.markNativeRuntime();
70
+ }
71
+
72
+ beginDeploy(): void {
73
+ this.#runtimeModules.beginDeploy();
74
+ }
75
+
76
+ finishDeploy(): Promise<boolean> {
77
+ return this.#runtimeModules.finishDeploy();
78
+ }
53
79
  }