solve-engine 1.0.0-beta.0 → 1.0.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 (249) hide show
  1. package/dist/BytecodeBuilder-3egMyuDi.d.cts +274 -0
  2. package/dist/BytecodeBuilder-3egMyuDi.d.ts +274 -0
  3. package/dist/Configuration-fVpd-HC-.d.cts +228 -0
  4. package/dist/Configuration-fVpd-HC-.d.ts +228 -0
  5. package/dist/EngineError-DvPSnhZi.d.cts +304 -0
  6. package/dist/EngineError-DvPSnhZi.d.ts +304 -0
  7. package/dist/Lexer-BJdhlnej.d.cts +531 -0
  8. package/dist/Lexer-D9l4Yrk2.d.ts +531 -0
  9. package/dist/NormalizerRule-BrVoVjmP.d.cts +163 -0
  10. package/dist/NormalizerRule-CEjf1FyD.d.ts +163 -0
  11. package/dist/PackageRegistry-D-Tv_7ca.d.cts +2572 -0
  12. package/dist/PackageRegistry-DaH4aIKP.d.ts +2572 -0
  13. package/dist/Parselet-CK8cQu2a.d.cts +299 -0
  14. package/dist/Parselet-ConOIdRO.d.ts +299 -0
  15. package/dist/ScopeManager-B5k0nk05.d.ts +559 -0
  16. package/dist/ScopeManager-CKfQtl7l.d.cts +559 -0
  17. package/dist/Token-0jpvUdPY.d.cts +245 -0
  18. package/dist/Token-0jpvUdPY.d.ts +245 -0
  19. package/dist/VMCheckpoints-BmDwIXgK.d.ts +171 -0
  20. package/dist/VMCheckpoints-CqFuLwrT.d.cts +171 -0
  21. package/dist/Value-KtAQNHur.d.cts +295 -0
  22. package/dist/Value-KtAQNHur.d.ts +295 -0
  23. package/dist/chunk-2RWAXT6O.js +3 -0
  24. package/dist/chunk-2RWAXT6O.js.map +1 -0
  25. package/dist/chunk-34RRD7PC.js +1492 -0
  26. package/dist/chunk-34RRD7PC.js.map +1 -0
  27. package/dist/chunk-3PPFLFH4.js +5097 -0
  28. package/dist/chunk-3PPFLFH4.js.map +1 -0
  29. package/dist/chunk-3VGDNEE3.js +84 -0
  30. package/dist/chunk-3VGDNEE3.js.map +1 -0
  31. package/dist/chunk-3YNVWKR2.cjs +370 -0
  32. package/dist/chunk-3YNVWKR2.cjs.map +1 -0
  33. package/dist/chunk-47LRVGOT.cjs +316 -0
  34. package/dist/chunk-47LRVGOT.cjs.map +1 -0
  35. package/dist/chunk-4MG4XKO2.js +1644 -0
  36. package/dist/chunk-4MG4XKO2.js.map +1 -0
  37. package/dist/chunk-4QADQTWS.js +2392 -0
  38. package/dist/chunk-4QADQTWS.js.map +1 -0
  39. package/dist/chunk-53B6KDDJ.cjs +1651 -0
  40. package/dist/chunk-53B6KDDJ.cjs.map +1 -0
  41. package/dist/chunk-5X2PTP6F.cjs +104 -0
  42. package/dist/chunk-5X2PTP6F.cjs.map +1 -0
  43. package/dist/chunk-5YEMOYSE.js +264 -0
  44. package/dist/chunk-5YEMOYSE.js.map +1 -0
  45. package/dist/chunk-64W6GLLZ.js +481 -0
  46. package/dist/chunk-64W6GLLZ.js.map +1 -0
  47. package/dist/chunk-6BKTCEUP.cjs +233 -0
  48. package/dist/chunk-6BKTCEUP.cjs.map +1 -0
  49. package/dist/chunk-6NTVRDQV.cjs +1642 -0
  50. package/dist/chunk-6NTVRDQV.cjs.map +1 -0
  51. package/dist/chunk-6RDSQURE.js +290 -0
  52. package/dist/chunk-6RDSQURE.js.map +1 -0
  53. package/dist/chunk-75JP4BZX.js +93 -0
  54. package/dist/chunk-75JP4BZX.js.map +1 -0
  55. package/dist/chunk-BVAR5J2F.js +257 -0
  56. package/dist/chunk-BVAR5J2F.js.map +1 -0
  57. package/dist/chunk-C4XZV6E7.cjs +269 -0
  58. package/dist/chunk-C4XZV6E7.cjs.map +1 -0
  59. package/dist/chunk-CCJPSHGI.cjs +40 -0
  60. package/dist/chunk-CCJPSHGI.cjs.map +1 -0
  61. package/dist/chunk-CLL7RUQV.cjs +316 -0
  62. package/dist/chunk-CLL7RUQV.cjs.map +1 -0
  63. package/dist/chunk-DM3LMRBC.js +329 -0
  64. package/dist/chunk-DM3LMRBC.js.map +1 -0
  65. package/dist/chunk-DUEZFTK2.js +38 -0
  66. package/dist/chunk-DUEZFTK2.js.map +1 -0
  67. package/dist/chunk-EBSPLUW4.cjs +489 -0
  68. package/dist/chunk-EBSPLUW4.cjs.map +1 -0
  69. package/dist/chunk-EHAHVROS.cjs +261 -0
  70. package/dist/chunk-EHAHVROS.cjs.map +1 -0
  71. package/dist/chunk-EPOXXJBK.js +96 -0
  72. package/dist/chunk-EPOXXJBK.js.map +1 -0
  73. package/dist/chunk-FEAIOR7N.cjs +91 -0
  74. package/dist/chunk-FEAIOR7N.cjs.map +1 -0
  75. package/dist/chunk-G535KJEG.js +312 -0
  76. package/dist/chunk-G535KJEG.js.map +1 -0
  77. package/dist/chunk-GCM5JHMX.js +88 -0
  78. package/dist/chunk-GCM5JHMX.js.map +1 -0
  79. package/dist/chunk-GOLDJNMZ.js +368 -0
  80. package/dist/chunk-GOLDJNMZ.js.map +1 -0
  81. package/dist/chunk-GW32KPCU.cjs +360 -0
  82. package/dist/chunk-GW32KPCU.cjs.map +1 -0
  83. package/dist/chunk-HO2CDZRC.js +5508 -0
  84. package/dist/chunk-HO2CDZRC.js.map +1 -0
  85. package/dist/chunk-HTXVVJRA.cjs +1549 -0
  86. package/dist/chunk-HTXVVJRA.cjs.map +1 -0
  87. package/dist/chunk-HWSZJQCI.js +311 -0
  88. package/dist/chunk-HWSZJQCI.js.map +1 -0
  89. package/dist/chunk-IF532O7C.js +101 -0
  90. package/dist/chunk-IF532O7C.js.map +1 -0
  91. package/dist/chunk-J73SJHR3.cjs +2415 -0
  92. package/dist/chunk-J73SJHR3.cjs.map +1 -0
  93. package/dist/chunk-KVILKGMS.js +228 -0
  94. package/dist/chunk-KVILKGMS.js.map +1 -0
  95. package/dist/chunk-LIPPNDBE.js +258 -0
  96. package/dist/chunk-LIPPNDBE.js.map +1 -0
  97. package/dist/chunk-M4N5GA5Z.cjs +309 -0
  98. package/dist/chunk-M4N5GA5Z.cjs.map +1 -0
  99. package/dist/chunk-MZOVP3WI.cjs +96 -0
  100. package/dist/chunk-MZOVP3WI.cjs.map +1 -0
  101. package/dist/chunk-N5YFR4UP.cjs +10 -0
  102. package/dist/chunk-N5YFR4UP.cjs.map +1 -0
  103. package/dist/chunk-NGBBNL4K.cjs +4 -0
  104. package/dist/chunk-NGBBNL4K.cjs.map +1 -0
  105. package/dist/chunk-NH2O2AUR.js +146 -0
  106. package/dist/chunk-NH2O2AUR.js.map +1 -0
  107. package/dist/chunk-NLNDMWZC.js +31 -0
  108. package/dist/chunk-NLNDMWZC.js.map +1 -0
  109. package/dist/chunk-NMCRQP3Z.cjs +778 -0
  110. package/dist/chunk-NMCRQP3Z.cjs.map +1 -0
  111. package/dist/chunk-NMD5VRN4.cjs +1327 -0
  112. package/dist/chunk-NMD5VRN4.cjs.map +1 -0
  113. package/dist/chunk-OC6LTXYS.cjs +286 -0
  114. package/dist/chunk-OC6LTXYS.cjs.map +1 -0
  115. package/dist/chunk-OT6OJY7C.cjs +114 -0
  116. package/dist/chunk-OT6OJY7C.cjs.map +1 -0
  117. package/dist/chunk-PEWZMGH7.cjs +5522 -0
  118. package/dist/chunk-PEWZMGH7.cjs.map +1 -0
  119. package/dist/chunk-PFSDGGYL.js +8 -0
  120. package/dist/chunk-PFSDGGYL.js.map +1 -0
  121. package/dist/chunk-QNJ4ACRT.cjs +155 -0
  122. package/dist/chunk-QNJ4ACRT.cjs.map +1 -0
  123. package/dist/chunk-QWY3VEZN.js +1637 -0
  124. package/dist/chunk-QWY3VEZN.js.map +1 -0
  125. package/dist/chunk-RFYD5TJE.js +111 -0
  126. package/dist/chunk-RFYD5TJE.js.map +1 -0
  127. package/dist/chunk-RIN643A3.js +354 -0
  128. package/dist/chunk-RIN643A3.js.map +1 -0
  129. package/dist/chunk-RZCWSXTA.cjs +5122 -0
  130. package/dist/chunk-RZCWSXTA.cjs.map +1 -0
  131. package/dist/chunk-SDGRK7EP.js +774 -0
  132. package/dist/chunk-SDGRK7EP.js.map +1 -0
  133. package/dist/chunk-VEKIWQ3F.cjs +39 -0
  134. package/dist/chunk-VEKIWQ3F.cjs.map +1 -0
  135. package/dist/chunk-W44QGY5J.cjs +172 -0
  136. package/dist/chunk-W44QGY5J.cjs.map +1 -0
  137. package/dist/chunk-WQTTOGXC.cjs +99 -0
  138. package/dist/chunk-WQTTOGXC.cjs.map +1 -0
  139. package/dist/chunk-X6K3HUOY.cjs +86 -0
  140. package/dist/chunk-X6K3HUOY.cjs.map +1 -0
  141. package/dist/chunk-XJCMXB2E.cjs +333 -0
  142. package/dist/chunk-XJCMXB2E.cjs.map +1 -0
  143. package/dist/chunk-XVWCOTR6.js +1324 -0
  144. package/dist/chunk-XVWCOTR6.js.map +1 -0
  145. package/dist/chunk-Z2XP767T.cjs +35 -0
  146. package/dist/chunk-Z2XP767T.cjs.map +1 -0
  147. package/dist/chunk-Z4X2RWOF.js +37 -0
  148. package/dist/chunk-Z4X2RWOF.js.map +1 -0
  149. package/dist/chunk-ZP3UY6ZX.js +167 -0
  150. package/dist/chunk-ZP3UY6ZX.js.map +1 -0
  151. package/dist/constants.cjs +21 -0
  152. package/dist/constants.cjs.map +1 -0
  153. package/dist/constants.d.cts +12 -0
  154. package/dist/constants.d.ts +12 -0
  155. package/dist/constants.js +4 -0
  156. package/dist/constants.js.map +1 -0
  157. package/dist/engine.cjs +78 -0
  158. package/dist/engine.cjs.map +1 -0
  159. package/dist/engine.d.cts +587 -0
  160. package/dist/engine.d.ts +587 -0
  161. package/dist/engine.js +33 -0
  162. package/dist/engine.js.map +1 -0
  163. package/dist/errors.cjs +80 -0
  164. package/dist/errors.cjs.map +1 -0
  165. package/dist/errors.d.cts +95 -0
  166. package/dist/errors.d.ts +95 -0
  167. package/dist/errors.js +3 -0
  168. package/dist/errors.js.map +1 -0
  169. package/dist/format.cjs +170 -0
  170. package/dist/format.cjs.map +1 -0
  171. package/dist/format.d.cts +51 -0
  172. package/dist/format.d.ts +51 -0
  173. package/dist/format.js +167 -0
  174. package/dist/format.js.map +1 -0
  175. package/dist/index.cjs +96 -0
  176. package/dist/index.cjs.map +1 -0
  177. package/dist/index.d.cts +152 -0
  178. package/dist/index.d.ts +152 -0
  179. package/dist/index.js +74 -0
  180. package/dist/index.js.map +1 -0
  181. package/dist/language.cjs +307 -0
  182. package/dist/language.cjs.map +1 -0
  183. package/dist/language.d.cts +110 -0
  184. package/dist/language.d.ts +110 -0
  185. package/dist/language.js +286 -0
  186. package/dist/language.js.map +1 -0
  187. package/dist/lexer.cjs +63 -0
  188. package/dist/lexer.cjs.map +1 -0
  189. package/dist/lexer.d.cts +56 -0
  190. package/dist/lexer.d.ts +56 -0
  191. package/dist/lexer.js +10 -0
  192. package/dist/lexer.js.map +1 -0
  193. package/dist/normalizer.cjs +39 -0
  194. package/dist/normalizer.cjs.map +1 -0
  195. package/dist/normalizer.d.cts +364 -0
  196. package/dist/normalizer.d.ts +364 -0
  197. package/dist/normalizer.js +10 -0
  198. package/dist/normalizer.js.map +1 -0
  199. package/dist/packages.cjs +120 -0
  200. package/dist/packages.cjs.map +1 -0
  201. package/dist/packages.d.cts +691 -0
  202. package/dist/packages.d.ts +691 -0
  203. package/dist/packages.js +23 -0
  204. package/dist/packages.js.map +1 -0
  205. package/dist/parser.cjs +54 -0
  206. package/dist/parser.cjs.map +1 -0
  207. package/dist/parser.d.cts +46 -0
  208. package/dist/parser.d.ts +46 -0
  209. package/dist/parser.js +9 -0
  210. package/dist/parser.js.map +1 -0
  211. package/dist/pipeline-CrsRPmsv.d.ts +285 -0
  212. package/dist/pipeline-DPUQt7yL.d.cts +285 -0
  213. package/dist/resolvers.cjs +12 -0
  214. package/dist/resolvers.cjs.map +1 -0
  215. package/dist/resolvers.d.cts +111 -0
  216. package/dist/resolvers.d.ts +111 -0
  217. package/dist/resolvers.js +3 -0
  218. package/dist/resolvers.js.map +1 -0
  219. package/dist/services.cjs +20 -0
  220. package/dist/services.cjs.map +1 -0
  221. package/dist/services.d.cts +30 -0
  222. package/dist/services.d.ts +30 -0
  223. package/dist/services.js +3 -0
  224. package/dist/services.js.map +1 -0
  225. package/dist/uom.cjs +55 -0
  226. package/dist/uom.cjs.map +1 -0
  227. package/dist/uom.d.cts +224 -0
  228. package/dist/uom.d.ts +224 -0
  229. package/dist/uom.js +10 -0
  230. package/dist/uom.js.map +1 -0
  231. package/dist/utilities.cjs +31 -0
  232. package/dist/utilities.cjs.map +1 -0
  233. package/dist/utilities.d.cts +113 -0
  234. package/dist/utilities.d.ts +113 -0
  235. package/dist/utilities.js +6 -0
  236. package/dist/utilities.js.map +1 -0
  237. package/dist/variables.cjs +16 -0
  238. package/dist/variables.cjs.map +1 -0
  239. package/dist/variables.d.cts +87 -0
  240. package/dist/variables.d.ts +87 -0
  241. package/dist/variables.js +3 -0
  242. package/dist/variables.js.map +1 -0
  243. package/dist/vm.cjs +283 -0
  244. package/dist/vm.cjs.map +1 -0
  245. package/dist/vm.d.cts +68 -0
  246. package/dist/vm.d.ts +68 -0
  247. package/dist/vm.js +193 -0
  248. package/dist/vm.js.map +1 -0
  249. package/package.json +2 -1
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ // src/resolvers/ResolverRegistry.ts
4
+ var ResolverRegistry = class {
5
+ constructor() {
6
+ this.resolvers = /* @__PURE__ */ new Map();
7
+ }
8
+ /**
9
+ * Register an async resolver.
10
+ *
11
+ * Each resolver must have a unique `namespace`. If a resolver with the
12
+ * same namespace is already registered, the old one is destroyed and
13
+ * replaced (with a console warning). Packages that need multiple async
14
+ * operations should use `asyncResolvers: [...]` with distinct namespaces
15
+ * (e.g., `"weather:current"`, `"weather:forecast"`).
16
+ */
17
+ register(resolver) {
18
+ if (this.resolvers.has(resolver.namespace)) {
19
+ console.warn(
20
+ `[ResolverRegistry] Namespace "${resolver.namespace}" already registered. Destroying old resolver and overwriting. Use distinct namespaces (e.g., "${resolver.namespace}:sub1", "${resolver.namespace}:sub2") to avoid collisions when a package needs multiple async resolvers.`
21
+ );
22
+ this.resolvers.get(resolver.namespace).destroy();
23
+ }
24
+ this.resolvers.set(resolver.namespace, resolver);
25
+ }
26
+ /**
27
+ * Unregister a resolver by namespace.
28
+ * Calls destroy() and clears all cache entries with that namespace prefix.
29
+ */
30
+ unregister(namespace, queryClient) {
31
+ const resolver = this.resolvers.get(namespace);
32
+ if (resolver) {
33
+ resolver.destroy();
34
+ this.resolvers.delete(namespace);
35
+ }
36
+ if (queryClient) {
37
+ queryClient.removeQueries({ queryKey: [namespace] });
38
+ }
39
+ }
40
+ /**
41
+ * Run all registered resolvers' preflight checks against compiled bytecode.
42
+ *
43
+ * Returns the first AsyncCheckResult found, or null if all data is ready.
44
+ * Short-circuits on first pending, subsequent pending ops will be
45
+ * discovered on re-evaluation when the first resolves.
46
+ */
47
+ preflightAll(tokens, bytecode, packageId, signal, queryClient) {
48
+ for (const resolver of this.resolvers.values()) {
49
+ if (!resolver.preflight) continue;
50
+ const result = resolver.preflight(tokens, bytecode, packageId, signal, queryClient);
51
+ if (result) return result;
52
+ }
53
+ return null;
54
+ }
55
+ /**
56
+ * Get a resolver by namespace.
57
+ * Returns undefined if no resolver is registered for that namespace.
58
+ */
59
+ get(namespace) {
60
+ return this.resolvers.get(namespace);
61
+ }
62
+ /** Number of registered resolvers. */
63
+ get size() {
64
+ return this.resolvers.size;
65
+ }
66
+ /**
67
+ * Check if a resolver is registered for the given namespace.
68
+ */
69
+ has(namespace) {
70
+ return this.resolvers.has(namespace);
71
+ }
72
+ /**
73
+ * Clear all resolvers (document switch, engine reset).
74
+ * Calls destroy() on each resolver and clears the map.
75
+ */
76
+ clear() {
77
+ for (const resolver of this.resolvers.values()) {
78
+ resolver.destroy();
79
+ }
80
+ this.resolvers.clear();
81
+ }
82
+ };
83
+
84
+ exports.ResolverRegistry = ResolverRegistry;
85
+ //# sourceMappingURL=chunk-X6K3HUOY.cjs.map
86
+ //# sourceMappingURL=chunk-X6K3HUOY.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/resolvers/ResolverRegistry.ts"],"names":[],"mappings":";;;AAuEO,IAAM,mBAAN,MAAuB;AAAA,EAAvB,WAAA,GAAA;AACN,IAAA,IAAA,CAAQ,SAAA,uBAAgB,GAAA,EAA4B;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWpD,SAAS,QAAA,EAAgC;AAExC,IAAA,IAAI,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AAC3C,MAAA,OAAA,CAAQ,IAAA;AAAA,QACP,CAAA,8BAAA,EAAiC,SAAS,SAAS,CAAA,+FAAA,EAExC,SAAS,SAAS,CAAA,SAAA,EAAY,SAAS,SAAS,CAAA,0EAAA;AAAA,OAE5D;AACA,MAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,QAAA,CAAS,SAAS,EAAG,OAAA,EAAQ;AAAA,IACjD;AACA,IAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,QAAA,CAAS,SAAA,EAAW,QAAQ,CAAA;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAA,CAAW,WAAmB,WAAA,EAAiC;AAC9D,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,SAAS,CAAA;AAC7C,IAAA,IAAI,QAAA,EAAU;AACb,MAAA,QAAA,CAAS,OAAA,EAAQ;AACjB,MAAA,IAAA,CAAK,SAAA,CAAU,OAAO,SAAS,CAAA;AAAA,IAChC;AAGA,IAAA,IAAI,WAAA,EAAa;AAChB,MAAA,WAAA,CAAY,cAAc,EAAE,QAAA,EAAU,CAAC,SAAS,GAAG,CAAA;AAAA,IACpD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAA,CAAa,MAAA,EAAiB,QAAA,EAA2B,SAAA,EAAmB,QAAqB,WAAA,EAAmD;AACnJ,IAAA,KAAA,MAAW,QAAA,IAAY,IAAA,CAAK,SAAA,CAAU,MAAA,EAAO,EAAG;AAC/C,MAAA,IAAI,CAAC,SAAS,SAAA,EAAW;AACzB,MAAA,MAAM,SAAS,QAAA,CAAS,SAAA,CAAU,QAAQ,QAAA,EAAU,SAAA,EAAW,QAAQ,WAAW,CAAA;AAClF,MAAA,IAAI,QAAQ,OAAO,MAAA;AAAA,IACpB;AACA,IAAA,OAAO,IAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,SAAA,EAA+C;AAClD,IAAA,OAAO,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,SAAS,CAAA;AAAA,EACpC;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AAClB,IAAA,OAAO,KAAK,SAAA,CAAU,IAAA;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,SAAA,EAA4B;AAC/B,IAAA,OAAO,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,SAAS,CAAA;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAA,GAAc;AACb,IAAA,KAAA,MAAW,QAAA,IAAY,IAAA,CAAK,SAAA,CAAU,MAAA,EAAO,EAAG;AAC/C,MAAA,QAAA,CAAS,OAAA,EAAQ;AAAA,IAClB;AACA,IAAA,IAAA,CAAK,UAAU,KAAA,EAAM;AAAA,EACtB;AACD","file":"chunk-X6K3HUOY.cjs","sourcesContent":["import type { QueryClient } from \"@tanstack/query-core\";\nimport type { Token } from \"@solve-js/lexer\";\nimport type { BytecodeProgram } from \"@solve-js/parser/BytecodeBuilder\";\nimport type { Value } from \"@solve-js/vm/Value\";\n\n/**\n * Result from a resolver's preflight() check.\n * Returned when async data is needed, the orchestrator uses this\n * to subscribe to the resolver and return a Pending value immediately.\n */\nexport interface AsyncCheckResult {\n\t/** Unique cache key for deduplication (e.g., \"{packageId}:rates:USD:GBP\") */\n\tqueryKey: string;\n\t/** Promise that resolves to the final Value */\n\tresolver: Promise<Value>;\n\t/** Package owning this resolver */\n\tpackageId: string;\n\t/** AbortSignal for stale-data prevention */\n\tsignal: AbortSignal;\n\t/** Optional metadata for diagnostics */\n\tmetadata?: Record<string, unknown>;\n}\n\n/**\n * Interface for async resolvers registered by packages/plugins.\n *\n * Each resolver handles a domain of async data (currency rates,\n * weather, stock prices, etc.). The preflight() method is called\n * BEFORE VM execution to check if all data is cached. If not,\n * it returns an AsyncCheckResult and the engine skips the VM,\n * returning a Pending value instead.\n */\nexport interface IAsyncResolver {\n\t/** Unique namespace for cache key scoping (e.g., \"currency\", \"weather\") */\n\treadonly namespace: string;\n\n\t/**\n\t * Pre-flight check: called BEFORE VM execution.\n\t *\n\t * Returns null if all data for this expression is cached and ready.\n\t * Returns an AsyncCheckResult if async work is needed.\n\t *\n\t * This method is synchronous, it only checks caches, never fetches.\n\t * If data is missing, it creates a Promise and returns immediately.\n\t *\n\t * @param tokens - The lexed tokens for the expression\n\t * @param bytecode - The compiled bytecode program\n\t * @param pluginId - The plugin ID (for cache key scoping)\n\t * @param signal - AbortSignal for the current evaluation\n\t */\n\tpreflight?(tokens: Token[], bytecode: BytecodeProgram, packageId: string, signal: AbortSignal, queryClient: QueryClient): AsyncCheckResult | null;\n\n\t/**\n\t * Called when the resolver's namespace is being unregistered.\n\t * Clean up any in-flight promises, clear caches, unsubscribe.\n\t */\n\tdestroy(): void;\n}\n\n/**\n * Registry of async resolvers, keyed by namespace.\n *\n * Plugins/packages register resolvers alongside their parselets\n * and opcode handlers. The engine calls preflightAll() before\n * VM execution to short-circuit async expressions.\n *\n * Lifecycle:\n * - register(), when a package is loaded\n * - unregister(), when a package is unloaded (clears cache entries)\n * - clear(), on document switch (clears all resolvers)\n */\nexport class ResolverRegistry {\n\tprivate resolvers = new Map<string, IAsyncResolver>();\n\n\t/**\n\t * Register an async resolver.\n\t *\n\t * Each resolver must have a unique `namespace`. If a resolver with the\n\t * same namespace is already registered, the old one is destroyed and\n\t * replaced (with a console warning). Packages that need multiple async\n\t * operations should use `asyncResolvers: [...]` with distinct namespaces\n\t * (e.g., `\"weather:current\"`, `\"weather:forecast\"`).\n\t */\n\tregister(resolver: IAsyncResolver): void {\n\t\t// Clean up previous resolver with same namespace if any\n\t\tif (this.resolvers.has(resolver.namespace)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[ResolverRegistry] Namespace \"${resolver.namespace}\" already registered. ` +\n\t\t\t\t`Destroying old resolver and overwriting. Use distinct namespaces ` +\n\t\t\t\t`(e.g., \"${resolver.namespace}:sub1\", \"${resolver.namespace}:sub2\") ` +\n\t\t\t\t`to avoid collisions when a package needs multiple async resolvers.`,\n\t\t\t);\n\t\t\tthis.resolvers.get(resolver.namespace)!.destroy();\n\t\t}\n\t\tthis.resolvers.set(resolver.namespace, resolver);\n\t}\n\n\t/**\n\t * Unregister a resolver by namespace.\n\t * Calls destroy() and clears all cache entries with that namespace prefix.\n\t */\n\tunregister(namespace: string, queryClient?: QueryClient): void {\n\t\tconst resolver = this.resolvers.get(namespace);\n\t\tif (resolver) {\n\t\t\tresolver.destroy();\n\t\t\tthis.resolvers.delete(namespace);\n\t\t}\n\t\t// Clear all cache entries for this namespace via TanStack Query\n\t\t// Hierarchical keys: [\"osrs\"] clears all [\"osrs\", ...] queries\n\t\tif (queryClient) {\n\t\t\tqueryClient.removeQueries({ queryKey: [namespace] });\n\t\t}\n\t}\n\n\t/**\n\t * Run all registered resolvers' preflight checks against compiled bytecode.\n\t *\n\t * Returns the first AsyncCheckResult found, or null if all data is ready.\n\t * Short-circuits on first pending, subsequent pending ops will be\n\t * discovered on re-evaluation when the first resolves.\n\t */\n\tpreflightAll(tokens: Token[], bytecode: BytecodeProgram, packageId: string, signal: AbortSignal, queryClient: QueryClient): AsyncCheckResult | null {\n\t\tfor (const resolver of this.resolvers.values()) {\n\t\t\tif (!resolver.preflight) continue;\n\t\t\tconst result = resolver.preflight(tokens, bytecode, packageId, signal, queryClient);\n\t\t\tif (result) return result;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * Get a resolver by namespace.\n\t * Returns undefined if no resolver is registered for that namespace.\n\t */\n\tget(namespace: string): IAsyncResolver | undefined {\n\t\treturn this.resolvers.get(namespace);\n\t}\n\n\t/** Number of registered resolvers. */\n\tget size(): number {\n\t\treturn this.resolvers.size;\n\t}\n\n\t/**\n\t * Check if a resolver is registered for the given namespace.\n\t */\n\thas(namespace: string): boolean {\n\t\treturn this.resolvers.has(namespace);\n\t}\n\n\t/**\n\t * Clear all resolvers (document switch, engine reset).\n\t * Calls destroy() on each resolver and clears the map.\n\t */\n\tclear(): void {\n\t\tfor (const resolver of this.resolvers.values()) {\n\t\t\tresolver.destroy();\n\t\t}\n\t\tthis.resolvers.clear();\n\t}\n}\n"]}
@@ -0,0 +1,333 @@
1
+ 'use strict';
2
+
3
+ var chunkVEKIWQ3F_cjs = require('./chunk-VEKIWQ3F.cjs');
4
+ var chunkM4N5GA5Z_cjs = require('./chunk-M4N5GA5Z.cjs');
5
+
6
+ // src/uom/CurrencyExchange.ts
7
+ var CurrencyErrorCodes = {
8
+ /** Frankfurter's rates endpoint returned a non-OK HTTP status. */
9
+ API_ERROR: "CURRENCY_API_ERROR",
10
+ /** A requested currency/crypto code isn't in the fetched rate table, an unrecognized code, not an API failure. */
11
+ UNKNOWN_CODE: "UNKNOWN_CURRENCY_CODE",
12
+ /** CoinGecko's simple-price endpoint returned a non-OK HTTP status. */
13
+ CRYPTO_API_ERROR: "CRYPTO_PRICE_API_ERROR"
14
+ };
15
+ var _CurrencyExchangeService = class _CurrencyExchangeService {
16
+ constructor() {
17
+ /**
18
+ * Live rate tables cached from successful getRate() fetches, keyed by
19
+ * uppercase base currency. Each table holds every rate the API returned
20
+ * for that base (plus the base itself at 1), so any pair whose two codes
21
+ * appear in one fresh table can be served synchronously, including
22
+ * cross pairs via triangulation (EUR→GBP through a USD-base table).
23
+ * Stale tables are ignored, not evicted; the next successful fetch for
24
+ * the same base overwrites them.
25
+ */
26
+ this.baseTables = /* @__PURE__ */ new Map();
27
+ }
28
+ isCryptoCode(code) {
29
+ return code.toUpperCase() in _CurrencyExchangeService.CRYPTO_IDS;
30
+ }
31
+ /**
32
+ * Fetch the live exchange rate for converting 1 unit of `from` into `to`.
33
+ *
34
+ * Routes cryptocurrency codes (see {@link CRYPTO_IDS}) to CoinGecko and
35
+ * everything else to Frankfurter (ECB reference rates, fiat-only). On
36
+ * success, caches the whole returned rate table for `from` so subsequent
37
+ * lookups, including cross-pairs via triangulation, can be served
38
+ * synchronously by {@link getRateSync} within the freshness window.
39
+ *
40
+ * @throws If the currency code is unrecognized or the fetch fails/times out.
41
+ */
42
+ async getRate(from, to, signal) {
43
+ const fromUpper = from.toUpperCase();
44
+ const toUpper = to.toUpperCase();
45
+ if (this.isCryptoCode(fromUpper) || this.isCryptoCode(toUpper)) {
46
+ return this.getCryptoRate(fromUpper, toUpper, signal);
47
+ }
48
+ const { signal: fetchSignal, cleanup } = chunkVEKIWQ3F_cjs.createTimeoutSignal(
49
+ signal,
50
+ _CurrencyExchangeService.FETCH_TIMEOUT_MS,
51
+ "Currency API fetch"
52
+ );
53
+ try {
54
+ const response = await fetch(`https://api.frankfurter.dev/v2/rates?base=${fromUpper}`, { signal: fetchSignal });
55
+ if (!response.ok) throw chunkM4N5GA5Z_cjs.ErrorFactory.external(CurrencyErrorCodes.API_ERROR, `Currency API returned ${response.status}`, { status: response.status });
56
+ const data = await response.json();
57
+ const rates = Array.isArray(data) ? Object.fromEntries(
58
+ data.filter((entry) => {
59
+ if (typeof entry !== "object" || entry === null) return false;
60
+ const candidate = entry;
61
+ return typeof candidate.quote === "string" && typeof candidate.rate === "number";
62
+ }).map((entry) => [entry.quote.toUpperCase(), entry.rate])
63
+ ) : data.rates ?? {};
64
+ if (rates[toUpper] === void 0) throw chunkM4N5GA5Z_cjs.ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${toUpper}`, { code: toUpper });
65
+ this.baseTables.set(fromUpper, {
66
+ fetchedAt: Date.now(),
67
+ rates: { ...rates, [fromUpper]: 1 }
68
+ });
69
+ return rates[toUpper];
70
+ } finally {
71
+ cleanup();
72
+ }
73
+ }
74
+ /**
75
+ * Crypto rate fetch, routed through CoinGecko's no-auth simple-price
76
+ * endpoint instead of Frankfurter (fiat-only, has no BTC/ETH concept).
77
+ * Handles all three combinations, crypto→crypto, crypto→fiat
78
+ * fiat→crypto, via prices denominated in USD (or the target fiat
79
+ * directly, which CoinGecko's `vs_currencies` also accepts), then
80
+ * caches the result as a same-shaped base table so getRateSync /
81
+ * convertSync keep working unchanged for crypto pairs too.
82
+ */
83
+ async getCryptoRate(fromUpper, toUpper, signal) {
84
+ const { signal: fetchSignal, cleanup } = chunkVEKIWQ3F_cjs.createTimeoutSignal(
85
+ signal,
86
+ _CurrencyExchangeService.FETCH_TIMEOUT_MS,
87
+ "Crypto price API fetch"
88
+ );
89
+ try {
90
+ const fromIsCrypto = this.isCryptoCode(fromUpper);
91
+ const toIsCrypto = this.isCryptoCode(toUpper);
92
+ let rate;
93
+ if (fromIsCrypto && toIsCrypto) {
94
+ const fromId = _CurrencyExchangeService.CRYPTO_IDS[fromUpper];
95
+ const toId = _CurrencyExchangeService.CRYPTO_IDS[toUpper];
96
+ const data = await this.fetchCoinGeckoPrices([fromId, toId], "usd", fetchSignal);
97
+ const fromUsd = data[fromId]?.usd;
98
+ const toUsd = data[toId]?.usd;
99
+ if (typeof fromUsd !== "number") throw chunkM4N5GA5Z_cjs.ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${fromUpper}`, { code: fromUpper });
100
+ if (typeof toUsd !== "number") throw chunkM4N5GA5Z_cjs.ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${toUpper}`, { code: toUpper });
101
+ rate = fromUsd / toUsd;
102
+ } else if (fromIsCrypto) {
103
+ const fromId = _CurrencyExchangeService.CRYPTO_IDS[fromUpper];
104
+ const vs = toUpper.toLowerCase();
105
+ const data = await this.fetchCoinGeckoPrices([fromId], vs, fetchSignal);
106
+ const value = data[fromId]?.[vs];
107
+ if (typeof value !== "number") throw chunkM4N5GA5Z_cjs.ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${toUpper}`, { code: toUpper });
108
+ rate = value;
109
+ } else {
110
+ const toId = _CurrencyExchangeService.CRYPTO_IDS[toUpper];
111
+ const vs = fromUpper.toLowerCase();
112
+ const data = await this.fetchCoinGeckoPrices([toId], vs, fetchSignal);
113
+ const priceOfToInFrom = data[toId]?.[vs];
114
+ if (typeof priceOfToInFrom !== "number") throw chunkM4N5GA5Z_cjs.ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${fromUpper}`, { code: fromUpper });
115
+ rate = 1 / priceOfToInFrom;
116
+ }
117
+ this.baseTables.set(fromUpper, {
118
+ fetchedAt: Date.now(),
119
+ rates: { [fromUpper]: 1, [toUpper]: rate }
120
+ });
121
+ return rate;
122
+ } finally {
123
+ cleanup();
124
+ }
125
+ }
126
+ async fetchCoinGeckoPrices(ids, vsCurrency, signal) {
127
+ const url = `https://api.coingecko.com/api/v3/simple/price?ids=${ids.join(",")}&vs_currencies=${vsCurrency}`;
128
+ const response = await fetch(url, { signal });
129
+ if (!response.ok) throw chunkM4N5GA5Z_cjs.ErrorFactory.external(CurrencyErrorCodes.CRYPTO_API_ERROR, `Crypto price API returned ${response.status}`, { status: response.status });
130
+ return response.json();
131
+ }
132
+ /**
133
+ * Seed a base rate table without a network fetch.
134
+ *
135
+ * Intended for tests and for future user-provided offline rates
136
+ * production live data always comes from {@link getRate}. Seeded rates
137
+ * obey the same freshness window as fetched ones.
138
+ *
139
+ * @param base - Base currency code (e.g. "USD").
140
+ * @param rates - Map of currency code → rate relative to the base.
141
+ */
142
+ primeRates(base, rates) {
143
+ const baseUpper = base.toUpperCase();
144
+ this.baseTables.set(baseUpper, {
145
+ fetchedAt: Date.now(),
146
+ rates: { ...rates, [baseUpper]: 1 }
147
+ });
148
+ }
149
+ /**
150
+ * Drop every cached/primed rate table.
151
+ *
152
+ * Mainly for test isolation: {@link sharedCurrencyExchange} is a
153
+ * module-level singleton, so a rate primed or fetched by one test can
154
+ * silently leak into a later test in the same file. Also usable in
155
+ * production if a caller ever wants to force a full re-fetch.
156
+ */
157
+ clearRates() {
158
+ this.baseTables.clear();
159
+ }
160
+ /**
161
+ * Synchronous rate lookup: `1` for same-currency pairs, a cached LIVE
162
+ * rate if one was fetched within {@link RATE_FRESHNESS_MS}, otherwise
163
+ * `null`, callers fall through to the async fetch path and the
164
+ * expression shows Pending until real data arrives.
165
+ *
166
+ * There is deliberately no hardcoded fallback table: a stale made-up
167
+ * rate presented as a real conversion is worse than a Pending state.
168
+ */
169
+ getRateSync(from, to) {
170
+ const fromUpper = from.toUpperCase();
171
+ const toUpper = to.toUpperCase();
172
+ if (fromUpper === toUpper) {
173
+ return 1;
174
+ }
175
+ const now = Date.now();
176
+ for (const table of this.baseTables.values()) {
177
+ if (now - table.fetchedAt > _CurrencyExchangeService.RATE_FRESHNESS_MS) continue;
178
+ const fromRate = table.rates[fromUpper];
179
+ const toRate = table.rates[toUpper];
180
+ if (fromRate && toRate) {
181
+ return toRate / fromRate;
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+ /** Convert `value` from `from` to `to` using a freshly-fetched live rate (see {@link getRate}). */
187
+ async convert(value, from, to) {
188
+ const rate = await this.getRate(from, to);
189
+ return value * rate;
190
+ }
191
+ /**
192
+ * Get all currently cached fresh rates, keyed "FROM:TO".
193
+ * @returns Snapshot of fresh live rates, or null when none are cached.
194
+ */
195
+ getAllRates() {
196
+ const now = Date.now();
197
+ const snapshot = {};
198
+ let any = false;
199
+ for (const [base, table] of this.baseTables) {
200
+ if (now - table.fetchedAt > _CurrencyExchangeService.RATE_FRESHNESS_MS) continue;
201
+ for (const [code, rate] of Object.entries(table.rates)) {
202
+ snapshot[`${base}:${code}`] = rate;
203
+ any = true;
204
+ }
205
+ }
206
+ return any ? snapshot : null;
207
+ }
208
+ /**
209
+ * Check whether any fresh live rates are currently cached.
210
+ */
211
+ hasRates() {
212
+ return this.getAllRates() !== null;
213
+ }
214
+ /**
215
+ * Synchronous conversion using cached rates only
216
+ * Returns null if rate not in cache
217
+ */
218
+ convertSync(value, from, to) {
219
+ const rate = this.getRateSync(from, to);
220
+ if (rate === null) return null;
221
+ return value * rate;
222
+ }
223
+ // ------------------------------------------------------------------------
224
+ // CURRENCY VALIDATION
225
+ // ------------------------------------------------------------------------
226
+ /**
227
+ * Check whether `code` is a recognized currency code (fiat or the
228
+ * cryptocurrencies in {@link CRYPTO_IDS}), a fixed allowlist, not a
229
+ * live lookup against any API.
230
+ */
231
+ isCurrency(code) {
232
+ const knownCurrencies = [
233
+ "USD",
234
+ "EUR",
235
+ "GBP",
236
+ "JPY",
237
+ "AUD",
238
+ "CAD",
239
+ "CHF",
240
+ "CNY",
241
+ "SEK",
242
+ "NOK",
243
+ "DKK",
244
+ "NZD",
245
+ "KRW",
246
+ "SGD",
247
+ "HKD",
248
+ "INR",
249
+ "BRL",
250
+ "ZAR",
251
+ "MXN",
252
+ "RUB",
253
+ "TRY",
254
+ "SAR",
255
+ "AED",
256
+ "ILS",
257
+ "PLN",
258
+ "CZK",
259
+ "HUF",
260
+ "THB",
261
+ "IDR",
262
+ "MYR",
263
+ "PHP",
264
+ "CLP",
265
+ "COP",
266
+ "ARS",
267
+ "NGN",
268
+ "EGP",
269
+ "PKR",
270
+ "BDT",
271
+ "VND",
272
+ "KES",
273
+ "MAD",
274
+ "QAR",
275
+ "KWD",
276
+ "OMR",
277
+ "BHD",
278
+ "JOD",
279
+ "BTC",
280
+ "ETH",
281
+ "SOL",
282
+ "XRP",
283
+ "ADA",
284
+ "DOGE",
285
+ "DOT"
286
+ ];
287
+ return knownCurrencies.includes(code.toUpperCase());
288
+ }
289
+ };
290
+ /**
291
+ * How long a fetched rate may be served synchronously by getRateSync().
292
+ * Beyond this window callers fall through to the async path (expression
293
+ * shows Pending until the fetch lands).
294
+ */
295
+ _CurrencyExchangeService.RATE_FRESHNESS_MS = 15 * 60 * 1e3;
296
+ /**
297
+ * Ticker → CoinGecko coin id, for the cryptocurrencies `isCurrency()`
298
+ * recognizes. Frankfurter (the fiat rate source below) is ECB reference
299
+ * rates only and has no concept of BTC/ETH/etc, routing a crypto code
300
+ * through it as `base=BTC` fails outright, which is why crypto pairs
301
+ * previously never resolved (see CurrencyAsyncResolver/VM.ts's ADD
302
+ * handling for the two bugs that let that failure pass silently instead
303
+ * of surfacing as a real fetch).
304
+ */
305
+ _CurrencyExchangeService.CRYPTO_IDS = {
306
+ BTC: "bitcoin",
307
+ ETH: "ethereum",
308
+ SOL: "solana",
309
+ XRP: "ripple",
310
+ ADA: "cardano",
311
+ DOGE: "dogecoin",
312
+ DOT: "polkadot"
313
+ };
314
+ // ------------------------------------------------------------------------
315
+ // RATE FETCHING
316
+ // ------------------------------------------------------------------------
317
+ /**
318
+ * Timeout (ms) for currency exchange rate fetches.
319
+ *
320
+ * If the frankfurter API doesn't respond within this window, the fetch
321
+ * is aborted, preventing indefinite "Pending" states in the playground
322
+ * and Obsidian plugin when the exchange rate API is unreachable.
323
+ */
324
+ _CurrencyExchangeService.FETCH_TIMEOUT_MS = 1e4;
325
+ var CurrencyExchangeService = _CurrencyExchangeService;
326
+ var currencyExchangeService = new CurrencyExchangeService();
327
+ var sharedCurrencyExchange = currencyExchangeService;
328
+
329
+ exports.CurrencyExchangeService = CurrencyExchangeService;
330
+ exports.currencyExchangeService = currencyExchangeService;
331
+ exports.sharedCurrencyExchange = sharedCurrencyExchange;
332
+ //# sourceMappingURL=chunk-XJCMXB2E.cjs.map
333
+ //# sourceMappingURL=chunk-XJCMXB2E.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/uom/CurrencyExchange.ts"],"names":["createTimeoutSignal","ErrorFactory"],"mappings":"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","file":"chunk-XJCMXB2E.cjs","sourcesContent":["/**\n * Production-grade currency exchange service with scalable architecture\n * Integrates with DataQueryService for worker-based execution\n */\n\nimport { createTimeoutSignal } from \"@solve-js/utilities/TimeoutSignal\";\nimport { ErrorFactory } from \"@solve-js/errors/UnifiedErrorFramework\";\n\n/**\n * Error codes for this service. Co-located rather than unioned into\n * `errors/ErrorCode.ts`'s core catalog (that catalog is scoped to the\n * parser/VM/engine/errors/config/lexer layers, not yet the ~17 domain\n * packages. See that file's module doc for the intended per-package\n * pattern this follows).\n */\nexport const CurrencyErrorCodes = {\n /** Frankfurter's rates endpoint returned a non-OK HTTP status. */\n API_ERROR: \"CURRENCY_API_ERROR\",\n /** A requested currency/crypto code isn't in the fetched rate table, an unrecognized code, not an API failure. */\n UNKNOWN_CODE: \"UNKNOWN_CURRENCY_CODE\",\n /** CoinGecko's simple-price endpoint returned a non-OK HTTP status. */\n CRYPTO_API_ERROR: \"CRYPTO_PRICE_API_ERROR\",\n} as const;\n\n// ============================================================================\n// CURRENCY EXCHANGE SERVICE\n// ============================================================================\n\n/**\n * Caches exchange rates fetched from an external source.\n *\n * Rates are global market data rather than per-engine configuration, which is\n * why one instance is shared. Two engines with private copies would fetch the\n * same endpoint twice and could disagree about one pair at one moment. See\n * `engine/EngineContext.ts`.\n */\nexport class CurrencyExchangeService {\n /**\n * Live rate tables cached from successful getRate() fetches, keyed by\n * uppercase base currency. Each table holds every rate the API returned\n * for that base (plus the base itself at 1), so any pair whose two codes\n * appear in one fresh table can be served synchronously, including\n * cross pairs via triangulation (EUR→GBP through a USD-base table).\n * Stale tables are ignored, not evicted; the next successful fetch for\n * the same base overwrites them.\n */\n private baseTables: Map<string, { fetchedAt: number; rates: Record<string, number> }> = new Map();\n\n /**\n * How long a fetched rate may be served synchronously by getRateSync().\n * Beyond this window callers fall through to the async path (expression\n * shows Pending until the fetch lands).\n */\n private static readonly RATE_FRESHNESS_MS = 15 * 60 * 1000;\n\n /**\n * Ticker → CoinGecko coin id, for the cryptocurrencies `isCurrency()`\n * recognizes. Frankfurter (the fiat rate source below) is ECB reference\n * rates only and has no concept of BTC/ETH/etc, routing a crypto code\n * through it as `base=BTC` fails outright, which is why crypto pairs\n * previously never resolved (see CurrencyAsyncResolver/VM.ts's ADD\n * handling for the two bugs that let that failure pass silently instead\n * of surfacing as a real fetch).\n */\n private static readonly CRYPTO_IDS: Record<string, string> = {\n BTC: \"bitcoin\", ETH: \"ethereum\", SOL: \"solana\", XRP: \"ripple\",\n ADA: \"cardano\", DOGE: \"dogecoin\", DOT: \"polkadot\",\n };\n\n private isCryptoCode(code: string): boolean {\n return code.toUpperCase() in CurrencyExchangeService.CRYPTO_IDS;\n }\n\n constructor() {}\n\n // ------------------------------------------------------------------------\n // RATE FETCHING\n // ------------------------------------------------------------------------\n\n /**\n * Timeout (ms) for currency exchange rate fetches.\n *\n * If the frankfurter API doesn't respond within this window, the fetch\n * is aborted, preventing indefinite \"Pending\" states in the playground\n * and Obsidian plugin when the exchange rate API is unreachable.\n */\n private static readonly FETCH_TIMEOUT_MS = 10_000;\n\n /**\n * Fetch the live exchange rate for converting 1 unit of `from` into `to`.\n *\n * Routes cryptocurrency codes (see {@link CRYPTO_IDS}) to CoinGecko and\n * everything else to Frankfurter (ECB reference rates, fiat-only). On\n * success, caches the whole returned rate table for `from` so subsequent\n * lookups, including cross-pairs via triangulation, can be served\n * synchronously by {@link getRateSync} within the freshness window.\n *\n * @throws If the currency code is unrecognized or the fetch fails/times out.\n */\n async getRate(from: string, to: string, signal?: AbortSignal): Promise<number> {\n const fromUpper = from.toUpperCase();\n const toUpper = to.toUpperCase();\n\n if (this.isCryptoCode(fromUpper) || this.isCryptoCode(toUpper)) {\n return this.getCryptoRate(fromUpper, toUpper, signal);\n }\n\n // Combine the caller's optional abort signal with a hard timeout so a\n // hanging currency API never blocks re-evaluation indefinitely.\n const { signal: fetchSignal, cleanup } = createTimeoutSignal(\n signal,\n CurrencyExchangeService.FETCH_TIMEOUT_MS,\n \"Currency API fetch\",\n );\n\n try {\n const response = await fetch(`https://api.frankfurter.dev/v2/rates?base=${fromUpper}`, { signal: fetchSignal });\n if (!response.ok) throw ErrorFactory.external(CurrencyErrorCodes.API_ERROR, `Currency API returned ${response.status}`, { status: response.status });\n const data = await response.json();\n // The v2 endpoint returns a flat array of { date, base, quote, rate }\n // entries (one per target currency) rather than the classic v1 shape\n // { base, date, rates: { CODE: rate } }. Reading data.rates against\n // the real response is always undefined, so every conversion used to\n // throw \"Unknown currency\" no matter which currencies were requested.\n // Accept both shapes so a future API revision back to the object form\n // doesn't silently break this again.\n const rates: Record<string, number> = Array.isArray(data)\n ? Object.fromEntries(\n data\n .filter((entry: unknown): entry is { quote: string; rate: number } => {\n if (typeof entry !== \"object\" || entry === null) return false;\n const candidate = entry as { quote?: unknown; rate?: unknown };\n return typeof candidate.quote === \"string\" && typeof candidate.rate === \"number\";\n })\n .map((entry: { quote: string; rate: number }) => [entry.quote.toUpperCase(), entry.rate])\n )\n : (data.rates ?? {});\n if (rates[toUpper] === undefined) throw ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${toUpper}`, { code: toUpper });\n\n // The API returns ALL rates for the base currency, cache the whole\n // table so subsequent conversions (including cross pairs via\n // triangulation) resolve synchronously within the freshness window\n // instead of going Pending again.\n this.baseTables.set(fromUpper, {\n fetchedAt: Date.now(),\n rates: { ...rates, [fromUpper]: 1 },\n });\n\n return rates[toUpper];\n } finally {\n cleanup();\n }\n }\n\n /**\n * Crypto rate fetch, routed through CoinGecko's no-auth simple-price\n * endpoint instead of Frankfurter (fiat-only, has no BTC/ETH concept).\n * Handles all three combinations, crypto→crypto, crypto→fiat\n * fiat→crypto, via prices denominated in USD (or the target fiat\n * directly, which CoinGecko's `vs_currencies` also accepts), then\n * caches the result as a same-shaped base table so getRateSync /\n * convertSync keep working unchanged for crypto pairs too.\n */\n private async getCryptoRate(fromUpper: string, toUpper: string, signal?: AbortSignal): Promise<number> {\n const { signal: fetchSignal, cleanup } = createTimeoutSignal(\n signal,\n CurrencyExchangeService.FETCH_TIMEOUT_MS,\n \"Crypto price API fetch\",\n );\n\n try {\n const fromIsCrypto = this.isCryptoCode(fromUpper);\n const toIsCrypto = this.isCryptoCode(toUpper);\n let rate: number;\n\n if (fromIsCrypto && toIsCrypto) {\n const fromId = CurrencyExchangeService.CRYPTO_IDS[fromUpper];\n const toId = CurrencyExchangeService.CRYPTO_IDS[toUpper];\n const data = await this.fetchCoinGeckoPrices([fromId, toId], \"usd\", fetchSignal);\n const fromUsd = data[fromId]?.usd;\n const toUsd = data[toId]?.usd;\n if (typeof fromUsd !== \"number\") throw ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${fromUpper}`, { code: fromUpper });\n if (typeof toUsd !== \"number\") throw ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${toUpper}`, { code: toUpper });\n rate = fromUsd / toUsd;\n } else if (fromIsCrypto) {\n const fromId = CurrencyExchangeService.CRYPTO_IDS[fromUpper];\n const vs = toUpper.toLowerCase();\n const data = await this.fetchCoinGeckoPrices([fromId], vs, fetchSignal);\n const value = data[fromId]?.[vs];\n if (typeof value !== \"number\") throw ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${toUpper}`, { code: toUpper });\n rate = value;\n } else {\n const toId = CurrencyExchangeService.CRYPTO_IDS[toUpper];\n const vs = fromUpper.toLowerCase();\n const data = await this.fetchCoinGeckoPrices([toId], vs, fetchSignal);\n const priceOfToInFrom = data[toId]?.[vs];\n if (typeof priceOfToInFrom !== \"number\") throw ErrorFactory.validation(CurrencyErrorCodes.UNKNOWN_CODE, `Unknown currency: ${fromUpper}`, { code: fromUpper });\n rate = 1 / priceOfToInFrom;\n }\n\n // Cache as a single-pair base table, same shape Frankfurter fetches\n // produce, so getRateSync/convertSync's triangulation logic doesn't\n // need to know or care which source a rate came from.\n this.baseTables.set(fromUpper, {\n fetchedAt: Date.now(),\n rates: { [fromUpper]: 1, [toUpper]: rate },\n });\n\n return rate;\n } finally {\n cleanup();\n }\n }\n\n private async fetchCoinGeckoPrices(ids: string[], vsCurrency: string, signal: AbortSignal): Promise<Record<string, Record<string, number>>> {\n const url = `https://api.coingecko.com/api/v3/simple/price?ids=${ids.join(\",\")}&vs_currencies=${vsCurrency}`;\n const response = await fetch(url, { signal });\n if (!response.ok) throw ErrorFactory.external(CurrencyErrorCodes.CRYPTO_API_ERROR, `Crypto price API returned ${response.status}`, { status: response.status });\n return response.json();\n }\n\n /**\n * Seed a base rate table without a network fetch.\n *\n * Intended for tests and for future user-provided offline rates\n * production live data always comes from {@link getRate}. Seeded rates\n * obey the same freshness window as fetched ones.\n *\n * @param base - Base currency code (e.g. \"USD\").\n * @param rates - Map of currency code → rate relative to the base.\n */\n primeRates(base: string, rates: Record<string, number>): void {\n const baseUpper = base.toUpperCase();\n this.baseTables.set(baseUpper, {\n fetchedAt: Date.now(),\n rates: { ...rates, [baseUpper]: 1 },\n });\n }\n\n /**\n * Drop every cached/primed rate table.\n *\n * Mainly for test isolation: {@link sharedCurrencyExchange} is a\n * module-level singleton, so a rate primed or fetched by one test can\n * silently leak into a later test in the same file. Also usable in\n * production if a caller ever wants to force a full re-fetch.\n */\n clearRates(): void {\n this.baseTables.clear();\n }\n\n /**\n * Synchronous rate lookup: `1` for same-currency pairs, a cached LIVE\n * rate if one was fetched within {@link RATE_FRESHNESS_MS}, otherwise\n * `null`, callers fall through to the async fetch path and the\n * expression shows Pending until real data arrives.\n *\n * There is deliberately no hardcoded fallback table: a stale made-up\n * rate presented as a real conversion is worse than a Pending state.\n */\n getRateSync(from: string, to: string): number | null {\n const fromUpper = from.toUpperCase();\n const toUpper = to.toUpperCase();\n if (fromUpper === toUpper) {\n return 1;\n }\n const now = Date.now();\n for (const table of this.baseTables.values()) {\n if (now - table.fetchedAt > CurrencyExchangeService.RATE_FRESHNESS_MS) continue;\n const fromRate = table.rates[fromUpper];\n const toRate = table.rates[toUpper];\n if (fromRate && toRate) {\n return toRate / fromRate;\n }\n }\n return null;\n }\n\n /** Convert `value` from `from` to `to` using a freshly-fetched live rate (see {@link getRate}). */\n async convert(value: number, from: string, to: string): Promise<number> {\n const rate = await this.getRate(from, to);\n return value * rate;\n }\n\n /**\n * Get all currently cached fresh rates, keyed \"FROM:TO\".\n * @returns Snapshot of fresh live rates, or null when none are cached.\n */\n getAllRates(): Record<string, number> | null {\n const now = Date.now();\n const snapshot: Record<string, number> = {};\n let any = false;\n for (const [base, table] of this.baseTables) {\n if (now - table.fetchedAt > CurrencyExchangeService.RATE_FRESHNESS_MS) continue;\n for (const [code, rate] of Object.entries(table.rates)) {\n snapshot[`${base}:${code}`] = rate;\n any = true;\n }\n }\n return any ? snapshot : null;\n }\n\n /**\n * Check whether any fresh live rates are currently cached.\n */\n hasRates(): boolean {\n return this.getAllRates() !== null;\n }\n\n /**\n * Synchronous conversion using cached rates only\n * Returns null if rate not in cache\n */\n convertSync(value: number, from: string, to: string): number | null {\n const rate = this.getRateSync(from, to);\n if (rate === null) return null;\n return value * rate;\n }\n\n // ------------------------------------------------------------------------\n // CURRENCY VALIDATION\n // ------------------------------------------------------------------------\n\n /**\n * Check whether `code` is a recognized currency code (fiat or the\n * cryptocurrencies in {@link CRYPTO_IDS}), a fixed allowlist, not a\n * live lookup against any API.\n */\n isCurrency(code: string): boolean {\n // Check against known currency codes\n const knownCurrencies = [\n \"USD\", \"EUR\", \"GBP\", \"JPY\", \"AUD\", \"CAD\", \"CHF\", \"CNY\", \"SEK\", \"NOK\",\n \"DKK\", \"NZD\", \"KRW\", \"SGD\", \"HKD\", \"INR\", \"BRL\", \"ZAR\", \"MXN\", \"RUB\",\n \"TRY\", \"SAR\", \"AED\", \"ILS\", \"PLN\", \"CZK\", \"HUF\", \"THB\", \"IDR\", \"MYR\",\n \"PHP\", \"CLP\", \"COP\", \"ARS\", \"NGN\", \"EGP\", \"PKR\", \"BDT\", \"VND\", \"KES\",\n \"MAD\", \"QAR\", \"KWD\", \"OMR\", \"BHD\", \"JOD\",\n \"BTC\", \"ETH\", \"SOL\", \"XRP\", \"ADA\", \"DOGE\", \"DOT\",\n ];\n return knownCurrencies.includes(code.toUpperCase());\n }\n\n}\n\n// ============================================================================\n// SINGLETON INSTANCE\n// ============================================================================\n\n/** The shared rate cache. See {@link CurrencyExchangeService} for why it is shared. */\nexport const currencyExchangeService = new CurrencyExchangeService();\n\n// Export for backward compatibility\n/** Alias for {@link currencyExchangeService}, kept for older imports. */\nexport const sharedCurrencyExchange = currencyExchangeService;\n\n// Default export\nexport default currencyExchangeService;\n"]}