@miragon/pulse-analytics-connector 0.0.0-bootstrap.0 → 0.21.0

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 (357) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -3
  3. package/dist/bpmn-name-lookup.d.ts +72 -0
  4. package/dist/bpmn-name-lookup.d.ts.map +1 -0
  5. package/dist/bpmn-name-lookup.js +123 -0
  6. package/dist/bpmn-name-lookup.js.map +1 -0
  7. package/dist/bpmn-name-lookup.test.d.ts +2 -0
  8. package/dist/bpmn-name-lookup.test.d.ts.map +1 -0
  9. package/dist/bpmn-name-lookup.test.js +287 -0
  10. package/dist/bpmn-name-lookup.test.js.map +1 -0
  11. package/dist/bpmn-names.d.ts +38 -0
  12. package/dist/bpmn-names.d.ts.map +1 -0
  13. package/dist/bpmn-names.js +22 -0
  14. package/dist/bpmn-names.js.map +1 -0
  15. package/dist/brand-gates.test.d.ts +2 -0
  16. package/dist/brand-gates.test.d.ts.map +1 -0
  17. package/dist/brand-gates.test.js +58 -0
  18. package/dist/brand-gates.test.js.map +1 -0
  19. package/dist/builder-texts.d.ts +4 -0
  20. package/dist/builder-texts.d.ts.map +1 -0
  21. package/dist/builder-texts.js +14 -0
  22. package/dist/builder-texts.js.map +1 -0
  23. package/dist/builder-texts.test.d.ts +2 -0
  24. package/dist/builder-texts.test.d.ts.map +1 -0
  25. package/dist/builder-texts.test.js +32 -0
  26. package/dist/builder-texts.test.js.map +1 -0
  27. package/dist/definition.d.ts +3 -0
  28. package/dist/definition.d.ts.map +1 -0
  29. package/dist/definition.js +151 -0
  30. package/dist/definition.js.map +1 -0
  31. package/dist/engine-ids.d.ts +42 -0
  32. package/dist/engine-ids.d.ts.map +1 -0
  33. package/dist/engine-ids.js +50 -0
  34. package/dist/engine-ids.js.map +1 -0
  35. package/dist/engine-scope.test.d.ts +2 -0
  36. package/dist/engine-scope.test.d.ts.map +1 -0
  37. package/dist/engine-scope.test.js +260 -0
  38. package/dist/engine-scope.test.js.map +1 -0
  39. package/dist/heatmap-data.d.ts +15 -0
  40. package/dist/heatmap-data.d.ts.map +1 -0
  41. package/dist/heatmap-data.js +2 -0
  42. package/dist/heatmap-data.js.map +1 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/instructions.d.ts +8 -0
  48. package/dist/instructions.d.ts.map +1 -0
  49. package/dist/instructions.js +33 -0
  50. package/dist/instructions.js.map +1 -0
  51. package/dist/messages/de.ask-ai.d.ts +9 -0
  52. package/dist/messages/de.ask-ai.d.ts.map +1 -0
  53. package/dist/messages/de.ask-ai.js +18 -0
  54. package/dist/messages/de.ask-ai.js.map +1 -0
  55. package/dist/messages/de.builder.d.ts +12 -0
  56. package/dist/messages/de.builder.d.ts.map +1 -0
  57. package/dist/messages/de.builder.js +53 -0
  58. package/dist/messages/de.builder.js.map +1 -0
  59. package/dist/messages/de.server.d.ts +8 -0
  60. package/dist/messages/de.server.d.ts.map +1 -0
  61. package/dist/messages/de.server.js +26 -0
  62. package/dist/messages/de.server.js.map +1 -0
  63. package/dist/messages/de.sweep.d.ts +10 -0
  64. package/dist/messages/de.sweep.d.ts.map +1 -0
  65. package/dist/messages/de.sweep.js +203 -0
  66. package/dist/messages/de.sweep.js.map +1 -0
  67. package/dist/messages/en.ask-ai.d.ts +25 -0
  68. package/dist/messages/en.ask-ai.d.ts.map +1 -0
  69. package/dist/messages/en.ask-ai.js +24 -0
  70. package/dist/messages/en.ask-ai.js.map +1 -0
  71. package/dist/messages/en.builder.d.ts +12 -0
  72. package/dist/messages/en.builder.d.ts.map +1 -0
  73. package/dist/messages/en.builder.js +53 -0
  74. package/dist/messages/en.builder.js.map +1 -0
  75. package/dist/messages/en.server.d.ts +8 -0
  76. package/dist/messages/en.server.d.ts.map +1 -0
  77. package/dist/messages/en.server.js +26 -0
  78. package/dist/messages/en.server.js.map +1 -0
  79. package/dist/messages/en.sweep.d.ts +11 -0
  80. package/dist/messages/en.sweep.d.ts.map +1 -0
  81. package/dist/messages/en.sweep.js +204 -0
  82. package/dist/messages/en.sweep.js.map +1 -0
  83. package/dist/messages/glossary.d.ts +26 -0
  84. package/dist/messages/glossary.d.ts.map +1 -0
  85. package/dist/messages/glossary.js +37 -0
  86. package/dist/messages/glossary.js.map +1 -0
  87. package/dist/messages/glossary.test.d.ts +2 -0
  88. package/dist/messages/glossary.test.d.ts.map +1 -0
  89. package/dist/messages/glossary.test.js +109 -0
  90. package/dist/messages/glossary.test.js.map +1 -0
  91. package/dist/messages/index.d.ts +12 -0
  92. package/dist/messages/index.d.ts.map +1 -0
  93. package/dist/messages/index.js +23 -0
  94. package/dist/messages/index.js.map +1 -0
  95. package/dist/messages/plural.d.ts +10 -0
  96. package/dist/messages/plural.d.ts.map +1 -0
  97. package/dist/messages/plural.js +17 -0
  98. package/dist/messages/plural.js.map +1 -0
  99. package/dist/messages/use-t.d.ts +10 -0
  100. package/dist/messages/use-t.d.ts.map +1 -0
  101. package/dist/messages/use-t.js +10 -0
  102. package/dist/messages/use-t.js.map +1 -0
  103. package/dist/module.d.ts +48 -0
  104. package/dist/module.d.ts.map +1 -0
  105. package/dist/module.js +174 -0
  106. package/dist/module.js.map +1 -0
  107. package/dist/module.test.d.ts +2 -0
  108. package/dist/module.test.d.ts.map +1 -0
  109. package/dist/module.test.js +230 -0
  110. package/dist/module.test.js.map +1 -0
  111. package/dist/named-views.d.ts +27 -0
  112. package/dist/named-views.d.ts.map +1 -0
  113. package/dist/named-views.js +73 -0
  114. package/dist/named-views.js.map +1 -0
  115. package/dist/named-views.test.d.ts +2 -0
  116. package/dist/named-views.test.d.ts.map +1 -0
  117. package/dist/named-views.test.js +110 -0
  118. package/dist/named-views.test.js.map +1 -0
  119. package/dist/plugin.d.ts +44 -0
  120. package/dist/plugin.d.ts.map +1 -0
  121. package/dist/plugin.js +52 -0
  122. package/dist/plugin.js.map +1 -0
  123. package/dist/prometheus-wiring.test.d.ts +2 -0
  124. package/dist/prometheus-wiring.test.d.ts.map +1 -0
  125. package/dist/prometheus-wiring.test.js +142 -0
  126. package/dist/prometheus-wiring.test.js.map +1 -0
  127. package/dist/server-locale.d.ts +48 -0
  128. package/dist/server-locale.d.ts.map +1 -0
  129. package/dist/server-locale.js +33 -0
  130. package/dist/server-locale.js.map +1 -0
  131. package/dist/settings-tools.d.ts +23 -0
  132. package/dist/settings-tools.d.ts.map +1 -0
  133. package/dist/settings-tools.js +100 -0
  134. package/dist/settings-tools.js.map +1 -0
  135. package/dist/settings.d.ts +77 -0
  136. package/dist/settings.d.ts.map +1 -0
  137. package/dist/settings.js +99 -0
  138. package/dist/settings.js.map +1 -0
  139. package/dist/settings.test.d.ts +2 -0
  140. package/dist/settings.test.d.ts.map +1 -0
  141. package/dist/settings.test.js +299 -0
  142. package/dist/settings.test.js.map +1 -0
  143. package/dist/steps/app-config.d.ts +30 -0
  144. package/dist/steps/app-config.d.ts.map +1 -0
  145. package/dist/steps/app-config.js +46 -0
  146. package/dist/steps/app-config.js.map +1 -0
  147. package/dist/steps/dashboard.d.ts +12 -0
  148. package/dist/steps/dashboard.d.ts.map +1 -0
  149. package/dist/steps/dashboard.js +58 -0
  150. package/dist/steps/dashboard.js.map +1 -0
  151. package/dist/steps/failure-dashboard.d.ts +10 -0
  152. package/dist/steps/failure-dashboard.d.ts.map +1 -0
  153. package/dist/steps/failure-dashboard.js +32 -0
  154. package/dist/steps/failure-dashboard.js.map +1 -0
  155. package/dist/steps/index.d.ts +3 -0
  156. package/dist/steps/index.d.ts.map +1 -0
  157. package/dist/steps/index.js +3 -0
  158. package/dist/steps/index.js.map +1 -0
  159. package/dist/steps/steps.test.d.ts +2 -0
  160. package/dist/steps/steps.test.d.ts.map +1 -0
  161. package/dist/steps/steps.test.js +153 -0
  162. package/dist/steps/steps.test.js.map +1 -0
  163. package/dist/tool-names.d.ts +13 -0
  164. package/dist/tool-names.d.ts.map +1 -0
  165. package/dist/tool-names.js +27 -0
  166. package/dist/tool-names.js.map +1 -0
  167. package/dist/tools/cluster-compare.d.ts +8 -0
  168. package/dist/tools/cluster-compare.d.ts.map +1 -0
  169. package/dist/tools/cluster-compare.js +17 -0
  170. package/dist/tools/cluster-compare.js.map +1 -0
  171. package/dist/tools/element.d.ts +8 -0
  172. package/dist/tools/element.d.ts.map +1 -0
  173. package/dist/tools/element.js +25 -0
  174. package/dist/tools/element.js.map +1 -0
  175. package/dist/tools/engine-compare.d.ts +8 -0
  176. package/dist/tools/engine-compare.d.ts.map +1 -0
  177. package/dist/tools/engine-compare.js +18 -0
  178. package/dist/tools/engine-compare.js.map +1 -0
  179. package/dist/tools/engine-compare.test.d.ts +2 -0
  180. package/dist/tools/engine-compare.test.d.ts.map +1 -0
  181. package/dist/tools/engine-compare.test.js +166 -0
  182. package/dist/tools/engine-compare.test.js.map +1 -0
  183. package/dist/tools/engine-landscape.d.ts +7 -0
  184. package/dist/tools/engine-landscape.d.ts.map +1 -0
  185. package/dist/tools/engine-landscape.js +13 -0
  186. package/dist/tools/engine-landscape.js.map +1 -0
  187. package/dist/tools/engine-landscape.test.d.ts +2 -0
  188. package/dist/tools/engine-landscape.test.d.ts.map +1 -0
  189. package/dist/tools/engine-landscape.test.js +110 -0
  190. package/dist/tools/engine-landscape.test.js.map +1 -0
  191. package/dist/tools/failures.d.ts +7 -0
  192. package/dist/tools/failures.d.ts.map +1 -0
  193. package/dist/tools/failures.js +13 -0
  194. package/dist/tools/failures.js.map +1 -0
  195. package/dist/tools/health.d.ts +7 -0
  196. package/dist/tools/health.d.ts.map +1 -0
  197. package/dist/tools/health.js +23 -0
  198. package/dist/tools/health.js.map +1 -0
  199. package/dist/tools/health.test.d.ts +2 -0
  200. package/dist/tools/health.test.d.ts.map +1 -0
  201. package/dist/tools/health.test.js +119 -0
  202. package/dist/tools/health.test.js.map +1 -0
  203. package/dist/tools/index.d.ts +18 -0
  204. package/dist/tools/index.d.ts.map +1 -0
  205. package/dist/tools/index.js +35 -0
  206. package/dist/tools/index.js.map +1 -0
  207. package/dist/tools/performance.d.ts +8 -0
  208. package/dist/tools/performance.d.ts.map +1 -0
  209. package/dist/tools/performance.js +25 -0
  210. package/dist/tools/performance.js.map +1 -0
  211. package/dist/tools/performance.test.d.ts +2 -0
  212. package/dist/tools/performance.test.d.ts.map +1 -0
  213. package/dist/tools/performance.test.js +176 -0
  214. package/dist/tools/performance.test.js.map +1 -0
  215. package/dist/tools/version-compare.d.ts +8 -0
  216. package/dist/tools/version-compare.d.ts.map +1 -0
  217. package/dist/tools/version-compare.js +17 -0
  218. package/dist/tools/version-compare.js.map +1 -0
  219. package/dist/tools/version-compare.test.d.ts +2 -0
  220. package/dist/tools/version-compare.test.d.ts.map +1 -0
  221. package/dist/tools/version-compare.test.js +82 -0
  222. package/dist/tools/version-compare.test.js.map +1 -0
  223. package/dist/toolsets.d.ts +17 -0
  224. package/dist/toolsets.d.ts.map +1 -0
  225. package/dist/toolsets.js +47 -0
  226. package/dist/toolsets.js.map +1 -0
  227. package/dist/toolsets.test.d.ts +2 -0
  228. package/dist/toolsets.test.d.ts.map +1 -0
  229. package/dist/toolsets.test.js +91 -0
  230. package/dist/toolsets.test.js.map +1 -0
  231. package/dist/view-title.test.d.ts +2 -0
  232. package/dist/view-title.test.d.ts.map +1 -0
  233. package/dist/view-title.test.js +91 -0
  234. package/dist/view-title.test.js.map +1 -0
  235. package/dist/widget-tools/comparisons.d.ts +13 -0
  236. package/dist/widget-tools/comparisons.d.ts.map +1 -0
  237. package/dist/widget-tools/comparisons.js +177 -0
  238. package/dist/widget-tools/comparisons.js.map +1 -0
  239. package/dist/widget-tools/comparisons.test.d.ts +2 -0
  240. package/dist/widget-tools/comparisons.test.d.ts.map +1 -0
  241. package/dist/widget-tools/comparisons.test.js +123 -0
  242. package/dist/widget-tools/comparisons.test.js.map +1 -0
  243. package/dist/widget-tools/shared.d.ts +36 -0
  244. package/dist/widget-tools/shared.d.ts.map +1 -0
  245. package/dist/widget-tools/shared.js +39 -0
  246. package/dist/widget-tools/shared.js.map +1 -0
  247. package/dist/widget-tools.d.ts +25 -0
  248. package/dist/widget-tools.d.ts.map +1 -0
  249. package/dist/widget-tools.js +269 -0
  250. package/dist/widget-tools.js.map +1 -0
  251. package/dist/widget-tools.test.d.ts +2 -0
  252. package/dist/widget-tools.test.d.ts.map +1 -0
  253. package/dist/widget-tools.test.js +272 -0
  254. package/dist/widget-tools.test.js.map +1 -0
  255. package/package.json +91 -5
  256. package/src/bpmn-name-lookup.test.ts +333 -0
  257. package/src/bpmn-name-lookup.ts +197 -0
  258. package/src/bpmn-names.ts +51 -0
  259. package/src/brand-gates.test.ts +69 -0
  260. package/src/builder-texts.test.ts +40 -0
  261. package/src/builder-texts.ts +15 -0
  262. package/src/definition.ts +181 -0
  263. package/src/engine-ids.ts +85 -0
  264. package/src/engine-scope.test.ts +324 -0
  265. package/src/heatmap-data.ts +14 -0
  266. package/src/index.ts +4 -0
  267. package/src/instructions.ts +33 -0
  268. package/src/messages/GLOSSARY.md +88 -0
  269. package/src/messages/de.ask-ai.ts +28 -0
  270. package/src/messages/de.builder.ts +77 -0
  271. package/src/messages/de.server.ts +39 -0
  272. package/src/messages/de.sweep.ts +244 -0
  273. package/src/messages/en.ask-ai.ts +34 -0
  274. package/src/messages/en.builder.ts +76 -0
  275. package/src/messages/en.server.ts +39 -0
  276. package/src/messages/en.sweep.ts +240 -0
  277. package/src/messages/glossary.test.ts +122 -0
  278. package/src/messages/glossary.ts +62 -0
  279. package/src/messages/index.ts +24 -0
  280. package/src/messages/plural.ts +18 -0
  281. package/src/messages/use-t.ts +12 -0
  282. package/src/module.test.ts +319 -0
  283. package/src/module.ts +211 -0
  284. package/src/named-views.test.ts +141 -0
  285. package/src/named-views.ts +97 -0
  286. package/src/plugin.ts +94 -0
  287. package/src/prometheus-wiring.test.ts +174 -0
  288. package/src/server-locale.ts +59 -0
  289. package/src/settings-tools.ts +170 -0
  290. package/src/settings.test.ts +402 -0
  291. package/src/settings.ts +114 -0
  292. package/src/steps/app-config.ts +75 -0
  293. package/src/steps/dashboard.ts +73 -0
  294. package/src/steps/failure-dashboard.ts +38 -0
  295. package/src/steps/index.ts +2 -0
  296. package/src/steps/steps.test.ts +216 -0
  297. package/src/tool-names.ts +26 -0
  298. package/src/tools/cluster-compare.ts +28 -0
  299. package/src/tools/element.ts +35 -0
  300. package/src/tools/engine-compare.test.ts +210 -0
  301. package/src/tools/engine-compare.ts +30 -0
  302. package/src/tools/engine-landscape.test.ts +138 -0
  303. package/src/tools/engine-landscape.ts +21 -0
  304. package/src/tools/failures.ts +19 -0
  305. package/src/tools/health.test.ts +156 -0
  306. package/src/tools/health.ts +29 -0
  307. package/src/tools/index.ts +44 -0
  308. package/src/tools/performance.test.ts +210 -0
  309. package/src/tools/performance.ts +38 -0
  310. package/src/tools/version-compare.test.ts +97 -0
  311. package/src/tools/version-compare.ts +28 -0
  312. package/src/toolsets.test.ts +107 -0
  313. package/src/toolsets.ts +56 -0
  314. package/src/view-title.test.ts +123 -0
  315. package/src/widget-tools/comparisons.test.ts +170 -0
  316. package/src/widget-tools/comparisons.ts +229 -0
  317. package/src/widget-tools/shared.ts +67 -0
  318. package/src/widget-tools.test.ts +306 -0
  319. package/src/widget-tools.ts +375 -0
  320. package/src/widgets/analytics-dashboard/activity-bottleneck-table.tsx +179 -0
  321. package/src/widgets/analytics-dashboard/definition-breakdown.tsx +108 -0
  322. package/src/widgets/analytics-dashboard/execution-performance-kpi.tsx +94 -0
  323. package/src/widgets/analytics-dashboard/execution-summary-kpi.tsx +122 -0
  324. package/src/widgets/analytics-dashboard/lib.ts +43 -0
  325. package/src/widgets/bpmn-heatmap.test.tsx +75 -0
  326. package/src/widgets/bpmn-heatmap.tsx +127 -0
  327. package/src/widgets/catalogue-sync.test.ts +16 -0
  328. package/src/widgets/cluster-compare.tsx +148 -0
  329. package/src/widgets/comparison-shared.test.ts +237 -0
  330. package/src/widgets/comparison-shared.tsx +371 -0
  331. package/src/widgets/comparison-widgets.test.tsx +307 -0
  332. package/src/widgets/dashboard-widgets.test.tsx +397 -0
  333. package/src/widgets/engine-compare.tsx +87 -0
  334. package/src/widgets/engine-landscape.tsx +352 -0
  335. package/src/widgets/failure-dashboard/error-patterns-table.tsx +122 -0
  336. package/src/widgets/failure-dashboard/failure-rate-table.test.ts +80 -0
  337. package/src/widgets/failure-dashboard/failure-rate-table.tsx +170 -0
  338. package/src/widgets/failure-dashboard/lib.ts +32 -0
  339. package/src/widgets/failure-dashboard/summary-kpi.tsx +122 -0
  340. package/src/widgets/failure-landscape-widgets.test.tsx +393 -0
  341. package/src/widgets/figure.tsx +79 -0
  342. package/src/widgets/hand-off.test.ts +126 -0
  343. package/src/widgets/hand-off.ts +102 -0
  344. package/src/widgets/index.ts +124 -0
  345. package/src/widgets/model-descriptions.dashboard.test.ts +280 -0
  346. package/src/widgets/model-descriptions.test.ts +251 -0
  347. package/src/widgets/model-descriptions.ts +428 -0
  348. package/src/widgets/names.test.ts +32 -0
  349. package/src/widgets/names.ts +39 -0
  350. package/src/widgets/query-gate.test.tsx +57 -0
  351. package/src/widgets/query-gate.tsx +50 -0
  352. package/src/widgets/settings-section.test.tsx +174 -0
  353. package/src/widgets/settings-section.tsx +244 -0
  354. package/src/widgets/version-compare.test.ts +68 -0
  355. package/src/widgets/version-compare.tsx +96 -0
  356. package/src/widgets/view-meta.ts +18 -0
  357. package/src/widgets/widget-render.test-support.tsx +103 -0
