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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/bin/cliOperations.ts +12 -0
  2. package/bin/restart.ts +66 -6
  3. package/components/Application.ts +1134 -112
  4. package/components/OptionsWatcher.ts +368 -102
  5. package/components/Scope.ts +20 -1
  6. package/components/componentLoader.ts +43 -4
  7. package/components/deploymentOperations.ts +4 -1
  8. package/components/deploymentRecorder.ts +9 -2
  9. package/components/operations.js +284 -52
  10. package/components/operationsValidation.js +49 -2
  11. package/components/packageComponent.ts +25 -1
  12. package/components/requestRestart.ts +11 -0
  13. package/config/RootConfigWatcher.ts +191 -37
  14. package/config/configReadRetry.ts +62 -0
  15. package/config/configUtils.ts +78 -26
  16. package/config/parseConfigFile.ts +34 -0
  17. package/config/readConfigFileSync.ts +44 -0
  18. package/config/watcherArming.ts +59 -0
  19. package/config-root.schema.json +4 -0
  20. package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
  21. package/dist/bin/cliOperations.js +13 -0
  22. package/dist/bin/cliOperations.js.map +1 -1
  23. package/dist/bin/restart.js +42 -6
  24. package/dist/bin/restart.js.map +1 -1
  25. package/dist/components/Application.d.ts +104 -9
  26. package/dist/components/Application.js +954 -102
  27. package/dist/components/Application.js.map +1 -1
  28. package/dist/components/OptionsWatcher.d.ts +4 -1
  29. package/dist/components/OptionsWatcher.js +378 -104
  30. package/dist/components/OptionsWatcher.js.map +1 -1
  31. package/dist/components/Scope.js +15 -1
  32. package/dist/components/Scope.js.map +1 -1
  33. package/dist/components/componentLoader.js +35 -3
  34. package/dist/components/componentLoader.js.map +1 -1
  35. package/dist/components/deploymentOperations.js +4 -1
  36. package/dist/components/deploymentOperations.js.map +1 -1
  37. package/dist/components/deploymentRecorder.d.ts +4 -2
  38. package/dist/components/deploymentRecorder.js +1 -0
  39. package/dist/components/deploymentRecorder.js.map +1 -1
  40. package/dist/components/operations.d.ts +28 -0
  41. package/dist/components/operations.js +263 -54
  42. package/dist/components/operations.js.map +1 -1
  43. package/dist/components/operationsValidation.js +48 -2
  44. package/dist/components/operationsValidation.js.map +1 -1
  45. package/dist/components/packageComponent.js +24 -0
  46. package/dist/components/packageComponent.js.map +1 -1
  47. package/dist/components/requestRestart.d.ts +1 -0
  48. package/dist/components/requestRestart.js +7 -0
  49. package/dist/components/requestRestart.js.map +1 -1
  50. package/dist/config/RootConfigWatcher.d.ts +2 -0
  51. package/dist/config/RootConfigWatcher.js +189 -35
  52. package/dist/config/RootConfigWatcher.js.map +1 -1
  53. package/dist/config/configReadRetry.d.ts +8 -0
  54. package/dist/config/configReadRetry.js +62 -0
  55. package/dist/config/configReadRetry.js.map +1 -0
  56. package/dist/config/configUtils.d.ts +10 -9
  57. package/dist/config/configUtils.js +63 -27
  58. package/dist/config/configUtils.js.map +1 -1
  59. package/dist/config/parseConfigFile.d.ts +4 -0
  60. package/dist/config/parseConfigFile.js +35 -0
  61. package/dist/config/parseConfigFile.js.map +1 -0
  62. package/dist/config/readConfigFileSync.d.ts +1 -0
  63. package/dist/config/readConfigFileSync.js +47 -0
  64. package/dist/config/readConfigFileSync.js.map +1 -0
  65. package/dist/config/watcherArming.d.ts +15 -0
  66. package/dist/config/watcherArming.js +59 -0
  67. package/dist/config/watcherArming.js.map +1 -0
  68. package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
  69. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.js +4 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/json/systemSchema.json +3 -0
  74. package/dist/resources/DatabaseTransaction.d.ts +25 -0
  75. package/dist/resources/DatabaseTransaction.js +224 -18
  76. package/dist/resources/DatabaseTransaction.js.map +1 -1
  77. package/dist/resources/LMDBTransaction.d.ts +2 -1
  78. package/dist/resources/LMDBTransaction.js +22 -3
  79. package/dist/resources/LMDBTransaction.js.map +1 -1
  80. package/dist/resources/PrimaryRocksDatabase.js +22 -6
  81. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  82. package/dist/resources/RecordEncoder.d.ts +1 -1
  83. package/dist/resources/RecordEncoder.js +21 -5
  84. package/dist/resources/RecordEncoder.js.map +1 -1
  85. package/dist/resources/Resource.js +97 -13
  86. package/dist/resources/Resource.js.map +1 -1
  87. package/dist/resources/ResourceInterface.d.ts +8 -0
  88. package/dist/resources/RocksIndexStore.js +2 -1
  89. package/dist/resources/RocksIndexStore.js.map +1 -1
  90. package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
  91. package/dist/resources/RocksTransactionLogStore.js +104 -33
  92. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  93. package/dist/resources/Table.d.ts +58 -7
  94. package/dist/resources/Table.js +1095 -346
  95. package/dist/resources/Table.js.map +1 -1
  96. package/dist/resources/analytics/write.js +10 -3
  97. package/dist/resources/analytics/write.js.map +1 -1
  98. package/dist/resources/auditStore.d.ts +170 -0
  99. package/dist/resources/auditStore.js +457 -11
  100. package/dist/resources/auditStore.js.map +1 -1
  101. package/dist/resources/dataLoader.js +3 -4
  102. package/dist/resources/dataLoader.js.map +1 -1
  103. package/dist/resources/databases.d.ts +16 -13
  104. package/dist/resources/databases.js +624 -176
  105. package/dist/resources/databases.js.map +1 -1
  106. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  107. package/dist/resources/derivedIndexRegistry.js +68 -0
  108. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  109. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  110. package/dist/resources/derivedIndexRuntime.js +2027 -0
  111. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  112. package/dist/resources/graphql.js +3 -2
  113. package/dist/resources/graphql.js.map +1 -1
  114. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
  115. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
  116. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  117. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  118. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  119. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  120. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  121. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  122. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  123. package/dist/resources/nodeIdMapping.d.ts +5 -0
  124. package/dist/resources/nodeIdMapping.js +49 -0
  125. package/dist/resources/nodeIdMapping.js.map +1 -1
  126. package/dist/resources/recordLock.d.ts +47 -4
  127. package/dist/resources/recordLock.js +138 -7
  128. package/dist/resources/recordLock.js.map +1 -1
  129. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  130. package/dist/resources/recordLockCoordinator.js +2565 -0
  131. package/dist/resources/recordLockCoordinator.js.map +1 -0
  132. package/dist/resources/replayLogs.js +5 -0
  133. package/dist/resources/replayLogs.js.map +1 -1
  134. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  135. package/dist/resources/replicatedApplyFailure.js +63 -0
  136. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  137. package/dist/resources/scheduler/scheduler.js +3 -3
  138. package/dist/resources/scheduler/scheduler.js.map +1 -1
  139. package/dist/resources/search.d.ts +10 -4
  140. package/dist/resources/search.js +160 -40
  141. package/dist/resources/search.js.map +1 -1
  142. package/dist/resources/tracked.d.ts +5 -1
  143. package/dist/resources/tracked.js +74 -23
  144. package/dist/resources/tracked.js.map +1 -1
  145. package/dist/security/jsLoader.js +6 -4
  146. package/dist/security/jsLoader.js.map +1 -1
  147. package/dist/server/REST.js +33 -2
  148. package/dist/server/REST.js.map +1 -1
  149. package/dist/server/http.d.ts +5 -1
  150. package/dist/server/http.js +34 -2
  151. package/dist/server/http.js.map +1 -1
  152. package/dist/server/serverHelpers/Headers.d.ts +2 -0
  153. package/dist/server/serverHelpers/Headers.js +6 -0
  154. package/dist/server/serverHelpers/Headers.js.map +1 -1
  155. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  156. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  157. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  158. package/dist/server/serverHelpers/Request.d.ts +5 -10
  159. package/dist/server/serverHelpers/Request.js +38 -136
  160. package/dist/server/serverHelpers/Request.js.map +1 -1
  161. package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
  162. package/dist/server/serverHelpers/contentTypes.js +189 -15
  163. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  164. package/dist/server/serverHelpers/serverUtilities.js +96 -17
  165. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  166. package/dist/server/storageReclamation.js +1 -1
  167. package/dist/server/storageReclamation.js.map +1 -1
  168. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  169. package/dist/server/threads/isolatedApplications.js +171 -0
  170. package/dist/server/threads/isolatedApplications.js.map +1 -0
  171. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  172. package/dist/server/threads/logRotationTransport.js +33 -0
  173. package/dist/server/threads/logRotationTransport.js.map +1 -0
  174. package/dist/server/threads/manageThreads.d.ts +64 -6
  175. package/dist/server/threads/manageThreads.js +261 -12
  176. package/dist/server/threads/manageThreads.js.map +1 -1
  177. package/dist/server/threads/socketRouter.d.ts +1 -0
  178. package/dist/server/threads/socketRouter.js +196 -13
  179. package/dist/server/threads/socketRouter.js.map +1 -1
  180. package/dist/server/threads/threadServer.js +30 -7
  181. package/dist/server/threads/threadServer.js.map +1 -1
  182. package/dist/utility/errors/hdbError.d.ts +24 -0
  183. package/dist/utility/errors/hdbError.js +58 -1
  184. package/dist/utility/errors/hdbError.js.map +1 -1
  185. package/dist/utility/hdbTerms.d.ts +2 -0
  186. package/dist/utility/hdbTerms.js +2 -0
  187. package/dist/utility/hdbTerms.js.map +1 -1
  188. package/dist/utility/logging/harper_logger.js +217 -38
  189. package/dist/utility/logging/harper_logger.js.map +1 -1
  190. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  191. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  192. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  193. package/dist/utility/logging/logRotation.d.ts +46 -0
  194. package/dist/utility/logging/logRotation.js +365 -0
  195. package/dist/utility/logging/logRotation.js.map +1 -0
  196. package/dist/utility/logging/logRotator.d.ts +1 -1
  197. package/dist/utility/logging/logRotator.js +172 -92
  198. package/dist/utility/logging/logRotator.js.map +1 -1
  199. package/dist/utility/npmUtilities.js +6 -4
  200. package/dist/utility/npmUtilities.js.map +1 -1
  201. package/dist/utility/watcherFallback.d.ts +0 -45
  202. package/dist/utility/watcherFallback.js +1 -125
  203. package/dist/utility/watcherFallback.js.map +1 -1
  204. package/dist/validation/configValidator.js +6 -3
  205. package/dist/validation/configValidator.js.map +1 -1
  206. package/index.ts +6 -0
  207. package/json/systemSchema.json +3 -0
  208. package/npm-shrinkwrap.json +131 -41
  209. package/package.json +10 -3
  210. package/resources/DESIGN.md +124 -19
  211. package/resources/DatabaseTransaction.ts +230 -17
  212. package/resources/LMDBTransaction.ts +21 -3
  213. package/resources/PrimaryRocksDatabase.ts +20 -7
  214. package/resources/RecordEncoder.ts +25 -5
  215. package/resources/Resource.ts +97 -13
  216. package/resources/ResourceInterface.ts +8 -0
  217. package/resources/RocksIndexStore.ts +2 -1
  218. package/resources/RocksTransactionLogStore.ts +111 -31
  219. package/resources/Table.ts +1224 -393
  220. package/resources/analytics/write.ts +10 -3
  221. package/resources/auditStore.ts +460 -11
  222. package/resources/dataLoader.ts +3 -4
  223. package/resources/databases.ts +610 -146
  224. package/resources/derivedIndexRegistry.ts +56 -0
  225. package/resources/derivedIndexRuntime.ts +2292 -0
  226. package/resources/graphql.ts +3 -2
  227. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
  228. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  229. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  230. package/resources/nodeIdMapping.ts +50 -0
  231. package/resources/recordLock.ts +173 -7
  232. package/resources/recordLockCoordinator.ts +3043 -0
  233. package/resources/replayLogs.ts +5 -0
  234. package/resources/replicatedApplyFailure.ts +77 -0
  235. package/resources/scheduler/scheduler.ts +4 -4
  236. package/resources/search.ts +169 -49
  237. package/resources/tracked.ts +73 -22
  238. package/security/jsLoader.ts +6 -4
  239. package/server/DESIGN.md +11 -0
  240. package/server/REST.ts +36 -3
  241. package/server/http.ts +34 -2
  242. package/server/serverHelpers/Headers.ts +5 -1
  243. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  244. package/server/serverHelpers/Request.ts +33 -131
  245. package/server/serverHelpers/contentTypes.ts +188 -15
  246. package/server/serverHelpers/serverUtilities.ts +143 -24
  247. package/server/storageReclamation.ts +2 -2
  248. package/server/threads/isolatedApplications.ts +157 -0
  249. package/server/threads/logRotationTransport.ts +40 -0
  250. package/server/threads/manageThreads.js +254 -12
  251. package/server/threads/socketRouter.ts +217 -11
  252. package/server/threads/threadServer.js +30 -7
  253. package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
  254. package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
  255. package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
  256. package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
  257. package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
  258. package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
  259. package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
  260. package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
  261. package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
  262. package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
  263. package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
  264. package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
  265. package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
  266. package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
  267. package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
  268. package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
  269. package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
  270. package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
  271. package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
  272. package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
  273. package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
  274. package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
  275. package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
  276. package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
  277. package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
  278. package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
  279. package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
  280. package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
  281. package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
  282. package/studio/web/index.html +1 -1
  283. package/utility/errors/hdbError.ts +54 -0
  284. package/utility/hdbTerms.ts +2 -0
  285. package/utility/logging/harper_logger.ts +209 -30
  286. package/utility/logging/logGenerationCoordinator.ts +196 -0
  287. package/utility/logging/logRotation.ts +367 -0
  288. package/utility/logging/logRotator.ts +196 -91
  289. package/utility/npmUtilities.ts +6 -4
  290. package/utility/watcherFallback.ts +0 -122
  291. package/validation/configValidator.ts +6 -3
