@harperfast/harper 5.2.0 → 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 (241) hide show
  1. package/bin/copyDb.ts +21 -4
  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/mcp/tools/operations.ts +6 -0
  12. package/components/operations.js +4 -6
  13. package/config/configUtils.ts +12 -9
  14. package/config-root.schema.json +10 -0
  15. package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
  16. package/dataLayer/hdbInfoController.ts +8 -0
  17. package/dataLayer/schemaDescribe.ts +2 -1
  18. package/dist/bin/copyDb.js +13 -2
  19. package/dist/bin/copyDb.js.map +1 -1
  20. package/dist/bin/harper.d.ts +6 -0
  21. package/dist/bin/harper.js +18 -50
  22. package/dist/bin/harper.js.map +1 -1
  23. package/dist/bin/help.d.ts +8 -0
  24. package/dist/bin/help.js +192 -0
  25. package/dist/bin/help.js.map +1 -0
  26. package/dist/components/Application.d.ts +16 -1
  27. package/dist/components/Application.js +210 -38
  28. package/dist/components/Application.js.map +1 -1
  29. package/dist/components/ApplicationScope.d.ts +7 -0
  30. package/dist/components/ApplicationScope.js +22 -0
  31. package/dist/components/ApplicationScope.js.map +1 -1
  32. package/dist/components/EntryHandler.d.ts +4 -4
  33. package/dist/components/EntryHandler.js +386 -95
  34. package/dist/components/EntryHandler.js.map +1 -1
  35. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  36. package/dist/components/RuntimeModuleTracker.js +189 -0
  37. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  38. package/dist/components/Scope.d.ts +1 -0
  39. package/dist/components/Scope.js +69 -22
  40. package/dist/components/Scope.js.map +1 -1
  41. package/dist/components/componentLoader.js +69 -10
  42. package/dist/components/componentLoader.js.map +1 -1
  43. package/dist/components/deployLifecycle.d.ts +6 -2
  44. package/dist/components/deployLifecycle.js +109 -31
  45. package/dist/components/deployLifecycle.js.map +1 -1
  46. package/dist/components/mcp/tools/operations.js +6 -0
  47. package/dist/components/mcp/tools/operations.js.map +1 -1
  48. package/dist/components/operations.js +4 -6
  49. package/dist/components/operations.js.map +1 -1
  50. package/dist/config/configUtils.js +13 -9
  51. package/dist/config/configUtils.js.map +1 -1
  52. package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
  53. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  54. package/dist/dataLayer/hdbInfoController.js +4 -0
  55. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  56. package/dist/dataLayer/schemaDescribe.js +2 -1
  57. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  58. package/dist/resources/DatabaseTransaction.d.ts +55 -0
  59. package/dist/resources/DatabaseTransaction.js +282 -103
  60. package/dist/resources/DatabaseTransaction.js.map +1 -1
  61. package/dist/resources/ResourceInterface.d.ts +8 -2
  62. package/dist/resources/ResourceInterface.js.map +1 -1
  63. package/dist/resources/Resources.js +22 -4
  64. package/dist/resources/Resources.js.map +1 -1
  65. package/dist/resources/Table.d.ts +6 -5
  66. package/dist/resources/Table.js +92 -15
  67. package/dist/resources/Table.js.map +1 -1
  68. package/dist/resources/analytics/write.js +6 -6
  69. package/dist/resources/analytics/write.js.map +1 -1
  70. package/dist/resources/blob.d.ts +0 -1
  71. package/dist/resources/blob.js +15 -7
  72. package/dist/resources/blob.js.map +1 -1
  73. package/dist/resources/databases.d.ts +22 -5
  74. package/dist/resources/databases.js +118 -9
  75. package/dist/resources/databases.js.map +1 -1
  76. package/dist/resources/jsResource.d.ts +4 -26
  77. package/dist/resources/jsResource.js +5 -59
  78. package/dist/resources/jsResource.js.map +1 -1
  79. package/dist/resources/models/Models.d.ts +11 -1
  80. package/dist/resources/models/Models.js +10 -1
  81. package/dist/resources/models/Models.js.map +1 -1
  82. package/dist/resources/models/backendRegistry.d.ts +9 -0
  83. package/dist/resources/models/backendRegistry.js +10 -0
  84. package/dist/resources/models/backendRegistry.js.map +1 -1
  85. package/dist/resources/models/openaiStream.d.ts +16 -1
  86. package/dist/resources/models/openaiStream.js +113 -21
  87. package/dist/resources/models/openaiStream.js.map +1 -1
  88. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  89. package/dist/resources/models/v1/chatCompletions.js +115 -0
  90. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  91. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  92. package/dist/resources/models/v1/embeddings.js +71 -0
  93. package/dist/resources/models/v1/embeddings.js.map +1 -0
  94. package/dist/resources/models/v1/errors.d.ts +54 -0
  95. package/dist/resources/models/v1/errors.js +130 -0
  96. package/dist/resources/models/v1/errors.js.map +1 -0
  97. package/dist/resources/models/v1/index.d.ts +36 -0
  98. package/dist/resources/models/v1/index.js +75 -0
  99. package/dist/resources/models/v1/index.js.map +1 -0
  100. package/dist/resources/models/v1/models.d.ts +26 -0
  101. package/dist/resources/models/v1/models.js +44 -0
  102. package/dist/resources/models/v1/models.js.map +1 -0
  103. package/dist/resources/models/v1/translation.d.ts +133 -0
  104. package/dist/resources/models/v1/translation.js +298 -0
  105. package/dist/resources/models/v1/translation.js.map +1 -0
  106. package/dist/resources/roles.d.ts +1 -1
  107. package/dist/resources/roles.js +54 -7
  108. package/dist/resources/roles.js.map +1 -1
  109. package/dist/security/jsLoader.js +84 -33
  110. package/dist/security/jsLoader.js.map +1 -1
  111. package/dist/security/role.js +4 -0
  112. package/dist/security/role.js.map +1 -1
  113. package/dist/security/superUserGuard.d.ts +7 -0
  114. package/dist/security/superUserGuard.js +23 -0
  115. package/dist/security/superUserGuard.js.map +1 -0
  116. package/dist/security/tokenAuthentication.d.ts +0 -1
  117. package/dist/security/tokenAuthentication.js +6 -2
  118. package/dist/security/tokenAuthentication.js.map +1 -1
  119. package/dist/security/user.d.ts +6 -1
  120. package/dist/security/user.js +23 -1
  121. package/dist/security/user.js.map +1 -1
  122. package/dist/server/http.d.ts +23 -2
  123. package/dist/server/http.js +99 -12
  124. package/dist/server/http.js.map +1 -1
  125. package/dist/server/operationsServer.d.ts +0 -2
  126. package/dist/server/operationsServer.js.map +1 -1
  127. package/dist/server/serverHelpers/multipartParser.js +9 -0
  128. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  129. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  130. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  131. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  132. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  133. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  134. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  135. package/dist/server/serverHelpers/serverHandlers.js +19 -3
  136. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  137. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  138. package/dist/server/serverHelpers/serverUtilities.js +10 -7
  139. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  140. package/dist/server/static.js +57 -18
  141. package/dist/server/static.js.map +1 -1
  142. package/dist/server/storageReclamation.d.ts +17 -0
  143. package/dist/server/storageReclamation.js +90 -6
  144. package/dist/server/storageReclamation.js.map +1 -1
  145. package/dist/server/threads/socketRouter.js +20 -0
  146. package/dist/server/threads/socketRouter.js.map +1 -1
  147. package/dist/server/threads/threadServer.js +12 -0
  148. package/dist/server/threads/threadServer.js.map +1 -1
  149. package/dist/sqlEngine/diff/differential.js +7 -3
  150. package/dist/sqlEngine/diff/differential.js.map +1 -1
  151. package/dist/sqlTranslator/index.js +6 -1
  152. package/dist/sqlTranslator/index.js.map +1 -1
  153. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  154. package/dist/upgrade/upgradePrompt.js +22 -3
  155. package/dist/upgrade/upgradePrompt.js.map +1 -1
  156. package/dist/utility/errors/commonErrors.d.ts +1 -0
  157. package/dist/utility/errors/commonErrors.js +1 -0
  158. package/dist/utility/errors/commonErrors.js.map +1 -1
  159. package/dist/utility/hdbTerms.d.ts +2 -0
  160. package/dist/utility/hdbTerms.js +2 -0
  161. package/dist/utility/hdbTerms.js.map +1 -1
  162. package/dist/utility/install/installer.js +32 -0
  163. package/dist/utility/install/installer.js.map +1 -1
  164. package/dist/utility/logging/harper_logger.d.ts +7 -0
  165. package/dist/utility/logging/harper_logger.js +57 -24
  166. package/dist/utility/logging/harper_logger.js.map +1 -1
  167. package/npm-shrinkwrap.json +197 -193
  168. package/package.json +9 -7
  169. package/resources/DatabaseTransaction.ts +310 -101
  170. package/resources/ResourceInterface.ts +8 -2
  171. package/resources/Resources.ts +22 -4
  172. package/resources/Table.ts +321 -243
  173. package/resources/analytics/write.ts +22 -20
  174. package/resources/blob.ts +15 -8
  175. package/resources/databases.ts +123 -12
  176. package/resources/jsResource.ts +5 -62
  177. package/resources/models/Models.ts +14 -1
  178. package/resources/models/backendRegistry.ts +10 -0
  179. package/resources/models/openaiStream.ts +131 -19
  180. package/resources/models/v1/chatCompletions.ts +128 -0
  181. package/resources/models/v1/embeddings.ts +70 -0
  182. package/resources/models/v1/errors.ts +141 -0
  183. package/resources/models/v1/index.ts +72 -0
  184. package/resources/models/v1/models.ts +53 -0
  185. package/resources/models/v1/translation.ts +362 -0
  186. package/resources/roles.ts +67 -7
  187. package/security/jsLoader.ts +84 -30
  188. package/security/role.ts +7 -0
  189. package/security/superUserGuard.ts +20 -0
  190. package/security/tokenAuthentication.ts +6 -3
  191. package/security/user.ts +26 -1
  192. package/server/DESIGN.md +45 -34
  193. package/server/http.ts +100 -13
  194. package/server/operationsServer.ts +0 -2
  195. package/server/serverHelpers/multipartParser.ts +9 -0
  196. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  197. package/server/serverHelpers/registeredOperations.ts +19 -15
  198. package/server/serverHelpers/serverHandlers.js +20 -3
  199. package/server/serverHelpers/serverUtilities.ts +10 -7
  200. package/server/static.ts +75 -20
  201. package/server/storageReclamation.ts +104 -8
  202. package/server/threads/socketRouter.ts +20 -0
  203. package/server/threads/threadServer.js +11 -0
  204. package/sqlTranslator/index.ts +6 -1
  205. package/static/defaultConfig.yaml +2 -0
  206. package/studio/web/assets/{Chat-DoVWScmq.js → Chat-aApwhRmz.js} +2 -2
  207. package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-aApwhRmz.js.map} +1 -1
  208. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-sC0H91n9.js} +4 -4
  209. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
  210. package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-DZ8JhHOI.js} +2 -2
  211. package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
  212. package/studio/web/assets/{applications-D03NA7wW.js → applications-9SEVLIO5.js} +2 -2
  213. package/studio/web/assets/{applications-D03NA7wW.js.map → applications-9SEVLIO5.js.map} +1 -1
  214. package/studio/web/assets/{index-Bh_CNAHr.js → index-Dy3uDGXb.js} +6 -6
  215. package/studio/web/assets/{index-Bh_CNAHr.js.map → index-Dy3uDGXb.js.map} +1 -1
  216. package/studio/web/assets/{index.lazy-Dx3MpyDC.js → index.lazy-CN1zq4I4.js} +4 -4
  217. package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
  218. package/studio/web/assets/{notifications-0edoFTsb.js → notifications-BFAF07xr.js} +2 -2
  219. package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-BFAF07xr.js.map} +1 -1
  220. package/studio/web/assets/{notifications-CwKhipK7.js → notifications-CKlYVvVN.js} +2 -2
  221. package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-CKlYVvVN.js.map} +1 -1
  222. package/studio/web/assets/{profile-DUfEPQtx.js → profile-A1zhEdFG.js} +2 -2
  223. package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-A1zhEdFG.js.map} +1 -1
  224. package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-SEtBt_GV.js} +2 -2
  225. package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
  226. package/studio/web/assets/{setup-B56Oz1_u.js → setup-BisINqdH.js} +2 -2
  227. package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-BisINqdH.js.map} +1 -1
  228. package/studio/web/assets/{status-BAod7p3o.js → status-BuQoCc7l.js} +2 -2
  229. package/studio/web/assets/{status-BAod7p3o.js.map → status-BuQoCc7l.js.map} +1 -1
  230. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-CAi_s1PC.js} +2 -2
  231. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
  232. package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-DT74tlkM.js} +2 -2
  233. package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-DT74tlkM.js.map} +1 -1
  234. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-CGRGc1n7.js} +2 -2
  235. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
  236. package/studio/web/index.html +1 -1
  237. package/upgrade/upgradePrompt.ts +22 -3
  238. package/utility/errors/commonErrors.ts +2 -0
  239. package/utility/hdbTerms.ts +2 -0
  240. package/utility/install/installer.ts +37 -0
  241. package/utility/logging/harper_logger.ts +57 -26