package/package.json CHANGED
@@ -1,8 +1,94 @@
1
1
  {
2
2
  "name": "@miragon/pulse-analytics-connector",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "Placeholder that sets up trusted publishing. The first release of @miragon/pulse-analytics-connector is 0.21.0.",
3
+ "version": "0.21.0",
4
+ "description": "Prometheus-backed process-analytics MCP module: tools, dashboards, and comparison widgets.",
5
5
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/Miragon/pulse.git" },
7
- "homepage": "https://github.com/Miragon/pulse#readme"
8
- }
6
+ "keywords": [
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "camunda",
10
+ "process-analytics",
11
+ "prometheus",
12
+ "widgets"
13
+ ],
14
+ "homepage": "https://github.com/Miragon/pulse/tree/main/packages/connectors/analytics/analytics-connector#readme",
15
+ "bugs": "https://github.com/Miragon/pulse/issues",
16
+ "engines": {
17
+ "node": ">=22.22.2"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/Miragon/pulse.git",
22
+ "directory": "packages/connectors/analytics/analytics-connector"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "files": [
29
+ "dist",
30
+ "src"
31
+ ],
32
+ "main": "dist/index.js",
33
+ "types": "dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ },
39
+ "./widgets": "./src/widgets/index.ts",
40
+ "./definition": "./src/definition.ts"
41
+ },
42
+ "dependencies": {
43
+ "@miragon/pulse-analytics-client": "0.21.0",
44
+ "@miragon/pulse-widget-shell": "0.21.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@miragon/mcp-toolkit-core": "~2.8.1",
48
+ "@miragon/mcp-toolkit-ui": "~2.8.1",
49
+ "lucide-react": "^0.562.0 || ^1.0.0",
50
+ "mcp-use": "2.7.3",
51
+ "react": "^19.2.0",
52
+ "react-dom": "^19.2.0",
53
+ "zod": "^4.4.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@miragon/mcp-toolkit-ui": {
57
+ "optional": true
58
+ },
59
+ "lucide-react": {
60
+ "optional": true
61
+ },
62
+ "react": {
63
+ "optional": true
64
+ },
65
+ "react-dom": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "devDependencies": {
70
+ "@miragon/mcp-toolkit-core": "2.8.1",
71
+ "@miragon/mcp-toolkit-ui": "2.8.1",
72
+ "@stryker-mutator/core": "10.0.0",
73
+ "@stryker-mutator/vitest-runner": "10.0.0",
74
+ "@testing-library/react": "16.3.3",
75
+ "@types/react": "19.3.0",
76
+ "bpmn-js": "18.30.1",
77
+ "happy-dom": "20.14.5",
78
+ "lucide-react": "0.562.0",
79
+ "mcp-use": "2.7.3",
80
+ "react": "19.3.0",
81
+ "react-dom": "19.3.0",
82
+ "typescript": "6.0.3",
83
+ "vitest": "4.1.11",
84
+ "zod": "4.6.5"
85
+ },
86
+ "scripts": {
87
+ "build": "tsc",
88
+ "test": "vitest run",
89
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.widgets.json",
90
+ "lint": "eslint src",
91
+ "lint:fix": "eslint src --fix",
92
+ "test:mutation": "stryker run"
93
+ }
94
+ }
@@ -0,0 +1,333 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest"
2
+ import { getMcpRequestInfo, runWithMcpRequestInfo } from "@miragon/pulse-widget-shell/server"
3
+ import { NO_NAMES, createNameLookup, type FetchBpmnXml } from "./bpmn-name-lookup.js"
4
+ import { elementNameOf, processNameOf } from "./bpmn-names.js"
5
+
6
+ /**
7
+ * The names behind the ids (U13): read on the server from the diagram the
8
+ * injected BPMN lookup returns, never a guess, never a failure. The reader
9
+ * itself is the kit's (`parseBpmnNames`, widget-shell's `bpmn-names.test.ts`):
10
+ * the one rule camunda7 names the same process and activities by.
11
+ */
12
+
13
+ const LEASING = `<?xml version="1.0" encoding="UTF-8"?>
14
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" id="Defs_1">
15
+ <!-- <bpmn:userTask id="Task_Commented" name="Never shown" /> -->
16
+ <bpmn:process id="miraveloLeasing" name="Bike Leasing" isExecutable="true">
17
+ <bpmn:startEvent id="StartEvent_1" name="Application&#10;&#10; received" />
18
+ <bpmn:userTask id="Activity_Decide" name='Decide on "Leasing &amp; Co"'>
19
+ <bpmn:documentation><![CDATA[<bpmn:task id="Task_InCdata" name="Nope"/>]]></bpmn:documentation>
20
+ </bpmn:userTask>
21
+ <bpmn:exclusiveGateway id="Gateway_Amount" name="Amount &gt; 1000?" />
22
+ <bpmn:serviceTask id="Activity_Unnamed" />
23
+ <bpmn:serviceTask id="Activity_Blank" name=" " />
24
+ <bpmn:endEvent id="EndEvent_Done" name="Done &#x2713;"/>
25
+ </bpmn:process>
26
+ </bpmn:definitions>`
27
+
28
+ describe("createNameLookup", () => {
29
+ afterEach(() => {
30
+ vi.useRealTimers()
31
+ })
32
+
33
+ it("knows no names without a BPMN lookup (no camunda7 module)", async () => {
34
+ expect(createNameLookup(undefined)).toBe(NO_NAMES)
35
+ expect(await NO_NAMES(["a"])).toEqual({})
36
+ })
37
+
38
+ it("reads each key once and keeps only the elements a view asks for", async () => {
39
+ const fetchBpmnXml = vi.fn((key: string) =>
40
+ Promise.resolve(key === "miraveloLeasing" ? LEASING : null),
41
+ )
42
+ const namesFor = createNameLookup(fetchBpmnXml)
43
+ const index = await namesFor(["miraveloLeasing", "miraveloLeasing", "", "unknown"], {
44
+ elementIds: { miraveloLeasing: ["Activity_Decide", "Activity_Missing"], unknown: [] },
45
+ })
46
+ // Strictly: an element the model does not name is left out, never `undefined`.
47
+ expect(index).toStrictEqual({
48
+ miraveloLeasing: {
49
+ name: "Bike Leasing",
50
+ elements: { Activity_Decide: 'Decide on "Leasing & Co"' },
51
+ },
52
+ })
53
+ expect(fetchBpmnXml.mock.calls.map((c) => c[0])).toEqual(["miraveloLeasing", "unknown"])
54
+ expect(processNameOf(index, "miraveloLeasing")).toBe("Bike Leasing")
55
+ expect(processNameOf(index, "unknown")).toBeNull()
56
+ expect(elementNameOf(index, "miraveloLeasing", "Activity_Decide")).toBe(
57
+ 'Decide on "Leasing & Co"',
58
+ )
59
+ expect(elementNameOf(index, "miraveloLeasing", "constructor")).toBeNull()
60
+ expect(elementNameOf(index, "unknown", "x")).toBeNull()
61
+ expect(elementNameOf(undefined, "a", "b")).toBeNull()
62
+ // Without an element list a key comes with every named element.
63
+ const all = await namesFor(["miraveloLeasing"])
64
+ expect(Object.keys(all.miraveloLeasing.elements)).toHaveLength(4)
65
+ })
66
+
67
+ it("reuses a read, an empty one too, until it is older than the ttl", async () => {
68
+ let now = 0
69
+ const fetchBpmnXml = vi.fn((key: string) => Promise.resolve(key === "a" ? LEASING : null))
70
+ const namesFor = createNameLookup(fetchBpmnXml, { ttlMs: 1000, now: () => now })
71
+ await namesFor(["a", "b"])
72
+ now = 999
73
+ await namesFor(["a", "b"])
74
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(2)
75
+ now = 1000
76
+ await namesFor(["a", "b"])
77
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(4)
78
+ })
79
+
80
+ it("goes on with the ids when a read fails or is slow, and asks again next time", async () => {
81
+ vi.useFakeTimers()
82
+ const fetchBpmnXml = vi.fn((key: string) =>
83
+ key === "slow"
84
+ ? new Promise<string | null>(() => {})
85
+ : Promise.reject(new Error("ECONNREFUSED")),
86
+ )
87
+ const namesFor = createNameLookup(fetchBpmnXml, { timeoutMs: 50 })
88
+ const pending = namesFor(["slow", "broken"])
89
+ await vi.advanceTimersByTimeAsync(50)
90
+ expect(await pending).toEqual({})
91
+ const again = namesFor(["slow", "broken"])
92
+ await vi.advanceTimersByTimeAsync(50)
93
+ await again
94
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(4)
95
+ expect(vi.getTimerCount()).toBe(0)
96
+ })
97
+
98
+ // The injected fetcher (camunda7's createBpmnXmlFetcher) REJECTS a failed
99
+ // read (503, refused, 401) and resolves null only for "no such model": the
100
+ // engine's failure is no answer to keep for five minutes.
101
+ it("keeps a 'no such model' answer but never a failure", async () => {
102
+ let engineUp = false
103
+ const fetchBpmnXml = vi.fn((key: string) =>
104
+ engineUp
105
+ ? Promise.resolve(key === "miraveloLeasing" ? LEASING : null)
106
+ : Promise.reject(new Error("503 Service Unavailable")),
107
+ )
108
+ const namesFor = createNameLookup(fetchBpmnXml)
109
+ expect(await namesFor(["miraveloLeasing", "unknown"])).toEqual({})
110
+ engineUp = true
111
+ expect(processNameOf(await namesFor(["miraveloLeasing", "unknown"]), "miraveloLeasing")).toBe(
112
+ "Bike Leasing",
113
+ )
114
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(4)
115
+ // Now both are answers: the names and "no such model", both kept.
116
+ await namesFor(["miraveloLeasing", "unknown"])
117
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(4)
118
+ })
119
+
120
+ it("leaves no timer pending after a fast view", async () => {
121
+ vi.useFakeTimers()
122
+ const fetchBpmnXml = vi.fn<FetchBpmnXml>(() => Promise.resolve(LEASING))
123
+ const index = await createNameLookup(fetchBpmnXml)(["miraveloLeasing"])
124
+ expect(processNameOf(index, "miraveloLeasing")).toBe("Bike Leasing")
125
+ expect(vi.getTimerCount()).toBe(0)
126
+ })
127
+
128
+ it("finishes a slow read for the next view", async () => {
129
+ vi.useFakeTimers()
130
+ let answer: (xml: string) => void = () => {}
131
+ const fetchBpmnXml = vi.fn(
132
+ () =>
133
+ new Promise<string | null>((resolve) => {
134
+ answer = resolve
135
+ }),
136
+ )
137
+ const namesFor = createNameLookup(fetchBpmnXml, { timeoutMs: 50 })
138
+ const first = namesFor(["miraveloLeasing"])
139
+ await vi.advanceTimersByTimeAsync(50)
140
+ expect(await first).toEqual({})
141
+ answer(LEASING)
142
+ await vi.advanceTimersByTimeAsync(0)
143
+ expect(processNameOf(await namesFor(["miraveloLeasing"]), "miraveloLeasing")).toBe(
144
+ "Bike Leasing",
145
+ )
146
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(1)
147
+ })
148
+
149
+ it("looks up at most maxKeys keys for one view, in the order it names them", async () => {
150
+ const fetchBpmnXml = vi.fn<FetchBpmnXml>(() => Promise.resolve(null))
151
+ await createNameLookup(fetchBpmnXml, { maxKeys: 2 })(["a", "b", "c"])
152
+ expect(fetchBpmnXml.mock.calls.map((c) => c[0])).toEqual(["a", "b"])
153
+ })
154
+
155
+ it("keeps at most maxEntries answers, dropping the oldest first", async () => {
156
+ const fetchBpmnXml = vi.fn<FetchBpmnXml>(() => Promise.resolve(null))
157
+ const namesFor = createNameLookup(fetchBpmnXml, { maxEntries: 2 })
158
+ await namesFor(["a", "b", "c"])
159
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(3)
160
+ // "b" and "c" are still kept; "a" went first and is read again.
161
+ await namesFor(["b", "c"])
162
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(3)
163
+ await namesFor(["a"])
164
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(4)
165
+ })
166
+
167
+ it("keeps a re-read answer as the newest, so the next to go is the oldest read", async () => {
168
+ let now = 0
169
+ const fetchBpmnXml = vi.fn<FetchBpmnXml>(() => Promise.resolve(null))
170
+ const namesFor = createNameLookup(fetchBpmnXml, { maxEntries: 2, ttlMs: 1000, now: () => now })
171
+ await namesFor(["a", "b"])
172
+ now = 1000
173
+ // "a" expired and is read again: now the newest, "b" the oldest.
174
+ await namesFor(["a"])
175
+ await namesFor(["c"])
176
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(4)
177
+ // "b" went for "c"; "a" is still kept.
178
+ await namesFor(["a"])
179
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(4)
180
+ await namesFor(["b"])
181
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(5)
182
+ })
183
+
184
+ it("never reads an inherited entry for a key such as constructor", async () => {
185
+ const fetchBpmnXml = vi.fn<FetchBpmnXml>(() =>
186
+ Promise.resolve('<process id="constructor" name="Odd"><task id="T" name="Step"/></process>'),
187
+ )
188
+ const index = await createNameLookup(fetchBpmnXml)(["constructor"], {
189
+ elementIds: {},
190
+ enginesByKey: {},
191
+ })
192
+ expect(index.constructor).toEqual({ name: "Odd", elements: { T: "Step" } })
193
+ expect(fetchBpmnXml.mock.calls).toEqual([["constructor", undefined]])
194
+ })
195
+ })
196
+
197
+ /**
198
+ * A process may run on any configured engine, not only the default one
199
+ * (#322 review): each model is read on the engines the view's row comes
200
+ * from, in order, until one of them has it.
201
+ */
202
+ describe("createNameLookup on several engines", () => {
203
+ afterEach(() => {
204
+ vi.useRealTimers()
205
+ })
206
+
207
+ /** An engine fleet: which engine holds which model, and which engine is down. */
208
+ function fleet(models: Record<string, Record<string, string>>, down: string[] = []) {
209
+ return vi.fn<FetchBpmnXml>((key, engine) => {
210
+ if (engine && down.includes(engine)) return Promise.reject(new Error("503"))
211
+ return Promise.resolve(models[engine ?? "default"]?.[key] ?? null)
212
+ })
213
+ }
214
+
215
+ it("reads a key on the view's engines in order until one has its model", async () => {
216
+ const fetchBpmnXml = fleet({ "prod-c": { miraveloLeasing: LEASING } }, ["prod-a"])
217
+ const index = await createNameLookup(fetchBpmnXml)(["miraveloLeasing"], {
218
+ engines: ["prod-a", "prod-b", "prod-c", "prod-d"],
219
+ })
220
+ expect(processNameOf(index, "miraveloLeasing")).toBe("Bike Leasing")
221
+ expect(fetchBpmnXml.mock.calls.map((c) => c[1])).toEqual(["prod-a", "prod-b", "prod-c"])
222
+ })
223
+
224
+ it("prefers a key's own engines over the view's, and the default engine without either", async () => {
225
+ const fetchBpmnXml = fleet({
226
+ "prod-b": { miraveloLeasing: LEASING },
227
+ default: { other: '<process id="other" name="Other"/>' },
228
+ })
229
+ const namesFor = createNameLookup(fetchBpmnXml)
230
+ const index = await namesFor(["miraveloLeasing"], {
231
+ enginesByKey: { miraveloLeasing: ["prod-b"] },
232
+ engines: ["prod-a"],
233
+ })
234
+ expect(processNameOf(index, "miraveloLeasing")).toBe("Bike Leasing")
235
+ expect(processNameOf(await namesFor(["other"]), "other")).toBe("Other")
236
+ expect(fetchBpmnXml.mock.calls).toEqual([
237
+ ["miraveloLeasing", "prod-b"],
238
+ ["other", undefined],
239
+ ])
240
+ })
241
+
242
+ it("takes an empty engine list as none: the view's engines, else the default engine", async () => {
243
+ const fetchBpmnXml = fleet({ "prod-a": { miraveloLeasing: LEASING } })
244
+ const namesFor = createNameLookup(fetchBpmnXml)
245
+ const index = await namesFor(["miraveloLeasing"], {
246
+ enginesByKey: { miraveloLeasing: [] },
247
+ engines: ["prod-a"],
248
+ })
249
+ expect(processNameOf(index, "miraveloLeasing")).toBe("Bike Leasing")
250
+ expect(await namesFor(["other"], { engines: [] })).toEqual({})
251
+ expect(fetchBpmnXml.mock.calls).toEqual([
252
+ ["miraveloLeasing", "prod-a"],
253
+ ["other", undefined],
254
+ ])
255
+ })
256
+
257
+ it("keeps each engine's answer apart", async () => {
258
+ const fetchBpmnXml = fleet({ "prod-b": { miraveloLeasing: LEASING } })
259
+ const namesFor = createNameLookup(fetchBpmnXml)
260
+ // Not on prod-a: kept as "no such model" there, and prod-b still asked.
261
+ expect(await namesFor(["miraveloLeasing"], { engines: ["prod-a"] })).toEqual({})
262
+ const index = await namesFor(["miraveloLeasing"], { engines: ["prod-a", "prod-b"] })
263
+ expect(processNameOf(index, "miraveloLeasing")).toBe("Bike Leasing")
264
+ expect(fetchBpmnXml.mock.calls.map((c) => c[1])).toEqual(["prod-a", "prod-b"])
265
+ })
266
+
267
+ it("stops at the view's time budget, not one budget per engine", async () => {
268
+ vi.useFakeTimers()
269
+ const fetchBpmnXml = vi.fn<FetchBpmnXml>((_key, engine) =>
270
+ engine === "prod-a" ? new Promise(() => {}) : Promise.resolve(LEASING),
271
+ )
272
+ const pending = createNameLookup(fetchBpmnXml, { timeoutMs: 50 })(["miraveloLeasing"], {
273
+ engines: ["prod-a", "prod-b"],
274
+ })
275
+ await vi.advanceTimersByTimeAsync(50)
276
+ expect(await pending).toEqual({})
277
+ expect(fetchBpmnXml.mock.calls.map((c) => c[1])).toEqual(["prod-a"])
278
+ expect(vi.getTimerCount()).toBe(0)
279
+ })
280
+ })
281
+
282
+ /**
283
+ * With passthrough auth the engine answers each caller with that caller's
284
+ * own permissions (#322 review): a model one caller may read never reaches
285
+ * another through the cache, and one caller's refusal never hides the names
286
+ * from the others.
287
+ */
288
+ describe("createNameLookup per caller", () => {
289
+ const ALICE = { authUserId: "alice", authorization: "Bearer token-alice" }
290
+ const BOB = { authUserId: "bob", authorization: "Bearer token-bob" }
291
+
292
+ /** A passthrough engine: only Alice's token may read the model. */
293
+ const passthrough = () =>
294
+ vi.fn<FetchBpmnXml>(() =>
295
+ Promise.resolve(getMcpRequestInfo()?.authorization === ALICE.authorization ? LEASING : null),
296
+ )
297
+
298
+ it("never hands one caller's names to another", async () => {
299
+ const fetchBpmnXml = passthrough()
300
+ const namesFor = createNameLookup(fetchBpmnXml)
301
+ const alice = await runWithMcpRequestInfo(ALICE, () => namesFor(["miraveloLeasing"]))
302
+ expect(processNameOf(alice, "miraveloLeasing")).toBe("Bike Leasing")
303
+ const bob = await runWithMcpRequestInfo(BOB, () => namesFor(["miraveloLeasing"]))
304
+ expect(bob).toEqual({})
305
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(2)
306
+ })
307
+
308
+ it("never hides the names from a caller because another was refused", async () => {
309
+ const fetchBpmnXml = passthrough()
310
+ const namesFor = createNameLookup(fetchBpmnXml)
311
+ expect(await runWithMcpRequestInfo(BOB, () => namesFor(["miraveloLeasing"]))).toEqual({})
312
+ // A request without a token is a caller of its own too.
313
+ expect(await namesFor(["miraveloLeasing"])).toEqual({})
314
+ const alice = await runWithMcpRequestInfo(ALICE, () => namesFor(["miraveloLeasing"]))
315
+ expect(processNameOf(alice, "miraveloLeasing")).toBe("Bike Leasing")
316
+ // Each caller's own answer is still kept.
317
+ await runWithMcpRequestInfo(ALICE, () => namesFor(["miraveloLeasing"]))
318
+ await runWithMcpRequestInfo(BOB, () => namesFor(["miraveloLeasing"]))
319
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(3)
320
+ })
321
+
322
+ it("takes the partition from the injected caller when one is given", async () => {
323
+ let caller = "a"
324
+ const fetchBpmnXml = vi.fn<FetchBpmnXml>(() => Promise.resolve(LEASING))
325
+ const namesFor = createNameLookup(fetchBpmnXml, { caller: () => caller })
326
+ await namesFor(["miraveloLeasing"])
327
+ await namesFor(["miraveloLeasing"])
328
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(1)
329
+ caller = "b"
330
+ await namesFor(["miraveloLeasing"])
331
+ expect(fetchBpmnXml).toHaveBeenCalledTimes(2)
332
+ })
333
+ })
@@ -0,0 +1,197 @@
1
+ import { callerCacheKey, parseBpmnNames } from "@miragon/pulse-widget-shell/server"
2
+ import type { NameIndex, ProcessNames } from "./bpmn-names.js"
3
+
4
+ /**
5
+ * Engine-agnostic BPMN-XML lookup injected by the host app (which owns the
6
+ * engine clients) — this module never talks to an engine itself. Resolves the
7
+ * diagram XML of a process definition key's latest deployed version on the
8
+ * engine `engineId` names (omitted: the host's default engine). `null` is an
9
+ * answer: that engine has no such definition. A read that failed
10
+ * (unreachable, refused, timed out) REJECTS, so a consumer tells "not there"
11
+ * from "not now" and never remembers a failure as an answer.
12
+ */
13
+ export type FetchBpmnXml = (
14
+ processDefinitionKey: string,
15
+ engineId?: string,
16
+ ) => Promise<string | null>
17
+
18
+ /** What one view asks the lookup for, besides its keys. */
19
+ export interface NameRequest {
20
+ /** Per key, the elements whose names the view shows; a key left out gets every named element. */
21
+ elementIds?: Readonly<Record<string, readonly string[]>>
22
+ /**
23
+ * Per key, the engines it runs on, in the order to read them (the
24
+ * landscape knows them per process). Preferred over {@link NameRequest.engines}.
25
+ */
26
+ enginesByKey?: Readonly<Record<string, readonly string[]>>
27
+ /**
28
+ * The view's engines, in the order to read them, for a key without a list
29
+ * of its own: the first whose model has the key names it. None: the host's
30
+ * default engine.
31
+ */
32
+ engines?: readonly string[] | null
33
+ }
34
+
35
+ /**
36
+ * Names for the process keys of one view: a {@link NameIndex} holding the keys
37
+ * whose model could be read on one of their engines, each with the elements
38
+ * the request asks for.
39
+ */
40
+ export type NameLookup = (keys: Iterable<string>, request?: NameRequest) => Promise<NameIndex>
41
+
42
+ /** A lookup that knows no names: what a server without the camunda7 module has. */
43
+ export const NO_NAMES: NameLookup = () => Promise.resolve({})
44
+
45
+ export interface NameLookupOptions {
46
+ /**
47
+ * How long one view waits for its names (ms), over every key and engine.
48
+ * Names are enrichment: a slow engine must not hold up the figures. A read
49
+ * still running then finishes for the next view.
50
+ */
51
+ timeoutMs?: number
52
+ /** How long an engine's answer is reused (ms): diagrams change with a deployment, not per call. */
53
+ ttlMs?: number
54
+ /** The most keys one view looks up, in the order it names them; the rest show their ids. */
55
+ maxKeys?: number
56
+ /** The most answers kept; the oldest go first. */
57
+ maxEntries?: number
58
+ /**
59
+ * Whose engine permissions a read runs under: the partition of the cache,
60
+ * so one caller's answer never serves another. Default: the request's
61
+ * caller and credential (`callerCacheKey`, widget-shell). With passthrough
62
+ * auth every engine read carries the caller's own token and the engine
63
+ * answers with THAT caller's permissions: a model one caller may read must
64
+ * never reach another through the cache, and a caller the engine refused
65
+ * must not hide the names from everyone else.
66
+ */
67
+ caller?: () => string
68
+ /** Clock, for tests. */
69
+ now?: () => number
70
+ }
71
+
72
+ const DEFAULT_TIMEOUT_MS = 2_000
73
+ const DEFAULT_TTL_MS = 5 * 60_000
74
+ const DEFAULT_MAX_KEYS = 40
75
+ const DEFAULT_MAX_ENTRIES = 2_000
76
+
77
+ /** One engine's answer: the names, `null` for "no such model", `undefined` for a failed read. */
78
+ type Answer = ProcessNames | null | undefined
79
+
80
+ /**
81
+ * The view's name lookup over the injected BPMN lookup, or {@link NO_NAMES}
82
+ * without one. Reads a key's model on its engines in order until one has it.
83
+ * Each engine's answer is kept per caller for `ttlMs` (a "no such model"
84
+ * too: a key an engine does not know stays unknown there that long); a failed
85
+ * read is not kept, so the next view asks again. Never waits longer than
86
+ * `timeoutMs` and never fails: a key without an answer is left out, and the
87
+ * view shows its id.
88
+ */
89
+ export function createNameLookup(
90
+ fetchBpmnXml: FetchBpmnXml | undefined,
91
+ options: NameLookupOptions = {},
92
+ ): NameLookup {
93
+ if (!fetchBpmnXml) return NO_NAMES
94
+ const fetchXml = fetchBpmnXml
95
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
96
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS
97
+ const maxKeys = options.maxKeys ?? DEFAULT_MAX_KEYS
98
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES
99
+ const now = options.now ?? Date.now
100
+ const callerOf = options.caller ?? callerCacheKey
101
+ /** (caller, engine, key) → that engine's answer for that caller, oldest first. */
102
+ const cache = new Map<string, { at: number; names: ProcessNames | null }>()
103
+
104
+ function remember(id: string, names: ProcessNames | null): void {
105
+ // Re-inserted at the end, so the map stays oldest first.
106
+ cache.delete(id)
107
+ cache.set(id, { at: now(), names })
108
+ for (const oldest of cache.keys()) {
109
+ if (cache.size <= maxEntries) break
110
+ cache.delete(oldest)
111
+ }
112
+ }
113
+
114
+ function readOn(caller: string, engine: string | undefined, key: string): Promise<Answer> {
115
+ const id = JSON.stringify([caller, engine ?? null, key])
116
+ const hit = cache.get(id)
117
+ if (hit && now() - hit.at < ttlMs) return Promise.resolve(hit.names)
118
+ return Promise.resolve()
119
+ .then(() => fetchXml(key, engine))
120
+ .then(
121
+ (xml) => {
122
+ const names = xml ? parseBpmnNames(xml, key) : null
123
+ remember(id, names)
124
+ return names
125
+ },
126
+ // A failed read is no answer: not kept, the next view asks again.
127
+ () => undefined,
128
+ )
129
+ }
130
+
131
+ return async (keys, request = {}) => {
132
+ const caller = callerOf()
133
+ const unique = [...new Set(keys)].filter((key) => key.length > 0).slice(0, maxKeys)
134
+ const deadline = startDeadline(timeoutMs)
135
+
136
+ async function namesOf(key: string): Promise<ProcessNames | null> {
137
+ for (const engine of enginesFor(key, request)) {
138
+ const names = await Promise.race([readOn(caller, engine, key), deadline.passed])
139
+ if (names) return names
140
+ if (deadline.expired()) break
141
+ }
142
+ return null
143
+ }
144
+
145
+ try {
146
+ const reads = await Promise.all(unique.map(async (key) => [key, await namesOf(key)] as const))
147
+ const index: NameIndex = {}
148
+ for (const [key, names] of reads) {
149
+ if (!names) continue
150
+ const wanted = own(request.elementIds, key)
151
+ index[key] = {
152
+ name: names.name,
153
+ elements: wanted ? pick(names.elements, wanted) : names.elements,
154
+ }
155
+ }
156
+ return index
157
+ } finally {
158
+ deadline.clear()
159
+ }
160
+ }
161
+ }
162
+
163
+ /** `record[key]` when the record itself holds it: a key such as `constructor` is no inherited lookup. */
164
+ function own<T>(record: Readonly<Record<string, T>> | undefined, key: string): T | undefined {
165
+ return record && Object.hasOwn(record, key) ? record[key] : undefined
166
+ }
167
+
168
+ /** The engines to read `key`'s model on, in order: its own, else the view's, else the default engine. */
169
+ function enginesFor(key: string, request: NameRequest): ReadonlyArray<string | undefined> {
170
+ const mine = own(request.enginesByKey, key)
171
+ if (mine && mine.length > 0) return mine
172
+ if (request.engines && request.engines.length > 0) return request.engines
173
+ return [undefined]
174
+ }
175
+
176
+ /** The entries of `elements` named in `ids` (what the view shows, nothing else). */
177
+ function pick(elements: Record<string, string>, ids: readonly string[]): Record<string, string> {
178
+ const out: Record<string, string> = {}
179
+ for (const id of ids) if (Object.hasOwn(elements, id)) out[id] = elements[id]
180
+ return out
181
+ }
182
+
183
+ /**
184
+ * One view's time budget: `passed` resolves `undefined` once `ms` passed, and
185
+ * `clear` stops the timer, so a fast view leaves nothing pending.
186
+ */
187
+ function startDeadline(ms: number) {
188
+ let timer: ReturnType<typeof setTimeout> | undefined
189
+ let expired = false
190
+ const passed = new Promise<undefined>((resolve) => {
191
+ timer = setTimeout(() => {
192
+ expired = true
193
+ resolve(undefined)
194
+ }, ms)
195
+ })
196
+ return { passed, expired: () => expired, clear: () => clearTimeout(timer) }
197
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The names a BPMN model gives its process and elements, for views that would
3
+ * otherwise show bare ids (U13). Metrics carry ids only by contract (the
4
+ * labels are model-bounded); the names come from the diagram the server
5
+ * reads through the injected BPMN lookup (`bpmn-name-lookup.ts`) and travel in
6
+ * a view's payload as a {@link NameIndex}. Shared by the server and the widget
7
+ * bundle, so it holds types and pure helpers only.
8
+ */
9
+
10
+ /**
11
+ * The names one model gives: its process and the elements that have one.
12
+ * The shape of the kit's `BpmnNames` (`parseBpmnNames`, widget-shell
13
+ * `/server`), the one rule camunda7 names the same model by.
14
+ */
15
+ export interface ProcessNames {
16
+ /** The process's own name in its model (or its pool's); null when it has none. */
17
+ name: string | null
18
+ /** Element id → name, only for elements that carry a name. */
19
+ elements: Record<string, string>
20
+ }
21
+
22
+ /**
23
+ * Names per process definition key. A key is absent when its model could not
24
+ * be read (no camunda7 module, the engine was unreachable, not deployed on the
25
+ * engine the lookup reads): the view then shows the id, never a guess.
26
+ */
27
+ export type NameIndex = Record<string, ProcessNames>
28
+
29
+ /**
30
+ * A view payload with the names of the processes (and, for the activity
31
+ * breakdown, the elements) it lists. `names` is absent where no lookup ran
32
+ * (a payload built by a plain query call): the view shows ids.
33
+ */
34
+ export type Named<T> = T & { names?: NameIndex }
35
+
36
+ /** The process name for `key`, or null when the index does not know one. */
37
+ export function processNameOf(names: NameIndex | undefined, key: string): string | null {
38
+ if (!names || !Object.hasOwn(names, key)) return null
39
+ return names[key].name
40
+ }
41
+
42
+ /** The name of element `id` in the model of `key`, or null when the index does not know one. */
43
+ export function elementNameOf(
44
+ names: NameIndex | undefined,
45
+ key: string,
46
+ id: string,
47
+ ): string | null {
48
+ if (!names || !Object.hasOwn(names, key)) return null
49
+ const elements = names[key].elements
50
+ return Object.hasOwn(elements, id) ? elements[id] : null
51
+ }