@@ -10,6 +10,10 @@ import * as envMgr from '../../utility/environment/environmentManager.ts';
10
10
  import { CONFIG_PARAMS } from '../../utility/hdbTerms.ts';
11
11
  import * as YAML from 'yaml';
12
12
  import { logger } from '../../utility/logging/logger.ts';
13
+ import {
14
+ errorToString as harperErrorToString,
15
+ errorForLog as harperErrorForLog,
16
+ } from '../../utility/logging/harper_logger.ts';
13
17
  import { Blob } from '../../resources/blob.ts';
14
18
  // TODO: Only load this if fastify is loaded
15
19
  import fp from 'fastify-plugin';
@@ -17,6 +21,36 @@ import { parseMultipartRequest } from './multipartParser.ts';
17
21
  const SERIALIZATION_BIGINT = envMgr.get(CONFIG_PARAMS.SERIALIZATION_BIGINT) !== false;
18
22
  const JSONStringify = SERIALIZATION_BIGINT ? stringify : JSON.stringify;
19
23
  const JSONParse = SERIALIZATION_BIGINT ? parse : JSON.parse;
24
+ const streamStartup = Symbol('streamStartup');
25
+ const serializedStreamError = Symbol('serializedStreamError');
26
+
27
+ type StreamStartup = {
28
+ result: Promise<{ failed?: true; error?: unknown }>;
29
+ abort: () => void;
30
+ start: () => void;
31
+ cancel?: () => void;
32
+ };
33
+
34
+ function streamErrorRecord(error: any) {
35
+ let name = 'Error';
36
+ let message;
37
+ try {
38
+ const code = error?.code;
39
+ if (typeof code === 'string' || typeof code === 'number') name = String(code);
40
+ else if (typeof error?.constructor?.name === 'string') name = error.constructor.name;
41
+ else if (typeof error?.name === 'string') name = error.name;
42
+ } catch {}
43
+ try {
44
+ if (typeof error?.message === 'string') message = error.message;
45
+ } catch {}
46
+ message ??= harperErrorToString(error);
47
+ const record: { error: string; message: string; status?: number } = { error: name, message };
48
+ try {
49
+ const status = error?.statusCode ?? error?.status;
50
+ if (typeof status === 'number') record.status = status;
51
+ } catch {}
52
+ return record;
53
+ }
20
54
 