@@ -0,0 +1,189 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync, readFileSync, readlinkSync } from 'node:fs';
3
+ import { lstat, readFile, readlink } from 'node:fs/promises';
4
+ import { createRequire, Module } from 'node:module';
5
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+
8
+ type Source = string | Buffer;
9
+
10
+ const MODULE_COMPARE_CONCURRENCY = 16;
11
+ const RESOLUTION_EXTENSIONS = ['', '.js', '.json', '.node', '.ts', '.tsx', '.cjs', '.mjs'];
12
+
13
+ type Resolution = {
14
+ resolvedUrl: string;
15
+ higherPriorityCandidates?: Map<string, string>;
16
+ };
17
+
18
+ export class RuntimeModuleTracker {
19
+ #getRoot: () => string | undefined;
20
+ #modules = new Map<string, string>();
21
+ #resolutions = new Map<string, Resolution>();
22
+ #nativeRuntime = false;
23
+ #deployInFlight = false;
24
+ #loadedDuringDeploy = false;
25
+ #comparison?: Promise<boolean>;
26
+
27
+ constructor(getRoot: () => string | undefined) {
28
+ this.#getRoot = getRoot;
29
+ }
30
+
31
+ beginDeploy(): void {
32
+ if (this.#deployInFlight) return;
33
+ this.#deployInFlight = true;
34
+ this.#loadedDuringDeploy = false;
35
+ this.#comparison = undefined;
36
+ }
37
+
38
+ markNativeRuntime(): void {
39
+ this.#nativeRuntime = true;
40
+ if (this.#deployInFlight) this.#loadedDuringDeploy = true;
41
+ }
42
+
43
+ recordModule(moduleUrl: string, source: Source): void {
44
+ const modulePath = this.#localPath(moduleUrl);
45
+ if (!modulePath) return;
46
+ if (!this.#modules.has(modulePath)) this.#modules.set(modulePath, digest(source));
47
+ if (this.#deployInFlight) this.#loadedDuringDeploy = true;
48
+ }
49
+
50
+ recordResolution(specifier: string, referrer: string, resolvedUrl: string): void {
51
+ const referrerPath = this.#localPath(referrer);
52
+ const resolvedPath = this.#localPath(resolvedUrl);
53
+ if (!referrerPath || !resolvedPath) return;
54
+ const key = `${referrerPath}\0${specifier}`;
55
+ if (!this.#resolutions.has(key)) {
56
+ const candidates = higherPriorityResolutionCandidates(specifier, referrerPath, resolvedPath);
57
+ this.#resolutions.set(key, {
58
+ resolvedUrl,
59
+ higherPriorityCandidates: candidates
60
+ ? new Map(candidates.map((candidate) => [candidate, candidateState(candidate)]))
61
+ : undefined,
62
+ });
63
+ if (this.#deployInFlight) this.#loadedDuringDeploy = true;
64
+ }
65
+ }
66
+
67
+ finishDeploy(): Promise<boolean> {
68
+ if (!this.#deployInFlight) return this.#comparison ?? Promise.resolve(false);
69
+ this.#deployInFlight = false;
70
+ this.#comparison = this.#compare();
71
+ return this.#comparison;
72
+ }
73
+
74
+ async #compare(): Promise<boolean> {
75
+ if (this.#nativeRuntime || this.#loadedDuringDeploy) return true;
76
+ const modules = [...this.#modules];
77
+ for (let start = 0; start < modules.length; start += MODULE_COMPARE_CONCURRENCY) {
78
+ const changed = await Promise.all(
79
+ modules.slice(start, start + MODULE_COMPARE_CONCURRENCY).map(async ([modulePath, previousDigest]) => {
80
+ try {
81
+ return digest(await readFile(modulePath)) !== previousDigest;
82
+ } catch {
83
+ return true;
84
+ }
85
+ })
86
+ );
87
+ if (changed.includes(true)) return true;
88
+ }
89
+ for (const [key, resolution] of this.#resolutions) {
90
+ const separator = key.indexOf('\0');
91
+ const referrerPath = key.slice(0, separator);
92
+ const specifier = key.slice(separator + 1);
93
+ if (
94
+ resolution.higherPriorityCandidates &&
95
+ (
96
+ await Promise.all(
97
+ [...resolution.higherPriorityCandidates].map(
98
+ async ([candidate, previousState]) => (await candidateStateAsync(candidate)) !== previousState
99
+ )
100
+ )
101
+ ).includes(true)
102
+ )
103
+ return true;
104
+ try {
105
+ // Node does not invalidate this cache when a component tree is replaced in place.
106
+ if (!invalidateResolutionCache(specifier, referrerPath, resolution.resolvedUrl)) return true;
107
+ const resolved = createRequire(pathToFileURL(referrerPath)).resolve(specifier);
108
+ const resolvedUrl = isAbsolute(resolved) ? pathToFileURL(resolved).toString() : resolved;
109
+ if (resolvedUrl !== resolution.resolvedUrl) return true;
110
+ } catch {
111
+ return true;
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+
117
+ #localPath(moduleUrl: string): string | undefined {
118
+ if (!moduleUrl.startsWith('file:')) return;
119
+ const root = this.#getRoot();
120
+ if (!root) return;
121
+ const modulePath = fileURLToPath(moduleUrl);
122
+ const relativePath = relative(resolve(root), modulePath);
123
+ if (
124
+ relativePath === '' ||
125
+ (!relativePath.startsWith(`..${sep}`) && relativePath !== '..' && !isAbsolute(relativePath))
126
+ )
127
+ return modulePath;
128
+ }
129
+ }
130
+
131
+ function higherPriorityResolutionCandidates(
132
+ specifier: string,
133
+ referrerPath: string,
134
+ resolvedPath: string
135
+ ): string[] | undefined {
136
+ if (!specifier.startsWith('.')) return;
137
+ const basePath = resolve(dirname(referrerPath), specifier);
138
+ const candidates = [...new Set(RESOLUTION_EXTENSIONS.map((extension) => basePath + extension))];
139
+ candidates.push(resolve(basePath, 'package.json'));
140
+ for (const extension of RESOLUTION_EXTENSIONS.slice(1)) candidates.push(resolve(basePath, `index${extension}`));
141
+ const resolvedIndex = candidates.indexOf(resolvedPath);
142
+ if (resolvedIndex === -1) return;
143
+ return candidates.slice(0, resolvedIndex);
144
+ }
145
+
146
+ function candidateState(path: string): string {
147
+ try {
148
+ const stats = lstatSync(path);
149
+ if (stats.isSymbolicLink()) return `link:${readlinkSync(path)}`;
150
+ if (stats.isDirectory()) return 'directory';
151
+ if (!stats.isFile()) return 'other';
152
+ return path.endsWith('package.json') ? `file:${digest(readFileSync(path))}` : 'file';
153
+ } catch (error) {
154
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';
155
+ return `error:${(error as NodeJS.ErrnoException).code ?? 'unknown'}`;
156
+ }
157
+ }
158
+
159
+ async function candidateStateAsync(path: string): Promise<string> {
160
+ try {
161
+ const stats = await lstat(path);
162
+ if (stats.isSymbolicLink()) return `link:${await readlink(path)}`;
163
+ if (stats.isDirectory()) return 'directory';
164
+ if (!stats.isFile()) return 'other';
165
+ return path.endsWith('package.json') ? `file:${digest(await readFile(path))}` : 'file';
166
+ } catch (error) {
167
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';
168
+ return `error:${(error as NodeJS.ErrnoException).code ?? 'unknown'}`;
169
+ }
170
+ }
171
+
172
+ function invalidateResolutionCache(specifier: string, referrerPath: string, previousResolvedUrl: string): boolean {
173
+ const pathCache = (Module as unknown as { _pathCache?: Record<string, string> })._pathCache;
174
+ if (!pathCache) return false;
175
+ const previousPath = previousResolvedUrl.startsWith('file:')
176
+ ? fileURLToPath(previousResolvedUrl)
177
+ : previousResolvedUrl;
178
+ const relativeKey = `${specifier}\0${dirname(referrerPath)}`;
179
+ if (pathCache[relativeKey] === previousPath) delete pathCache[relativeKey];
180
+ if (specifier.startsWith('.')) return true;
181
+ for (const cacheKey of Object.keys(pathCache)) {
182
+ if (cacheKey.startsWith(`${specifier}\0`) && pathCache[cacheKey] === previousPath) delete pathCache[cacheKey];
183
+ }
184
+ return true;
185
+ }
186
+
187
+ function digest(source: Source): string {
188
+ return createHash('sha256').update(source).digest('base64');
189
+ }
@@ -38,8 +38,10 @@ export type ScopeEventsMap = {
38
38
  // file-driven work to avoid acting on intermediate states.
39
39
  'deploy:start': [componentName: string];
40
40
  // Fired after deploy I/O completes (success or failure). The scope's
41
- // EntryHandlers have been recreated by this point; subsequent `add`/`change`
42
- // events reflect the post-deploy tree.
41
+ // EntryHandlers have been resumed by this point; their replacement watcher
42
+ // generation compares the post-deploy scan with the retained pre-deploy
43
+ // snapshot, so subsequent events are the logical differences of that tree,
44
+ // not a replay of every surviving file as an `add`.
43
45
  'deploy:end': [componentName: string];
44
46
  [record: string]: [...args: unknown[]];
45
47
  };
@@ -64,11 +66,8 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
64
66
  #pendingInitialLoads: Set<Promise<void>>;
65
67
  #deployStartHandler: (name: string) => void;
66
68
  #deployEndHandler: (name: string) => void;
67
- // While a deploy of this component is in flight, EntryHandler events do not
68
- // drive requestRestart() — the deploy itself produces hundreds of file
69
- // changes that would otherwise pile up. A single coalesced restart is
70
- // triggered by the post-deploy re-scan instead.
71
69
  #deployInFlight: boolean = false;
70
+ #restartRequestedDuringDeploy: boolean = false;
72
71
  applicationScope?: ApplicationScope;
73
72
 
74
73
  options: OptionsWatcher;
@@ -113,6 +112,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
113
112
  this.#directory = directory;
114
113
  this.#configFilePath = configFilePath;
115
114
  this.#logger = loggerWithTag(this.#appName);
115
+ this.#deployInFlight = deployLifecycle.isDeployInFlight(this.#appName);
116
116
 
117
117
  this.databaseEvents = databaseEventsEmitter;
118
118
  this.applicationScope = applicationScope;
@@ -156,6 +156,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
156
156
  this.options = new OptionsWatcher(pluginName, configFilePath, this.#logger, isRootConfig)
157
157
  .on('error', this.#handleError.bind(this))
158
158
  .on('change', this.#optionsWatcherChangeListener.bind(this)())
159
+ .on('remove', this.#optionsWatcherRemoveListener())
159
160
  .on('ready', this.#handleOptionsWatcherReady.bind(this));
160
161
 
161
162
  // Bridge cross-thread deploy lifecycle events for this component. The
@@ -254,7 +255,8 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
254
255
  }
255
256
 
256
257
  #handleError(error: unknown): void {
257
- this.emit('error', error);
258
+ if (this.listenerCount('error') > 0) this.emit('error', error);
259
+ else this.#logger.error?.('Error in component scope:', error);
258
260
  }
259
261
 
260
262
  async close(): Promise<this> {
@@ -301,6 +303,8 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
301
303
 
302
304
  #onDeployStart(componentName: string): void {
303
305
  this.#deployInFlight = true;
306
+ this.#restartRequestedDuringDeploy = false;
307
+ this.applicationScope?.beginDeploy();
304
308
  // Pause each EntryHandler so it stops emitting events for the
305
309
  // intermediate filesystem state the deploy is writing, and so it
306
310
  // releases its inotify handles while npm install is unpacking
@@ -315,25 +319,27 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
315
319
 
316
320
  #onDeployEnd(componentName: string): void {
317
321
  this.#deployInFlight = false;
322
+ const restartRequestedDuringDeploy = this.#restartRequestedDuringDeploy;
323
+ this.#restartRequestedDuringDeploy = false;
318
324
 
319
- // Resume each EntryHandler BEFORE notifying plugins. Otherwise a plugin
320
- // throwing in its deploy:end handler would abort this function and
321
- // leave the watchers permanently paused (surfaced by Gemini review).
322
- // The fresh chokidar watcher does an initial scan and emits add events
323
- // for the post-deploy tree; the existing per-event listener calls
324
- // scope.requestRestart() for each, and the restart debounce in
325
- // componentLoader collapses them into a single restart cycle. Plugin
326
- // handlers stay attached across the pause.
325
+ // Resume before notifying plugins so a throwing deploy:end listener cannot strand the watchers.
327
326
  for (const entryHandler of this.#entryHandlers) {
328
- void entryHandler.resume();
327
+ void entryHandler.resume().catch(() => {});
329
328
  }
329
+ if (restartRequestedDuringDeploy) this.requestRestart();
330
+ void this.applicationScope
331
+ ?.finishDeploy()
332
+ .then((runtimeChanged) => {
333
+ if (runtimeChanged) this.requestRestart();
334
+ })
335
+ .catch((error) => {
336
+ this.#logger.error?.(`Could not verify the loaded runtime after deploying ${this.#appName}:`, error);
337
+ this.requestRestart();
338
+ });
330
339
 
331
340
  this.#safeEmit('deploy:end', componentName);
332
341
  }
333
342
 
334
- // Swallow and log listener exceptions so one buggy plugin can't stop us
335
- // from running the rest of the deploy-lifecycle bookkeeping. EventEmitter
336
- // by default rethrows from synchronous listeners.
337
343
  #safeEmit(event: 'deploy:start' | 'deploy:end', componentName: string): void {
338
344
  try {
339
345
  this.emit(event, componentName);
@@ -350,6 +356,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
350
356
  .on('unlink', this.#defaultEntryHandlerListener('unlink'))
351
357
  .on('addDir', this.#defaultEntryHandlerListener('addDir'))
352
358
  .on('unlinkDir', this.#defaultEntryHandlerListener('unlinkDir'));
359
+ if (this.#deployInFlight) entryHandler.pause();
353
360
 
354
361
  this.#entryHandlers.push(entryHandler);
355
362
 
@@ -391,8 +398,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
391
398
  return;
392
399
  }
393
400
 
394
- // Otherwise, if an entry handler exists, update it with the new config
395
- scope.#entryHandler.update(config as FileAndURLPathConfig);
401
+ void scope.#entryHandler.update(config as FileAndURLPathConfig).catch(() => {});
396
402
 
397
403
  return;
398
404
  }
@@ -407,6 +413,23 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
407
413
  };
408
414
  }
409
415
 
416
+ #optionsWatcherRemoveListener() {
417
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
418
+ const scope = this;
419
+ // Deleting a component's config block emits `remove` (not `change`), so without
420
+ // this listener a running component keeps serving until some unrelated restart —
421
+ // even though absence is the canonical disabled state for opt-in built-ins like
422
+ // the /v1 models gateway. Mirrors the change listener: a plugin that registers
423
+ // its own `remove` handler owns the response and no restart is requested.
424
+ return function handleOptionsWatcherRemove(this: OptionsWatcher) {
425
+ if (this.listenerCount('remove') > 1) {
426
+ return;
427
+ }
428
+ scope.#logger.debug?.('Options removed, requesting restart');
429
+ scope.requestRestart();
430
+ };
431
+ }
432
+
410
433
  #getFilesOption(): FileAndURLPathConfig | undefined {
411
434
  const config = this.options.getAll();
412
435
  if (
@@ -520,9 +543,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
520
543
 
521
544
  requestRestart() {
522
545
  if (this.#deployInFlight) {
523
- // Suppressed: a deploy is rewriting this component's files. The
524
- // post-deploy re-scan in #onDeployEnd will trigger the coalesced
525
- // restart instead.
546
+ this.#restartRequestedDuringDeploy = true;
526
547
  this.#logger.debug?.(`Restart suppressed (deploy in flight) for ${this.#appName}`);
527
548
  return;
528
549
  }
@@ -541,6 +562,29 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
541
562
  }
542
563
  }
543
564
 
565
+ waitForDeployCompletion(timeoutMs = 6 * 60 * 60 * 1000): Promise<void> {
566
+ if (!this.#deployInFlight) return Promise.resolve();
567
+ return new Promise((resolve, reject) => {
568
+ const timer = setTimeout(() => {
569
+ deployLifecycle.off('deploy:end', handleDeployEnd);
570
+ reject(new Error(`Timed out waiting for deployment of ${this.#appName} to complete`));
571
+ }, timeoutMs);
572
+ timer.unref?.();
573
+ const handleDeployEnd = (componentName: string) => {
574
+ if (componentName !== this.#appName || this.#deployInFlight) return;
575
+ deployLifecycle.off('deploy:end', handleDeployEnd);
576
+ clearTimeout(timer);
577
+ resolve();
578
+ };
579
+ deployLifecycle.on('deploy:end', handleDeployEnd);
580
+ if (!this.#deployInFlight) {
581
+ deployLifecycle.off('deploy:end', handleDeployEnd);
582
+ clearTimeout(timer);
583
+ resolve();
584
+ }
585
+ });
586
+ }
587
+
544
588
  /**
545
589
  * Import a file into the scope's sandbox.
546
590
  * @param filePath - The path of the file to import.
@@ -29,6 +29,7 @@ import * as scheduler from '../resources/scheduler/scheduler.ts';
29
29
  import { restartWorkers, getWorkerIndex } from '../server/threads/manageThreads.js';
30
30
  import { resetRestartNeeded, subscribeToRestartRequests } from './requestRestart.ts';
31
31
  import { trackScopeClose } from './scopeShutdown.ts';
32
+ import { deployLifecycle } from './deployLifecycle.ts';
32
33
  import { toScopeMount, nestScopeMount, type ScopeMount } from './scopeMount.ts';
33
34
  import { scopedImport } from '../security/jsLoader.ts';
34
35
  import { server } from '../server/Server.ts';
@@ -170,6 +171,16 @@ export const TRUSTED_RESOURCE_PLUGINS: any = {
170
171
  return require('../server/fastifyRoutes');
171
172
  },
172
173
  login,
174
+ // String entry: the loader `await import()`s these lazily when the component is actually
175
+ // processed, so the gateway's module graph is not pulled into componentLoader's own
176
+ // evaluation. `#src/*` is used rather than a relative path because it resolves under both
177
+ // conditions (source under --conditions=typestrip, dist otherwise); a relative extensionless
178
+ // require would only resolve against dist.
179
+ //
180
+ // The component is only *processed* when a `modelsGateway` key exists in the config, since
181
+ // the root loop skips absent keys (`if (!componentConfig) continue`). `defaultConfig.yaml`
182
+ // ships no such key, so an instance that never opts in pays nothing for this entry.
183
+ modelsGateway: '#src/resources/models/v1/index',
173
184
  static: staticFiles,
174
185
  customFunctions: {},
175
186
  http: httpComponent,
@@ -312,6 +323,7 @@ function symlinkHarperModule(componentDirectory: string) {
312
323
  */
313
324
  function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
314
325
  return scope.ready.then(async () => {
326
+ await scope.waitForDeployCompletion();
315
327
  // Timeout priority is user config, plugin default, finally 30 seconds
316
328
  const timeout = scope.options.get(['timeout']) || plugin.defaultTimeout || 30_000; // default 30 second timeout
317
329
  if (typeof timeout !== 'number') {
@@ -333,35 +345,76 @@ function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
333
345
  }, timeout + 5_000); // extra time for lock acquisition
334
346
  });
335
347
  }
336
- let loadTimeout: NodeJS.Timeout;
337
348
  try {
338
349
  // note that handleApplication can throw sync or async errors, need to run finally block for both
339
- await Promise.race([
350
+ await withDeployAwareTimeout(
340
351
  Promise.resolve(plugin.handleApplication(scope)).then(async () => {
341
352
  // Wait for any initial entry handler loads to complete
342
353
  // This ensures all async operations (like secureImport) finish before the component is marked as loaded
343
354
  await scope.waitForInitialLoads();
344
355
  }),
345
- new Promise(
346
- (_, reject) =>
347
- (loadTimeout = setTimeout(
348
- () =>
349
- reject(
350
- new Error(
351
- `handleApplication timed out after ${timeout}ms for ${scope.pluginName} on behalf of ${scope.appName}`
352
- )
353
- ),
354
- timeout
355
- ))
356
- ),
357
- ]);
356
+ scope,
357
+ timeout
358
+ );
358
359
  } finally {
359
360
  Status.primaryStore.unlock(scope.pluginName);
360
- clearTimeout(loadTimeout);
361
361
  }
362
362
  });
363
363
  }
364
364
 
365
+ function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout: number): Promise<T> {
366
+ return new Promise((resolve, reject) => {
367
+ const absoluteTimeout = timeout + 6 * 60 * 60 * 1000;
368
+ let remaining = timeout;
369
+ let activeSince = 0;
370
+ let timer: NodeJS.Timeout | undefined;
371
+ let absoluteTimer: NodeJS.Timeout;
372
+ const cleanup = () => {
373
+ if (timer) clearTimeout(timer);
374
+ clearTimeout(absoluteTimer);
375
+ deployLifecycle.off('deploy:start', handleDeployStart);
376
+ deployLifecycle.off('deploy:end', handleDeployEnd);
377
+ };
378
+ const rejectTimeout = (limit = timeout) => {
379
+ cleanup();
380
+ reject(
381
+ new Error(`handleApplication timed out after ${limit}ms for ${scope.pluginName} on behalf of ${scope.appName}`)
382
+ );
383
+ };
384
+ absoluteTimer = setTimeout(() => rejectTimeout(absoluteTimeout), absoluteTimeout);
385
+ absoluteTimer.unref?.();
386
+ const arm = () => {
387
+ if (deployLifecycle.isDeployInFlight(scope.appName)) return;
388
+ if (remaining <= 0) return rejectTimeout();
389
+ activeSince = Date.now();
390
+ timer = setTimeout(rejectTimeout, remaining);
391
+ };
392
+ function handleDeployStart(componentName: string) {
393
+ if (componentName !== scope.appName || !timer) return;
394
+ remaining -= Date.now() - activeSince;
395
+ clearTimeout(timer);
396
+ timer = undefined;
397
+ }
398
+ function handleDeployEnd(componentName: string) {
399
+ if (componentName === scope.appName) arm();
400
+ }
401
+
402
+ deployLifecycle.on('deploy:start', handleDeployStart);
403
+ deployLifecycle.on('deploy:end', handleDeployEnd);
404
+ operation.then(
405
+ (value) => {
406
+ cleanup();
407
+ resolve(value);
408
+ },
409
+ (error) => {
410
+ cleanup();
411
+ reject(error);
412
+ }
413
+ );
414
+ arm();
415
+ });
416
+ }
417
+
365
418
  export interface LoadComponentOptions {
366
419
  isRoot?: boolean;
367
420
  applicationScope?: ApplicationScope;
@@ -405,6 +458,7 @@ export async function loadComponent(
405
458
  appName,
406
459
  mount,
407
460
  } = options;
461
+ applicationScope.runtimeRoot ??= resolvedFolder;
408
462
  applicationScope.allowedPath ??= realpathSync(componentDirectory);
409
463
  if (providedLoadedComponents) loadedComponents = providedLoadedComponents;
410
464
  try {