21
55
  const PUBLIC_ENCODE_OPTIONS = {
22
56
  useRecords: false,
@@ -103,9 +137,14 @@ mediaTypes.set('text/yaml', {
103
137
  });
104
138
 
105
139
  const ndjsonHandler = {
106
- serializeStream(data: any) {
140
+ serializeStream(data: any, _response?: Response, request?: any) {
107
141
  if (data?.[Symbol.iterator] || data?.[Symbol.asyncIterator]) {
108
- return Readable.from(transformIterable(data, (msg: any) => JSONStringify(msg) + '\n'));
142
+ return errorFrameStream(
143
+ data,
144
+ (msg: any) => JSONStringify(msg) + '\n',
145
+ (error) => JSONStringify({ $harperStreamError: streamErrorRecord(error) }) + '\n',
146
+ request?.method === 'GET'
147
+ );
109
148
  }
110
149
  return JSONStringify(data) + '\n';
111
150
  },
@@ -132,9 +171,14 @@ function serializeSSEData(data: any) {
132
171
 
133
172
  mediaTypes.set('text/event-stream', {
134
173
  // Server-Sent Events (SSE)
135
- serializeStream: function (iterable) {
174
+ serializeStream: function (iterable, _response?: Response, request?: any) {
136
175
  // create a readable stream that we use to stream out events from our subscription
137
- return Readable.from(transformIterable(iterable, this.serialize));
176
+ return errorFrameStream(
177
+ iterable,
178
+ this.serialize,
179
+ (error) => this.serialize({ event: 'harper-error', data: streamErrorRecord(error) }),
180
+ request?.method === 'GET'
181
+ );
138
182
  },
139
183
  serialize: function (message) {
140
184
  if (message.acknowledge) message.acknowledge();
@@ -396,7 +440,12 @@ export function serialize(responseData, request, responseObject) {
396
440
  (responseData[Symbol.iterator] || responseData[Symbol.asyncIterator]) &&
397
441
  serializer.serializer.serializeStream
398
442
  ) {
399
- if (responseData.mapError) {
443
+ if (
444
+ responseData.mapError &&
445
+ serializer.type !== 'application/x-ndjson' &&
446
+ serializer.type !== 'application/ndjson' &&
447
+ serializer.type !== 'text/event-stream'
448
+ ) {
400
449
  // indicate that we want iterator errors to be returned so we can serialize them in a meaningful way, if possible
401
450
  const getColumns = responseData.getColumns;
402
451
  responseData = responseData.mapError((error) => {
@@ -407,9 +456,10 @@ export function serialize(responseData, request, responseObject) {
407
456
  });
408
457
  responseData.getColumns = getColumns;
409
458
  }
410
- let stream = serializer.serializer.serializeStream(responseData, responseObject);
459
+ let stream = serializer.serializer.serializeStream(responseData, responseObject, request);
411
460
  if (canCompress) {
412
461
  responseObject.headers.set('Content-Encoding', 'br');
462
+ const uncompressedStream = stream;
413
463
  stream = stream.pipe(
414
464
  createBrotliCompress({
415
465
  params: {
@@ -421,6 +471,7 @@ export function serialize(responseData, request, responseObject) {
421
471
  },
422
472
  })
423
473
  );
474
+ if (uncompressedStream[streamStartup]) stream[streamStartup] = uncompressedStream[streamStartup];
424
475
  }
425
476
  return stream;
426
477
  }
@@ -623,24 +674,146 @@ function deserializerUnknownType(contentType: ContentType): Deserialize {
623
674
  }
624
675
  }
625
676
 
626
- function transformIterable(iterable, transform) {
677
+ function errorFrameStream(iterable, transform, serializeError, eager: boolean) {
678
+ const transformed = transformIterable(iterable, transform, serializeError, eager);
679
+ const readable = Readable.from(transformed);
680
+ const startup: StreamStartup = transformed[streamStartup];
681
+ startup.cancel = () => readable.destroy();
682
+ readable[streamStartup] = startup;
683
+ return readable;
684
+ }
685
+
686
+ export function discardSerializedStream(stream) {
687
+ stream?.[streamStartup]?.cancel?.();
688
+ stream?.destroy?.();
689
+ }
690
+
691
+ export function waitForStreamStartup(stream) {
692
+ const startup: StreamStartup | undefined = stream?.[streamStartup];
693
+ if (!startup) return;
694
+ startup.start();
695
+ return startup.result.then((result) => {
696
+ if (result.failed) {
697
+ startup.abort();
698
+ discardSerializedStream(stream);
699
+ throw result.error;
700
+ }
701
+ });
702
+ }
703
+
704
+ function transformIterable(iterable, transform, serializeError, eager) {
705
+ let iterator;
706
+ let terminal = false;
707
+ let first = true;
708
+ let started = false;
709
+ let firstStep;
710
+ let startupSettled = false;
711
+ const startupResult = Promise.withResolvers<{ failed?: true; error?: unknown }>();
712
+ const errorDecision = Promise.withResolvers<boolean>();
713
+ let startupTimer;
714
+
715
+ const commit = () => {
716
+ if (startupTimer) clearImmediate(startupTimer);
717
+ if (!startupSettled) {
718
+ startupSettled = true;
719
+ startupResult.resolve({});
720
+ }
721
+ errorDecision.resolve(true);
722
+ };
723
+ const abort = () => {
724
+ if (startupTimer) clearImmediate(startupTimer);
725
+ errorDecision.resolve(false);
726
+ };
727
+ const handleError = async (error) => {
728
+ terminal = true;
729
+ try {
730
+ const returned = iterator.return?.();
731
+ returned?.catch?.(() => {});
732
+ } catch {}
733
+ if (!startupSettled) startupResult.resolve({ failed: true, error });
734
+ if (!startupSettled && !(await errorDecision.promise)) return { done: true };
735
+ logger.warn?.('Error serializing in stream', harperErrorForLog(error));
736
+ return { value: serializeError(error), done: false, [serializedStreamError]: true };
737
+ };
738
+ const transformStep = (step) => {
739
+ if (step?.[serializedStreamError]) return step;
740
+ if (terminal) return { done: true };
741
+ if (step.done) return step;
742
+ try {
743
+ return { value: transform(step.value), done: false };
744
+ } catch (error) {
745
+ return handleError(error);
746
+ }
747
+ };
748
+ const getRawNext = () => {
749
+ if (terminal) return { done: true };
750
+ let step;
751
+ try {
752
+ step = iterator.next();
753
+ } catch (error) {
754
+ return handleError(error);
755
+ }
756
+ if (step.then) return step.then(undefined, handleError);
757
+ return step;
758
+ };
759
+ const getNext = () => {
760
+ if (terminal) return { done: true };
761
+ let step;
762
+ try {
763
+ step = iterator.next();
764
+ } catch (error) {
765
+ return handleError(error);
766
+ }
767
+ if (step.then) return step.then(transformStep, handleError);
768
+ return transformStep(step);
769
+ };
770
+ const start = () => {
771
+ if (started) return;
772
+ started = true;
773
+ iterator = iterable[Symbol.asyncIterator] ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator]();
774
+ firstStep = getRawNext();
775
+ Promise.resolve(firstStep).then(
776
+ () => {
777
+ if (!terminal) commit();
778
+ },
779
+ () => {}
780
+ );
781
+ // After this turn, the HTTP status can no longer wait for a first item without delaying long-lived streams.
782
+ startupTimer = setImmediate(commit);
783
+ };
784
+ if (eager) start();
785
+
627
786
  return {
787
+ [streamStartup]: {
788
+ result: startupResult.promise,
789
+ abort,
790
+ start,
791
+ },
628
792
  [Symbol.asyncIterator]() {
629
- const iterator = iterable[Symbol.asyncIterator] ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator]();
630
793
  return {
631
794
  next() {
632
- const step = iterator.next();
633
- if (step.then) {
634
- // don't transform the terminal sentinel step, whose value is undefined
635
- return step.then((result) => (result.done ? result : { value: transform(result.value), done: false }));
795
+ if (terminal) return { done: true };
796
+ if (first) {
797
+ first = false;
798
+ start();
799
+ const step = firstStep;
800
+ firstStep = undefined;
801
+ if (step.then) return step.then(transformStep);
802
+ return transformStep(step);
636
803
  }
637
- return step.done ? step : { value: transform(step.value), done: false };
804
+ return getNext();
638
805
  },
639
806
  return(value) {
640
- return iterator.return(value);
807
+ terminal = true;
808
+ started = true;
809
+ if (!iterator)
810
+ iterator = iterable[Symbol.asyncIterator]
811
+ ? iterable[Symbol.asyncIterator]()
812
+ : iterable[Symbol.iterator]?.();
813
+ return iterator?.return?.(value) ?? { value, done: true };
641
814
  },
642
815
  throw(error) {
643
- return iterator.throw(error);
816
+ return iterator?.throw?.(error) ?? Promise.reject(error);
644
817
  },
645
818
  };
646
819
  },
@@ -235,6 +235,17 @@ server.setMcpQuotaHandler = (handler) => {
235
235
  setMcpQuotaHandler(handler);
236
236
  };
237
237
 
238
+ // The nested `search_operation` operations dataLayer/export.ts's getRecords can actually run (its
239
+ // VALID_SEARCH_OPERATIONS). Validated at dispatch so an unsupported or missing nested operation is a
240
+ // request-time 400 rather than an async job failure, and so the nested authorization below always
241
+ // resolves a real handler.
242
+ const EXPORT_SEARCH_OPERATIONS = new Set<string>([
243
+ terms.OPERATIONS_ENUM.SEARCH_BY_VALUE,
244
+ terms.OPERATIONS_ENUM.SEARCH_BY_HASH,
245
+ terms.OPERATIONS_ENUM.SEARCH_BY_CONDITIONS,
246
+ terms.OPERATIONS_ENUM.SQL,
247
+ ]);
248
+
238
249
  export function chooseOperation(json: OperationRequestBody, bypassAuth = false) {
239
250
  let getOpResult: OperationFunctionObject;
240
251
  try {
@@ -255,32 +266,112 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
255
266
 
256
267
  const { operation_function, job_operation_function } = getOpResult;
257
268
 
258
- // Here there is a SQL statement in either the operation or the searchOperation (from jobs like export_local). Need to check the perms
259
- // on all affected tables/attributes.
269
+ const isSqlOperation = json.operation === terms.OPERATIONS_ENUM.SQL;
270
+ // `verifyPerms` derives the tables it checks from the object it is handed, so the caller-supplied
271
+ // `search_operation` may only stand in as the permission subject for the operations that actually
272
+ // run it (dataLayer/export.ts is its only consumer). For anything else it would check the nested
273
+ // tables while the handler runs against the top-level ones.
274
+ const nestedSearch =
275
+ json.operation === terms.OPERATIONS_ENUM.EXPORT_LOCAL || json.operation === terms.OPERATIONS_ENUM.EXPORT_TO_S3
276
+ ? json.search_operation
277
+ : undefined;
278
+ if (nestedSearch !== undefined) {
279
+ // It becomes the permission subject below, and a primitive would throw on the principal
280
+ // assignment — a 400 rather than a wrapped 500.
281
+ if (nestedSearch === null || typeof nestedSearch !== 'object' || Array.isArray(nestedSearch)) {
282
+ throw handleHDBError(
283
+ new Error(),
284
+ `'search_operation' must be an object`,
285
+ hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
286
+ undefined,
287
+ undefined,
288
+ true
289
+ );
290
+ }
291
+ // The object check alone lets `{}` and unsupported operations through — they would then fail
292
+ // asynchronously in the export worker (or dereference `search_operation.operation`). Require a
293
+ // supported operation at request time; the nested authorization below relies on it resolving.
294
+ if (typeof nestedSearch.operation !== 'string' || !EXPORT_SEARCH_OPERATIONS.has(nestedSearch.operation)) {
295
+ throw handleHDBError(
296
+ new Error(),
297
+ `'search_operation.operation' must be one of: ${[...EXPORT_SEARCH_OPERATIONS].join(', ')}`,
298
+ hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
299
+ undefined,
300
+ undefined,
301
+ true
302
+ );
303
+ }
304
+ // Dispatch state, never client input: the export worker re-reads `parsed_sql_object` off this
305
+ // same object (evaluateSQL), and it carries `permissions_checked`, so a body-supplied one would
306
+ // execute an AST no check ever saw. Deleting it rather than replacing it with the authorized
307
+ // parse is deliberate — the worker then re-parses and re-runs the check itself.
308
+ delete nestedSearch.parsed_sql_object;
309
+ }
310
+ // The AST check below covers only the statement's tables, never the caller's right to invoke the
311
+ // job operation, so it is additive to verifyPerms rather than an alternative to it.
312
+ const hasNestedSqlSearch = nestedSearch?.operation === terms.OPERATIONS_ENUM.SQL;
313
+
260
314
  try {
261
- if (json.operation === 'sql' || (json.search_operation && json.search_operation.operation === 'sql')) {
315
+ if (isSqlOperation || hasNestedSqlSearch) {
262
316
  const sql = require('../../sqlTranslator/index');
263
- const sqlStatement = json.operation === 'sql' ? json.sql : json.search_operation.sql;
317
+ const sqlStatement = isSqlOperation ? json.sql : nestedSearch.sql;
264
318
  // Before this dispatch's own parse is assigned, so a body-supplied object cannot survive it.
265
319
  stripSuppliedParsedSqlObject(json);
266
320
  const parsedSqlObject = sql.convertSQLToAST(sqlStatement);
267
- json.parsed_sql_object = parsedSqlObject;
321
+ if (hasNestedSqlSearch && parsedSqlObject.variant !== terms.VALID_SQL_OPS_ENUM.SELECT) {
322
+ throw handleHDBError(
323
+ new Error(),
324
+ `'search_operation.sql' must be a SELECT statement`,
325
+ hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
326
+ undefined,
327
+ undefined,
328
+ true
329
+ );
330
+ }
331
+ // Only the direct-SQL path consumes `json.parsed_sql_object` (evaluateSQL). A nested export
332
+ // re-parses off `search_operation` — whose `parsed_sql_object` was deleted above — so setting
333
+ // it here for a job would be inert.
334
+ if (isSqlOperation) {
335
+ json.parsed_sql_object = parsedSqlObject;
336
+ }
268
337
  if (!bypassAuth) {
269
- // The SQL path never reaches verifyPerms, so the role `operations` allowlist must be
270
- // enforced here — otherwise an allowlisted role reaches unlisted operations via `sql`.
271
- // json.operation is already the API name ('sql', or the outer job op like 'export_local').
272
- const allowlistDenial = opAuth.verifyOperationsAllowlist(json, json.operation);
273
- if (allowlistDenial) {
274
- operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
275
- operationLog.warn(`User '${json.hdb_user?.username}' is not permitted to ${json.operation}`);
276
- throw handleHDBError(
277
- new Error(),
278
- allowlistDenial,
279
- hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
280
- undefined,
281
- undefined,
282
- true
283
- );
338
+ if (hasNestedSqlSearch) {
339
+ // A nested-SQL export's outer op (export_local/export_to_s3) is `requires_su`, which the
340
+ // AST table check below does not enforce. Authorize invoking it through verifyPerms
341
+ // exactly as the non-SQL job path does — otherwise SQL is a way around the requires_su
342
+ // gate that path enforces. The nested table reads are then covered additively by the AST
343
+ // check. verifyPerms also runs the allowlist (gate 1) and the export token scope.
344
+ const functionToCheck = job_operation_function === undefined ? operation_function : job_operation_function;
345
+ const outerDenial = opAuth.verifyPerms(json, functionToCheck, { apiOperation: json.operation });
346
+ if (outerDenial) {
347
+ operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
348
+ operationLog.warn(`User '${json.hdb_user?.username}' is not permitted to ${json.operation}`);
349
+ throw handleHDBError(
350
+ new Error(),
351
+ outerDenial,
352
+ hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
353
+ undefined,
354
+ false,
355
+ true
356
+ );
357
+ }
358
+ } else {
359
+ // Direct SQL: the invoked operation IS `sql`, so the role `operations` allowlist is the
360
+ // operation-invocation check — otherwise an allowlisted role reaches unlisted operations
361
+ // via `sql`. json.operation is already the API name.
362
+ const allowlistDenial = opAuth.verifyOperationsAllowlist(json, json.operation);
363
+ if (allowlistDenial) {
364
+ operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
365
+ operationLog.warn(`User '${json.hdb_user?.username}' is not permitted to ${json.operation}`);
366
+ throw handleHDBError(
367
+ new Error(),
368
+ allowlistDenial,
369
+ hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
370
+ undefined,
371
+ undefined,
372
+ true
373
+ );
374
+ }
284
375
  }
285
376
  // `json.operation` explicitly — the operation this dispatch already resolved, not a
286
377
  // field read back off the request body.
@@ -309,10 +400,9 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
309
400
  json.operation !== terms.OPERATIONS_ENUM.EXCHANGE_OIDC_TOKEN
310
401
  ) {
311
402
  const functionToCheck = job_operation_function === undefined ? operation_function : job_operation_function;
312
- const operation_json = json.search_operation ? json.search_operation : json;
313
- if (!operation_json.hdb_user) {
314
- operation_json.hdb_user = json.hdb_user;
315
- }
403
+ const operation_json = nestedSearch ?? json;
404
+ // Authentication is the only source of the principal; a nested one is overwritten, not honored.
405
+ operation_json.hdb_user = json.hdb_user;
316
406
 
317
407
  // Pass the top-level operation for the token-scope check: for an export job, operation_json
318
408
  // is the nested search_operation, so json.operation (export_local/export_to_s3) is the op the
@@ -335,6 +425,35 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
335
425
  true
336
426
  );
337
427
  }
428
+
429
+ // The check above authorizes invoking the operation, but for a non-SQL export it returns
430
+ // before any table check — a requires_su export is granted the moment its `operations`
431
+ // allowlist lists it (verifyPerms gate 2) — and the job worker then runs `search_operation`
432
+ // through searchByValue/Hash/Conditions, none of which check permissions. So the nested read
433
+ // is only enforceable here: authorize it additively against its real search handler and the
434
+ // authenticated principal (assigned above via operation_json), so a role that may invoke the
435
+ // export but lacks read on the table is denied. A nested SQL search is covered by the AST
436
+ // branch above instead.
437
+ if (nestedSearch) {
438
+ const nestedFunction = getOperationFunction(nestedSearch).operation_function;
439
+ const nestedPermsResult = opAuth.verifyPerms(nestedSearch, nestedFunction, {
440
+ apiOperation: json.operation,
441
+ });
442
+ if (nestedPermsResult) {
443
+ operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
444
+ operationLog.warn(
445
+ `User '${operation_json.hdb_user?.username}' is not permitted to ${nestedSearch.operation} within ${json.operation}`
446
+ );
447
+ throw handleHDBError(
448
+ new Error(),
449
+ nestedPermsResult,
450
+ hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
451
+ undefined,
452
+ false,
453
+ true
454
+ );
455
+ }
456
+ }
338
457
  }
339
458
  } catch (err) {
340
459
  throw handleHDBError(err, `There was an error when trying to choose an operation path`, 500);
@@ -1,7 +1,7 @@
1
1
  import { readFile, realpath } from 'node:fs/promises';
2
2
  import { statfs } from 'node:fs/promises';
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
- import { getWorkerIndex, getWorkerCount } from '../server/threads/manageThreads.js';
4
+ import { ownsStoreMaintenance } from '../server/threads/manageThreads.js';
5
5
  import { logger } from '../utility/logging/logger.ts';
6
6
  import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
7
7
  import * as envMgr from '../utility/environment/environmentManager.ts';
@@ -106,7 +106,7 @@ export function onStorageReclamation(
106
106
  handler: (priority: number) => Promise<void> | void,
107
107
  skipThreadCheck?: boolean
108
108
  ) {
109
- if (skipThreadCheck || getWorkerIndex() === getWorkerCount() - 1) {
109
+ if (skipThreadCheck || ownsStoreMaintenance(path)) {
110
110
  // only run on one thread (last one)
111
111
  if (!path) {
112
112
  throw new Error('Storage reclamation path cannot be empty');
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Isolated applications: an application whose root-config entry carries `isolated: true` runs in a
3
+ * worker thread of its own that loads no other application (harper#642, tier 2).
4
+ *
5
+ * The invariant every helper here serves: an isolated application is loaded by exactly one thread,
6
+ * and that thread loads nothing else. The dedicated worker is an ordinary `http` worker by type --
7
+ * so restarts, overlap rules and shutdown treat it as one -- distinguished only by
8
+ * `workerData.isolatedApplication`. It binds none of the public ports (the kernel would hand it
9
+ * connections for every other application) and is reachable only through its own UDS mirror, whose
10
+ * metadata names the application and its hosts for the fronting proxy to route by.
11
+ */
12
+ import { isMainThread, workerData } from 'node:worker_threads';
13
+ import { getWorkerIndex } from './manageThreads.js';
14
+ import { getConfigObj, getConfigPath } from '../../config/configUtils.ts';
15
+ import * as env from '../../utility/environment/environmentManager.ts';
16
+ import { CONFIG_PARAMS } from '../../utility/hdbTerms.ts';
17
+ import { isDomainSocketPathTooLong } from '../../utility/domainSocket.ts';
18
+ import { existsSync } from 'node:fs';
19
+ import { basename, join } from 'node:path';
20
+
21
+ export const DEFAULT_MAX_ISOLATED_APPLICATIONS = 8;
22
+
23
+ /** The application this thread is dedicated to, if it is a dedicated worker. */
24
+ export function thisThreadsIsolatedApplication(): string | undefined {
25
+ return (workerData as any)?.isolatedApplication;
26
+ }
27
+
28
+ export function thisThreadOwnsApplication(
29
+ appName: string | undefined,
30
+ owner: string | undefined = thisThreadsIsolatedApplication()
31
+ ): boolean {
32
+ return owner !== undefined && appName === owner;
33
+ }
34
+
35
+ export function isIsolatedApplication(
36
+ appName: string,
37
+ config: Record<string, any> | undefined = getConfigObj()
38
+ ): boolean {
39
+ const entry = config?.[appName];
40
+ return typeof entry === 'object' && entry !== null && entry.isolated === true;
41
+ }
42
+
43
+ /** Every application the root config marks isolated, in config order. */
44
+ export function isolatedApplicationNames(config: Record<string, any> | undefined = getConfigObj()): string[] {
45
+ if (!config) return [];
46
+ return Object.keys(config).filter((name) => isIsolatedApplication(name, config));
47
+ }
48
+
49
+ export function presentIsolatedApplicationNames(
50
+ config: Record<string, any> | undefined = getConfigObj(),
51
+ componentsRoot: string = getConfigPath(CONFIG_PARAMS.COMPONENTSROOT) as string,
52
+ installedRoot: string = join(env.get(CONFIG_PARAMS.ROOTPATH), 'components'),
53
+ runApplicationPath: string | undefined = process.env.RUN_HDB_APP
54
+ ): string[] {
55
+ return isolatedApplicationNames(config).filter(
56
+ (application) =>
57
+ existsSync(join(componentsRoot, application)) ||
58
+ existsSync(join(installedRoot, application)) ||
59
+ (runApplicationPath && basename(runApplicationPath) === application)
60
+ );
61
+ }
62
+
63
+ export function maxIsolatedApplications(): number {
64
+ const configured = Number(env.get(CONFIG_PARAMS.THREADS_MAXISOLATED));
65
+ return Number.isInteger(configured) && configured >= 0 ? configured : DEFAULT_MAX_ISOLATED_APPLICATIONS;
66
+ }
67
+
68
+ /** Why `appName` cannot claim a place in the dedicated-worker budget, if the budget is full. */
69
+ export function isolatedApplicationCapacityRefusal(
70
+ appName: string,
71
+ runningApplications: Iterable<string>,
72
+ max = maxIsolatedApplications()
73
+ ): string | undefined {
74
+ const running = new Set(runningApplications);
75
+ if (running.has(appName) || running.size < max) return undefined;
76
+ return `the instance already runs ${max} isolated application(s) (threads.maxIsolated)`;
77
+ }
78
+
79
+ /**
80
+ * Whether the calling thread is the one that loads the application `appName` (an application: a
81
+ * directory under componentsRoot, or a root-config entry with `package`). A dedicated worker loads
82
+ * only its own application; every other thread -- pool workers and the main thread alike -- loads only
83
+ * the applications that are not isolated. Decided before any of the application's modules are
84
+ * imported, so a skipped application has no side effects on the thread that skipped it.
85
+ */
86
+ export function shouldLoadApplicationHere(
87
+ appName: string,
88
+ owner: string | undefined = thisThreadsIsolatedApplication(),
89
+ config: Record<string, any> | undefined = getConfigObj()
90
+ ): boolean {
91
+ if (owner !== undefined) return appName === owner;
92
+ return !isIsolatedApplication(appName, config);
93
+ }
94
+
95
+ /**
96
+ * Whether this thread starts the uWS listener for one `uwsServeConfigs` entry (see listenOnPorts()).
97
+ * A dedicated worker starts only its own per-application UDS mirrors -- the entries that carry a
98
+ * `socketPath` -- and never a global port listener, which the kernel would hand connections for every
99
+ * other application.
100
+ */
101
+ export function shouldStartUwsListenerHere(
102
+ serveConfig: { socketPath?: string },
103
+ owner: string | undefined = thisThreadsIsolatedApplication()
104
+ ): boolean {
105
+ return owner === undefined || Boolean(serveConfig.socketPath);
106
+ }
107
+
108
+ /**
109
+ * Whether a dedicated worker can be reached at all: it binds only UDS mirrors of the secure port, so
110
+ * without `tls.unixDomainSockets` and a secure port an isolated application would load and answer
111
+ * nothing, silently. Refused at admission instead. Returns the reason, or undefined when reachable.
112
+ */
113
+ export function isolatedApplicationsUnreachableReason(platform = process.platform): string | undefined {
114
+ if (platform === 'win32') return 'Windows does not support per-application UDS mirrors';
115
+ // the main thread standing in as the only worker (threads.count: 0) has no thread to give
116
+ if (isMainThread && getWorkerIndex() === 0) return 'threads.count is 0, so there is no worker thread to dedicate';
117
+ if (!env.get(CONFIG_PARAMS.HTTP_SECUREPORT)) return 'no http.securePort is configured';
118
+ if (!env.get(CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) return 'tls.unixDomainSockets is not enabled';
119
+ return undefined;
120
+ }
121
+
122
+ /** Why `appName` cannot get a dedicated worker on this instance, or undefined when it can. */
123
+ export function isolatedApplicationRefusal(appName: string): string | undefined {
124
+ const unreachable = isolatedApplicationsUnreachableReason();
125
+ if (unreachable) return `its worker would be unreachable: ${unreachable}`;
126
+ const socketPath = join(
127
+ env.getHdbBasePath(),
128
+ 'sockets',
129
+ `${applicationSocketName(appName, env.get(CONFIG_PARAMS.HTTP_SECUREPORT))}.sock`
130
+ );
131
+ if (isDomainSocketPathTooLong(socketPath)) return `its UDS mirror path ${socketPath} exceeds the platform limit`;
132
+ return undefined;
133
+ }
134
+
135
+ /**
136
+ * The filesystem name of an isolated worker's UDS mirror for `port`: every UTF-8 byte outside
137
+ * `[A-Za-z0-9._]` becomes a fixed-width `%XX`, which is injective (two application names never share
138
+ * a socket), and the `app-` prefix keeps it apart from the pool's `<workerIndex>-<port>` names.
139
+ */
140
+ export function applicationSocketName(appName: string, port: number | string): string {
141
+ let encoded = '';
142
+ for (const byte of Buffer.from(appName, 'utf8')) {
143
+ const c = String.fromCharCode(byte);
144
+ encoded += byte < 0x80 && /[A-Za-z0-9._]/.test(c) ? c : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
145
+ }
146
+ return `app-${encoded}-${port}`;
147
+ }
148
+
149
+ /** What a dedicated worker publishes in its mirror metadata for the proxy to route by. */
150
+ export function isolatedApplicationRoute(
151
+ appName: string | undefined = thisThreadsIsolatedApplication(),
152
+ config: Record<string, any> | undefined = getConfigObj()
153
+ ): { application: string; hosts: string[] } | undefined {
154
+ if (!appName) return undefined;
155
+ const host = config?.[appName]?.host; // a string, as the mount parser requires
156
+ return { application: appName, hosts: typeof host === 'string' && host ? [host] : [] };
157
+ }