@dxos/app-framework 0.8.4-main.c85a9c8dae → 0.8.4-main.cb12b3f963

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 (325) hide show
  1. package/dist/lib/browser/{capability-7RLVE42K.mjs → capability-5RRH3WIB.mjs} +11 -10
  2. package/dist/lib/browser/capability-5RRH3WIB.mjs.map +7 -0
  3. package/dist/lib/browser/{capability-2GL5JAGJ.mjs → capability-LUKGKUQH.mjs} +10 -9
  4. package/dist/lib/browser/{chunk-5RJNZV7K.mjs → chunk-23D4SJUE.mjs} +11 -13
  5. package/dist/lib/browser/{chunk-5RJNZV7K.mjs.map → chunk-23D4SJUE.mjs.map} +3 -3
  6. package/dist/lib/browser/{chunk-YNFPIQGB.mjs → chunk-3JWJXGLK.mjs} +5 -2
  7. package/dist/lib/browser/chunk-3JWJXGLK.mjs.map +7 -0
  8. package/dist/lib/browser/chunk-45CHLTBV.mjs +34 -0
  9. package/dist/lib/browser/chunk-45CHLTBV.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-PKQT6C53.mjs → chunk-66IXTIVK.mjs} +3 -2
  11. package/dist/lib/browser/chunk-66IXTIVK.mjs.map +7 -0
  12. package/dist/lib/browser/chunk-CZ4BIAHH.mjs +422 -0
  13. package/dist/lib/browser/chunk-CZ4BIAHH.mjs.map +7 -0
  14. package/dist/lib/browser/chunk-FJ4765WW.mjs +8 -0
  15. package/dist/lib/browser/{chunk-FHQTHCX7.mjs.map → chunk-FJ4765WW.mjs.map} +3 -3
  16. package/dist/lib/browser/chunk-FO3IYSLV.mjs +68 -0
  17. package/dist/lib/browser/chunk-FO3IYSLV.mjs.map +7 -0
  18. package/dist/lib/browser/chunk-MX5DKEJH.mjs +584 -0
  19. package/dist/lib/browser/chunk-MX5DKEJH.mjs.map +7 -0
  20. package/dist/lib/browser/chunk-NBXPP7JR.mjs +1174 -0
  21. package/dist/lib/browser/chunk-NBXPP7JR.mjs.map +7 -0
  22. package/dist/lib/browser/chunk-PC4NOADA.mjs +471 -0
  23. package/dist/lib/browser/chunk-PC4NOADA.mjs.map +7 -0
  24. package/dist/lib/browser/{chunk-REORGDJT.mjs → chunk-WBHCSOBW.mjs} +18 -18
  25. package/dist/lib/browser/chunk-WBHCSOBW.mjs.map +7 -0
  26. package/dist/lib/browser/{chunk-FNKT2QQ2.mjs → chunk-Z55LVAGN.mjs} +85 -17
  27. package/dist/lib/browser/chunk-Z55LVAGN.mjs.map +7 -0
  28. package/dist/lib/browser/{chunk-ZRWBPIZG.mjs → chunk-ZGJAZSNE.mjs} +9 -37
  29. package/dist/lib/browser/chunk-ZGJAZSNE.mjs.map +7 -0
  30. package/dist/lib/browser/cli/index.mjs +16 -29
  31. package/dist/lib/browser/cli/index.mjs.map +3 -3
  32. package/dist/lib/browser/common/activation-events.mjs +9 -8
  33. package/dist/lib/browser/common/capabilities.mjs +9 -8
  34. package/dist/lib/browser/core/activation-event.mjs +1 -1
  35. package/dist/lib/browser/core/capability.mjs +3 -1
  36. package/dist/lib/browser/core/plugin-manager.mjs +8 -4
  37. package/dist/lib/browser/core/plugin.mjs +14 -4
  38. package/dist/lib/browser/core/url-loader.mjs +24 -0
  39. package/dist/lib/browser/index.mjs +37 -22
  40. package/dist/lib/browser/index.mjs.map +3 -3
  41. package/dist/lib/browser/{invoker-capability-BNLVNYHU.mjs → invoker-capability-K4GHUFXF.mjs} +22 -14
  42. package/dist/lib/browser/invoker-capability-K4GHUFXF.mjs.map +7 -0
  43. package/dist/lib/browser/meta.json +1 -1
  44. package/dist/lib/browser/testing/index.mjs +184 -49
  45. package/dist/lib/browser/testing/index.mjs.map +4 -4
  46. package/dist/lib/browser/testing/react.mjs +78 -0
  47. package/dist/lib/browser/testing/react.mjs.map +7 -0
  48. package/dist/lib/browser/ui/index.mjs +18 -15
  49. package/dist/lib/node-esm/{capability-CHIMU6LX.mjs → capability-FCGZVIEG.mjs} +10 -9
  50. package/dist/lib/{browser/capability-2GL5JAGJ.mjs.map → node-esm/capability-FCGZVIEG.mjs.map} +1 -1
  51. package/dist/lib/node-esm/{capability-EVZK4REM.mjs → capability-JOIQ2MQE.mjs} +11 -10
  52. package/dist/lib/node-esm/capability-JOIQ2MQE.mjs.map +7 -0
  53. package/dist/lib/node-esm/{chunk-UEWJDI2L.mjs → chunk-37Z53PXZ.mjs} +2 -2
  54. package/dist/lib/node-esm/{chunk-UEWJDI2L.mjs.map → chunk-37Z53PXZ.mjs.map} +3 -3
  55. package/dist/lib/node-esm/chunk-42J2ZUQQ.mjs +472 -0
  56. package/dist/lib/node-esm/chunk-42J2ZUQQ.mjs.map +7 -0
  57. package/dist/lib/node-esm/chunk-6XW6LET6.mjs +35 -0
  58. package/dist/lib/node-esm/chunk-6XW6LET6.mjs.map +7 -0
  59. package/dist/lib/node-esm/{chunk-2A4PRBIX.mjs → chunk-D347W3KO.mjs} +9 -37
  60. package/dist/lib/node-esm/chunk-D347W3KO.mjs.map +7 -0
  61. package/dist/lib/node-esm/{chunk-CJCQS2YL.mjs → chunk-HTBJU5FX.mjs} +85 -17
  62. package/dist/lib/node-esm/chunk-HTBJU5FX.mjs.map +7 -0
  63. package/dist/lib/node-esm/chunk-M3HKPRPO.mjs +423 -0
  64. package/dist/lib/node-esm/chunk-M3HKPRPO.mjs.map +7 -0
  65. package/dist/lib/node-esm/chunk-MUVUQC3G.mjs +1175 -0
  66. package/dist/lib/node-esm/chunk-MUVUQC3G.mjs.map +7 -0
  67. package/dist/lib/node-esm/{chunk-VUIUFIGT.mjs → chunk-SBS2YMPT.mjs} +11 -13
  68. package/dist/lib/node-esm/{chunk-VUIUFIGT.mjs.map → chunk-SBS2YMPT.mjs.map} +3 -3
  69. package/dist/lib/node-esm/{chunk-SB5ODNPX.mjs → chunk-SDJ4B2LU.mjs} +5 -2
  70. package/dist/lib/node-esm/chunk-SDJ4B2LU.mjs.map +7 -0
  71. package/dist/lib/node-esm/{chunk-UFW652GS.mjs → chunk-WFSRZKBP.mjs} +18 -18
  72. package/dist/lib/node-esm/chunk-WFSRZKBP.mjs.map +7 -0
  73. package/dist/lib/node-esm/chunk-WK7OIQKI.mjs +70 -0
  74. package/dist/lib/node-esm/chunk-WK7OIQKI.mjs.map +7 -0
  75. package/dist/lib/node-esm/chunk-WKTLE7MG.mjs +585 -0
  76. package/dist/lib/node-esm/chunk-WKTLE7MG.mjs.map +7 -0
  77. package/dist/lib/node-esm/{chunk-7OWSHPYK.mjs → chunk-XOCUANHO.mjs} +3 -2
  78. package/dist/lib/node-esm/chunk-XOCUANHO.mjs.map +7 -0
  79. package/dist/lib/node-esm/cli/index.mjs +16 -29
  80. package/dist/lib/node-esm/cli/index.mjs.map +3 -3
  81. package/dist/lib/node-esm/common/activation-events.mjs +9 -8
  82. package/dist/lib/node-esm/common/capabilities.mjs +9 -8
  83. package/dist/lib/node-esm/core/activation-event.mjs +1 -1
  84. package/dist/lib/node-esm/core/capability.mjs +3 -1
  85. package/dist/lib/node-esm/core/plugin-manager.mjs +8 -4
  86. package/dist/lib/node-esm/core/plugin.mjs +14 -4
  87. package/dist/lib/node-esm/core/url-loader.mjs +25 -0
  88. package/dist/lib/node-esm/index.mjs +37 -22
  89. package/dist/lib/node-esm/index.mjs.map +3 -3
  90. package/dist/lib/node-esm/{invoker-capability-VF6SP44V.mjs → invoker-capability-XEPW5LMJ.mjs} +22 -14
  91. package/dist/lib/node-esm/invoker-capability-XEPW5LMJ.mjs.map +7 -0
  92. package/dist/lib/node-esm/meta.json +1 -1
  93. package/dist/lib/node-esm/testing/index.mjs +184 -49
  94. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  95. package/dist/lib/node-esm/testing/react.mjs +79 -0
  96. package/dist/lib/node-esm/testing/react.mjs.map +7 -0
  97. package/dist/lib/node-esm/ui/index.mjs +18 -15
  98. package/dist/plugin/node-esm/index.mjs +875 -0
  99. package/dist/plugin/node-esm/index.mjs.map +7 -0
  100. package/dist/plugin/node-esm/meta.json +1 -0
  101. package/dist/types/src/common/activation-events.d.ts +1 -1
  102. package/dist/types/src/common/activation-events.d.ts.map +1 -1
  103. package/dist/types/src/common/annotations.d.ts +1 -0
  104. package/dist/types/src/common/annotations.d.ts.map +1 -0
  105. package/dist/types/src/common/capabilities.d.ts +4 -8
  106. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  107. package/dist/types/src/common/operations.d.ts +8 -22
  108. package/dist/types/src/common/operations.d.ts.map +1 -1
  109. package/dist/types/src/core/activation-event.d.ts +5 -5
  110. package/dist/types/src/core/activation-event.d.ts.map +1 -1
  111. package/dist/types/src/core/capability-manager.d.ts +5 -0
  112. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  113. package/dist/types/src/core/capability.d.ts +8 -2
  114. package/dist/types/src/core/capability.d.ts.map +1 -1
  115. package/dist/types/src/core/edge-registry-plugin-provider.d.ts +30 -0
  116. package/dist/types/src/core/edge-registry-plugin-provider.d.ts.map +1 -0
  117. package/dist/types/src/core/index.d.ts +6 -0
  118. package/dist/types/src/core/index.d.ts.map +1 -1
  119. package/dist/types/src/core/plugin-asset-cache.d.ts +71 -0
  120. package/dist/types/src/core/plugin-asset-cache.d.ts.map +1 -0
  121. package/dist/types/src/core/plugin-manager.d.ts +177 -4
  122. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  123. package/dist/types/src/core/plugin-manifest.d.ts +101 -0
  124. package/dist/types/src/core/plugin-manifest.d.ts.map +1 -0
  125. package/dist/types/src/core/plugin-manifest.test.d.ts +2 -0
  126. package/dist/types/src/core/plugin-manifest.test.d.ts.map +1 -0
  127. package/dist/types/src/core/plugin.d.ts +113 -7
  128. package/dist/types/src/core/plugin.d.ts.map +1 -1
  129. package/dist/types/src/core/registry.d.ts +101 -0
  130. package/dist/types/src/core/registry.d.ts.map +1 -0
  131. package/dist/types/src/core/url-loader.d.ts +127 -0
  132. package/dist/types/src/core/url-loader.d.ts.map +1 -0
  133. package/dist/types/src/core/url-loader.test.d.ts +2 -0
  134. package/dist/types/src/core/url-loader.test.d.ts.map +1 -0
  135. package/dist/types/src/helpers.d.ts.map +1 -1
  136. package/dist/types/src/plugin-operation/OperationPlugin.d.ts.map +1 -1
  137. package/dist/types/src/plugin-operation/history/capability.d.ts.map +1 -1
  138. package/dist/types/src/plugin-operation/history/errors.d.ts +30 -3
  139. package/dist/types/src/plugin-operation/history/errors.d.ts.map +1 -1
  140. package/dist/types/src/plugin-operation/history/history-tracker.d.ts +1 -1
  141. package/dist/types/src/plugin-operation/history/history-tracker.d.ts.map +1 -1
  142. package/dist/types/src/plugin-operation/history/types.d.ts +1 -1
  143. package/dist/types/src/plugin-operation/history/types.d.ts.map +1 -1
  144. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts +1 -1
  145. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts.map +1 -1
  146. package/dist/types/src/plugin-operation/history/undo-registry.d.ts +1 -1
  147. package/dist/types/src/plugin-operation/history/undo-registry.d.ts.map +1 -1
  148. package/dist/types/src/plugin-operation/invoker-capability.d.ts +1 -1
  149. package/dist/types/src/plugin-operation/invoker-capability.d.ts.map +1 -1
  150. package/dist/types/src/plugin-operation/testing.d.ts +27 -77
  151. package/dist/types/src/plugin-operation/testing.d.ts.map +1 -1
  152. package/dist/types/src/plugin-runtime/RuntimePlugin.d.ts.map +1 -1
  153. package/dist/types/src/plugin-runtime/capability.d.ts +1 -1
  154. package/dist/types/src/plugin-runtime/capability.d.ts.map +1 -1
  155. package/dist/types/src/testing/harness.d.ts +67 -0
  156. package/dist/types/src/testing/harness.d.ts.map +1 -0
  157. package/dist/types/src/testing/index.d.ts +1 -0
  158. package/dist/types/src/testing/index.d.ts.map +1 -1
  159. package/dist/types/src/testing/react.d.ts +27 -0
  160. package/dist/types/src/testing/react.d.ts.map +1 -0
  161. package/dist/types/src/testing/react.test.d.ts +2 -0
  162. package/dist/types/src/testing/react.test.d.ts.map +1 -0
  163. package/dist/types/src/testing/service.d.ts.map +1 -1
  164. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  165. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  166. package/dist/types/src/ui/components/App/App.d.ts +3 -2
  167. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  168. package/dist/types/src/ui/components/App/App.stories.d.ts +2 -2
  169. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  170. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +64 -0
  171. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +1 -0
  172. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +19 -0
  173. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +1 -0
  174. package/dist/types/src/ui/components/Placeholder/index.d.ts +2 -0
  175. package/dist/types/src/ui/components/Placeholder/index.d.ts.map +1 -0
  176. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
  177. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +16 -4
  178. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  179. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  180. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts +48 -0
  181. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -0
  182. package/dist/types/src/ui/components/Surface/index.d.ts +22 -6
  183. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  184. package/dist/types/src/ui/components/Surface/types.d.ts +110 -9
  185. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  186. package/dist/types/src/ui/components/Surface/types.test.d.ts +2 -0
  187. package/dist/types/src/ui/components/Surface/types.test.d.ts.map +1 -0
  188. package/dist/types/src/ui/components/index.d.ts +1 -0
  189. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  190. package/dist/types/src/ui/hooks/index.d.ts +0 -1
  191. package/dist/types/src/ui/hooks/index.d.ts.map +1 -1
  192. package/dist/types/src/ui/hooks/useApp.d.ts +43 -4
  193. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  194. package/dist/types/src/ui/hooks/useApp.test.d.ts +2 -0
  195. package/dist/types/src/ui/hooks/useApp.test.d.ts.map +1 -0
  196. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  197. package/dist/types/src/ui/hooks/useLoading.d.ts.map +1 -1
  198. package/dist/types/src/ui/hooks/useSettingsState.d.ts.map +1 -1
  199. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +34 -0
  200. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +1 -0
  201. package/dist/types/src/vite-plugin/boot-loader/index.d.ts +52 -0
  202. package/dist/types/src/vite-plugin/boot-loader/index.d.ts.map +1 -0
  203. package/dist/types/src/vite-plugin/composer/index.d.ts +34 -0
  204. package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -0
  205. package/dist/types/src/vite-plugin/import-map/index.d.ts +28 -0
  206. package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -0
  207. package/dist/types/src/vite-plugin/index.d.ts +5 -0
  208. package/dist/types/src/vite-plugin/index.d.ts.map +1 -0
  209. package/dist/types/src/vite-plugin/manifest.d.ts +41 -0
  210. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -0
  211. package/dist/types/src/vite-plugin/manifest.test.d.ts +2 -0
  212. package/dist/types/src/vite-plugin/manifest.test.d.ts.map +1 -0
  213. package/dist/types/src/vite-plugin/packages.d.ts +13 -0
  214. package/dist/types/src/vite-plugin/packages.d.ts.map +1 -0
  215. package/dist/types/tsconfig.tsbuildinfo +1 -1
  216. package/moon.yml +15 -0
  217. package/package.json +48 -53
  218. package/src/cli/cli.ts +3 -3
  219. package/src/common/activation-events.ts +6 -6
  220. package/src/common/annotations.ts +3 -0
  221. package/src/common/capabilities.ts +18 -23
  222. package/src/common/operations.ts +7 -10
  223. package/src/context.ts +1 -1
  224. package/src/core/activation-event.ts +5 -2
  225. package/src/core/capability-manager.test.ts +1 -1
  226. package/src/core/capability-manager.ts +22 -1
  227. package/src/core/capability.ts +13 -2
  228. package/src/core/edge-registry-plugin-provider.ts +92 -0
  229. package/src/core/index.ts +6 -0
  230. package/src/core/plugin-asset-cache.ts +60 -0
  231. package/src/core/plugin-manager.test.ts +855 -29
  232. package/src/core/plugin-manager.ts +808 -188
  233. package/src/core/plugin-manifest.test.ts +75 -0
  234. package/src/core/plugin-manifest.ts +134 -0
  235. package/src/core/plugin.ts +144 -12
  236. package/src/core/registry.ts +157 -0
  237. package/src/core/url-loader.test.ts +221 -0
  238. package/src/core/url-loader.ts +388 -0
  239. package/src/plugin-operation/OperationPlugin.ts +2 -3
  240. package/src/plugin-operation/history/capability.ts +1 -2
  241. package/src/plugin-operation/history/errors.ts +2 -6
  242. package/src/plugin-operation/history/history-tracker.test.ts +37 -43
  243. package/src/plugin-operation/history/history-tracker.ts +1 -2
  244. package/src/plugin-operation/history/types.ts +1 -1
  245. package/src/plugin-operation/history/undo-mapping.ts +1 -1
  246. package/src/plugin-operation/history/undo-registry.test.ts +3 -4
  247. package/src/plugin-operation/history/undo-registry.ts +1 -1
  248. package/src/plugin-operation/invoker-capability.ts +19 -4
  249. package/src/plugin-operation/meta.ts +1 -1
  250. package/src/plugin-operation/testing.ts +26 -45
  251. package/src/plugin-runtime/RuntimePlugin.ts +2 -3
  252. package/src/plugin-runtime/meta.ts +1 -1
  253. package/src/testing/harness.ts +229 -0
  254. package/src/testing/index.ts +1 -0
  255. package/src/testing/react.test.tsx +48 -0
  256. package/src/testing/react.tsx +113 -0
  257. package/src/testing/service.ts +3 -3
  258. package/src/testing/withPluginManager.stories.tsx +1 -2
  259. package/src/testing/withPluginManager.tsx +40 -18
  260. package/src/ui/components/App/App.stories.tsx +5 -5
  261. package/src/ui/components/App/App.tsx +29 -5
  262. package/src/ui/components/Placeholder/Placeholder.stories.tsx +77 -0
  263. package/src/ui/components/Placeholder/Placeholder.tsx +155 -0
  264. package/src/ui/components/Placeholder/index.ts +5 -0
  265. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +8 -6
  266. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +16 -15
  267. package/src/ui/components/Surface/SurfaceComponent.tsx +109 -53
  268. package/src/ui/components/Surface/SurfaceProfilerContext.tsx +207 -0
  269. package/src/ui/components/Surface/index.ts +35 -1
  270. package/src/ui/components/Surface/types.test.ts +126 -0
  271. package/src/ui/components/Surface/types.ts +164 -12
  272. package/src/ui/components/index.ts +1 -0
  273. package/src/ui/hooks/index.ts +0 -1
  274. package/src/ui/hooks/useApp.test.tsx +159 -0
  275. package/src/ui/hooks/useApp.tsx +226 -15
  276. package/src/ui/hooks/useLoading.tsx +14 -6
  277. package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +263 -0
  278. package/src/vite-plugin/boot-loader/boot-loader.css +294 -0
  279. package/src/vite-plugin/boot-loader/boot-loader.js +274 -0
  280. package/src/vite-plugin/boot-loader/index.ts +112 -0
  281. package/src/vite-plugin/composer/index.ts +306 -0
  282. package/src/vite-plugin/import-map/index.ts +524 -0
  283. package/src/vite-plugin/index.ts +10 -0
  284. package/src/vite-plugin/manifest.test.ts +46 -0
  285. package/src/vite-plugin/manifest.ts +57 -0
  286. package/src/vite-plugin/packages.ts +188 -0
  287. package/tsconfig.json +19 -1
  288. package/tsconfig.node.json +1 -1
  289. package/vitest.config.ts +1 -1
  290. package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
  291. package/dist/lib/browser/capability-7RLVE42K.mjs.map +0 -7
  292. package/dist/lib/browser/chunk-4CTRO67U.mjs +0 -703
  293. package/dist/lib/browser/chunk-4CTRO67U.mjs.map +0 -7
  294. package/dist/lib/browser/chunk-FHQTHCX7.mjs +0 -8
  295. package/dist/lib/browser/chunk-FNKT2QQ2.mjs.map +0 -7
  296. package/dist/lib/browser/chunk-HE27PNNQ.mjs +0 -824
  297. package/dist/lib/browser/chunk-HE27PNNQ.mjs.map +0 -7
  298. package/dist/lib/browser/chunk-NPUEVX42.mjs +0 -34
  299. package/dist/lib/browser/chunk-NPUEVX42.mjs.map +0 -7
  300. package/dist/lib/browser/chunk-PKQT6C53.mjs.map +0 -7
  301. package/dist/lib/browser/chunk-REORGDJT.mjs.map +0 -7
  302. package/dist/lib/browser/chunk-YAFEA4GV.mjs +0 -1
  303. package/dist/lib/browser/chunk-YNFPIQGB.mjs.map +0 -7
  304. package/dist/lib/browser/chunk-ZRWBPIZG.mjs.map +0 -7
  305. package/dist/lib/browser/invoker-capability-BNLVNYHU.mjs.map +0 -7
  306. package/dist/lib/node-esm/capability-EVZK4REM.mjs.map +0 -7
  307. package/dist/lib/node-esm/chunk-2A4PRBIX.mjs.map +0 -7
  308. package/dist/lib/node-esm/chunk-7CPNAEGV.mjs +0 -704
  309. package/dist/lib/node-esm/chunk-7CPNAEGV.mjs.map +0 -7
  310. package/dist/lib/node-esm/chunk-7OWSHPYK.mjs.map +0 -7
  311. package/dist/lib/node-esm/chunk-CJCQS2YL.mjs.map +0 -7
  312. package/dist/lib/node-esm/chunk-DTCHT2X2.mjs +0 -825
  313. package/dist/lib/node-esm/chunk-DTCHT2X2.mjs.map +0 -7
  314. package/dist/lib/node-esm/chunk-JAZVHID3.mjs +0 -35
  315. package/dist/lib/node-esm/chunk-JAZVHID3.mjs.map +0 -7
  316. package/dist/lib/node-esm/chunk-SB5ODNPX.mjs.map +0 -7
  317. package/dist/lib/node-esm/chunk-UFW652GS.mjs.map +0 -7
  318. package/dist/lib/node-esm/chunk-Z4TJPSMP.mjs +0 -2
  319. package/dist/lib/node-esm/invoker-capability-VF6SP44V.mjs.map +0 -7
  320. package/dist/types/src/ui/hooks/useOperationResolver.d.ts +0 -19
  321. package/dist/types/src/ui/hooks/useOperationResolver.d.ts.map +0 -1
  322. package/src/ui/hooks/useOperationResolver.ts +0 -40
  323. /package/dist/lib/{node-esm/capability-CHIMU6LX.mjs.map → browser/capability-LUKGKUQH.mjs.map} +0 -0
  324. /package/dist/lib/browser/{chunk-YAFEA4GV.mjs.map → core/url-loader.mjs.map} +0 -0
  325. /package/dist/lib/node-esm/{chunk-Z4TJPSMP.mjs.map → core/url-loader.mjs.map} +0 -0
@@ -16,12 +16,64 @@ import * as Ref from 'effect/Ref';
16
16
 
17
17
  import { runAndForwardErrors } from '@dxos/effect';
18
18
  import { Performance } from '@dxos/effect';
19
+ import { BaseError } from '@dxos/errors';
19
20
  import { log } from '@dxos/log';
20
21
 
21
22
  import * as ActivationEvent from './activation-event';
22
23
  import * as Capability from './capability';
23
24
  import * as CapabilityManager from './capability-manager';
24
25
  import * as Plugin from './plugin';
26
+ // Imported with a `PluginRegistry` alias because the unrelated `@effect-atom/atom-react`
27
+ // `Registry` is already imported above; from outside this file the namespace is
28
+ // re-exported as `Registry` via `./index.ts`.
29
+ import * as PluginRegistry from './registry';
30
+
31
+ /**
32
+ * Tagged error for failures during the constructor-launched core/enabled
33
+ * `enable()` chain. Surfaces via {@link PluginManager.activate}'s wait on
34
+ * `_initialization` so a caller blocked on initialization gets a typed
35
+ * failure (with the original error preserved as `cause`) instead of an
36
+ * untyped `Error`.
37
+ */
38
+ export class PluginInitializationError extends BaseError.extend(
39
+ 'PluginInitializationError',
40
+ 'Plugin manager initialization failed',
41
+ ) {}
42
+
43
+ /**
44
+ * Tagged error raised when a plugin exceeds its configured load or activation
45
+ * timeout. The plugin manager records the failure on the `failed` atom and
46
+ * auto-disables the plugin so that one stuck remote does not stall app boot.
47
+ * `context.id` is the plugin id, `context.phase` is `'load'` or `'activation'`.
48
+ */
49
+ export class PluginTimeoutError extends BaseError.extend('PluginTimeoutError', 'Plugin operation timed out') {}
50
+
51
+ /** Phase of the plugin lifecycle in which the failure was observed. */
52
+ export type PluginFailurePhase = 'load' | 'activation';
53
+
54
+ /** Why the plugin entered a failed state. */
55
+ export type PluginFailureReason = 'timeout' | 'error';
56
+
57
+ /**
58
+ * Record of a plugin that failed to load or activate. Surfaced via the
59
+ * {@link PluginManager.failed} atom so registry / UI consumers can flag
60
+ * unhealthy plugins (e.g. a remote host that has gone offline) rather than
61
+ * leaving the app in a half-broken state.
62
+ */
63
+ export type PluginFailure = {
64
+ readonly id: string;
65
+ readonly phase: PluginFailurePhase;
66
+ readonly reason: PluginFailureReason;
67
+ readonly error: Error;
68
+ /** `Date.now()` when the failure was recorded. */
69
+ readonly timestamp: number;
70
+ };
71
+
72
+ /** Default deadline for resolving a lazy plugin's dynamic import. */
73
+ const DEFAULT_LOAD_TIMEOUT = Duration.seconds(30);
74
+
75
+ /** Default deadline for a single module's `activate()` body. */
76
+ const DEFAULT_ACTIVATION_TIMEOUT = Duration.seconds(30);
25
77
 
26
78
  /**
27
79
  * Identifier denoting a Manager.
@@ -29,15 +81,63 @@ import * as Plugin from './plugin';
29
81
  export const ManagerTypeId: unique symbol = Symbol.for('@dxos/app-framework/Manager');
30
82
  export type ManagerTypeId = typeof ManagerTypeId;
31
83
 
84
+ /**
85
+ * Loader result that carries optional metadata about how the plugin was sourced.
86
+ *
87
+ * `dev: true` marks a plugin as session-only and triggers shadow-on-id-collision
88
+ * inside the manager: if a plugin with the same id is already registered (a
89
+ * builtin, or a previously-installed plugin from the registry), the dev plugin
90
+ * temporarily takes over that id slot. The original is restored when the dev
91
+ * plugin is removed (or on page reload, since dev plugins aren't persisted).
92
+ */
93
+ export type LoadedPlugin = {
94
+ plugin: Plugin.Plugin;
95
+ /** True when the plugin came from a dev source. See type doc for semantics. */
96
+ dev?: boolean;
97
+ };
98
+
32
99
  export type ManagerOptions = {
33
- pluginLoader: (id: string) => Effect.Effect<Plugin.Plugin, Error>;
100
+ pluginLoader: (id: string) => Effect.Effect<LoadedPlugin, Error>;
34
101
  plugins?: Plugin.Plugin[];
35
102
  core?: string[];
36
103
  enabled?: string[];
37
104
  registry?: Registry.Registry;
105
+ /**
106
+ * Backend for the plugin registry catalog. When omitted the manager exposes a
107
+ * no-op `pluginRegistry` (empty list, no versions endpoint). Implementations
108
+ * live in app-framework alongside the interface (e.g.
109
+ * `EdgeRegistryPluginProvider`); the host app instantiates one and passes it in.
110
+ */
111
+ pluginRegistryProvider?: PluginRegistry.PluginProvider;
112
+ /**
113
+ * Hook called when a plugin is removed via {@link PluginManager.remove}. Used by the
114
+ * host app to clean up persisted state (e.g. evict offline-cached plugin assets).
115
+ * Failures are logged and swallowed; removal still succeeds even if the hook fails.
116
+ */
117
+ onRemove?: (id: string) => Effect.Effect<void, unknown>;
118
+ /**
119
+ * Maximum time allowed for a lazy plugin's dynamic `import()` to resolve.
120
+ * Plugins that exceed this are flagged on the {@link PluginManager.failed}
121
+ * atom and auto-disabled so a stuck remote host can't stall app boot.
122
+ * Defaults to 30 seconds; pass `Duration.infinity` to disable.
123
+ */
124
+ loadTimeout?: Duration.DurationInput;
125
+ /**
126
+ * Maximum time allowed for a single module's `activate()` Effect to settle.
127
+ * Modules that exceed this fail with {@link PluginTimeoutError}; the owning
128
+ * plugin is recorded on `failed` and auto-disabled. Defaults to 30 seconds;
129
+ * pass `Duration.infinity` to disable.
130
+ */
131
+ activationTimeout?: Duration.DurationInput;
38
132
  };
39
133
 
40
- type ActivationMessage = { event: string; state: 'activating' | 'activated' | 'error'; error?: Error };
134
+ export type ActivationMessage = {
135
+ event: string;
136
+ state: 'activating' | 'activated' | 'error';
137
+ /** Module ID when the message pertains to a specific module activation. */
138
+ module?: string;
139
+ error?: Error;
140
+ };
41
141
 
42
142
  /**
43
143
  * Interface for the Plugin Manager.
@@ -47,6 +147,13 @@ export interface PluginManager {
47
147
  readonly activation: PubSub.PubSub<ActivationMessage>;
48
148
  readonly capabilities: CapabilityManager.CapabilityManager;
49
149
  readonly registry: Registry.Registry;
150
+ /**
151
+ * Cached registry catalog state plus pass-throughs for `listVersions` /
152
+ * `getPlugin`. Always present — the host supplies a `pluginRegistryProvider`
153
+ * via {@link ManagerOptions} for real backends, or it falls back to a no-op
154
+ * implementation that yields an empty catalog.
155
+ */
156
+ readonly pluginRegistry: PluginRegistry.Manager;
50
157
 
51
158
  readonly plugins: Atom.Atom<readonly Plugin.Plugin[]>;
52
159
  readonly core: Atom.Atom<readonly string[]>;
@@ -55,6 +162,19 @@ export interface PluginManager {
55
162
  readonly active: Atom.Atom<readonly string[]>;
56
163
  readonly eventsFired: Atom.Atom<readonly string[]>;
57
164
  readonly pendingReset: Atom.Atom<readonly string[]>;
165
+ /**
166
+ * Plugins that failed to load or activate. Subscribers (e.g. the registry
167
+ * UI) can use this to flag unhealthy entries; a plugin id appears here at
168
+ * most once with its most recent failure.
169
+ */
170
+ readonly failed: Atom.Atom<readonly PluginFailure[]>;
171
+ /**
172
+ * Ids of currently-registered plugins that came from a dev source (loaded
173
+ * via {@link LoadedPlugin} with `dev: true`). Subscribers can use this to
174
+ * badge dev-overridden plugins or to derive the id of the active dev plugin
175
+ * for an "uninstall dev plugin" affordance.
176
+ */
177
+ readonly devPluginIds: Atom.Atom<readonly string[]>;
58
178
 
59
179
  getPlugins(): readonly Plugin.Plugin[];
60
180
  getCore(): readonly string[];
@@ -63,10 +183,23 @@ export interface PluginManager {
63
183
  getActive(): readonly string[];
64
184
  getEventsFired(): readonly string[];
65
185
  getPendingReset(): readonly string[];
186
+ getFailed(): readonly PluginFailure[];
187
+ getDevPluginIds(): readonly string[];
188
+
189
+ /**
190
+ * Clears the failure record for a plugin so it can be retried. Returns
191
+ * whether a failure record existed and was removed.
192
+ */
193
+ clearFailure(id: string): boolean;
66
194
 
67
- add(id: string): Effect.Effect<boolean, Error>;
195
+ /**
196
+ * Loads a plugin via the plugin loader and registers it without enabling it.
197
+ * Returns the loaded plugin so callers can enable it by its canonical id
198
+ * (which may differ from the locator used to load it, e.g. URL loaders).
199
+ */
200
+ add(id: string): Effect.Effect<Plugin.Plugin, Error>;
68
201
  enable(id: string): Effect.Effect<boolean, Error>;
69
- remove(id: string): boolean;
202
+ remove(id: string): Effect.Effect<boolean, Error>;
70
203
  disable(id: string): Effect.Effect<boolean, Error>;
71
204
  // TODO(wittjosiah): Improve error typing.
72
205
  activate(
@@ -75,6 +208,13 @@ export interface PluginManager {
75
208
  ): Effect.Effect<boolean, Error>;
76
209
  deactivate(id: string): Effect.Effect<boolean, Error>;
77
210
  reset(event: ActivationEvent.ActivationEvent | string): Effect.Effect<boolean, Error>;
211
+
212
+ /**
213
+ * Shuts down the manager by deactivating all active modules in reverse activation order,
214
+ * clearing all capabilities, and resetting lifecycle bookkeeping.
215
+ * Plugins, core, enabled, and modules remain intact so the manager can be reused.
216
+ */
217
+ shutdown(): Effect.Effect<boolean, Error>;
78
218
  }
79
219
 
80
220
  /**
@@ -92,6 +232,7 @@ class ManagerImpl implements PluginManager {
92
232
  readonly activation = Effect.runSync(PubSub.unbounded<ActivationMessage>());
93
233
  readonly capabilities: CapabilityManager.CapabilityManager;
94
234
  readonly registry: Registry.Registry;
235
+ readonly pluginRegistry: PluginRegistry.Manager;
95
236
 
96
237
  private readonly _pluginsAtom: Atom.Writable<Plugin.Plugin[]>;
97
238
  private readonly _coreAtom: Atom.Writable<string[]>;
@@ -100,12 +241,41 @@ class ManagerImpl implements PluginManager {
100
241
  private readonly _activeAtom: Atom.Writable<string[]>;
101
242
  private readonly _eventsFiredAtom: Atom.Writable<string[]>;
102
243
  private readonly _pendingResetAtom: Atom.Writable<string[]>;
244
+ private readonly _failedAtom: Atom.Writable<PluginFailure[]>;
103
245
  private readonly _pluginLoader: ManagerOptions['pluginLoader'];
246
+ private readonly _onRemove: ManagerOptions['onRemove'];
247
+ private readonly _loadTimeout: Duration.DurationInput;
248
+ private readonly _activationTimeout: Duration.DurationInput;
104
249
  private readonly _capabilities = new Map<string, Capability.Any[]>();
105
250
  private readonly _moduleMemoMap = new Map<Plugin.PluginModule['id'], Deferred.Deferred<Capability.Any[], Error>>();
106
251
  private readonly _moduleSemaphores = new Map<Plugin.PluginModule['id'], Effect.Semaphore>();
252
+ // Coalesces concurrent `_resolveLazyPlugin` calls per plugin id. Without
253
+ // this, two callers entering `enable(id)` before the swap completes would
254
+ // each invoke `mod.default(options)` and produce distinct module objects,
255
+ // defeating `_addModule`'s reference-equality dedupe and racing the
256
+ // `_pluginsAtom` swap.
257
+ private readonly _resolvingPlugins = new Map<string, Deferred.Deferred<Plugin.Plugin, Plugin.LazyPluginError>>();
258
+ // Tracks dev-source plugins (loaded via a Vite dev server) keyed by id.
259
+ // When `shadow` is present, the entry has displaced an existing plugin —
260
+ // `remove` reinstates it and re-enables iff `wasEnabled`. Entries without a
261
+ // shadow are dev plugins with no underlying registry/builtin to restore.
262
+ // The atom mirrors the map's keys for UI subscribers (they don't need the
263
+ // shadow internals); the two stay in sync via {@link _markDev}/{@link _unmarkDev}.
264
+ private readonly _devPlugins = new Map<string, { shadow?: { plugin: Plugin.Plugin; wasEnabled: boolean } }>();
265
+ private readonly _devPluginIdsAtom: Atom.Writable<string[]>;
107
266
  private readonly _activatingEvents = Effect.runSync(Ref.make<string[]>([]));
108
267
  private readonly _activatingModules = Effect.runSync(Ref.make<string[]>([]));
268
+ private readonly _inFlightFibers = Effect.runSync(Ref.make<Array<Fiber.Fiber<unknown, unknown>>>([]));
269
+ private readonly _shutdownSemaphore = Effect.runSync(Effect.makeSemaphore(1));
270
+ private readonly _shuttingDown = Effect.runSync(Ref.make(false));
271
+ // Tracks the constructor-launched core/enabled `enable()` calls so that
272
+ // `activate` can wait for module registration before dispatching events.
273
+ // Lazy plugins make `enable` asynchronous (a dynamic `import()` happens
274
+ // inside it), so without this synchronization an `activate` triggered
275
+ // immediately after `make` could fire on an empty module set. Failures
276
+ // are wrapped in `PluginInitializationError` so awaiters get a tagged
277
+ // error rather than the wide `Error` produced by the underlying chain.
278
+ private readonly _initialization = Effect.runSync(Deferred.make<void, PluginInitializationError>());
109
279
 
110
280
  constructor({
111
281
  pluginLoader,
@@ -113,13 +283,21 @@ class ManagerImpl implements PluginManager {
113
283
  core = plugins.map(({ meta }) => meta.id),
114
284
  enabled = [],
115
285
  registry,
286
+ pluginRegistryProvider,
287
+ onRemove,
288
+ loadTimeout = DEFAULT_LOAD_TIMEOUT,
289
+ activationTimeout = DEFAULT_ACTIVATION_TIMEOUT,
116
290
  }: ManagerOptions) {
117
291
  this.registry = registry ?? Registry.make();
118
292
  this.capabilities = CapabilityManager.make({
119
293
  registry: this.registry,
120
294
  });
295
+ this.pluginRegistry = new PluginRegistry.Manager(pluginRegistryProvider, this.registry);
121
296
 
122
297
  this._pluginLoader = pluginLoader;
298
+ this._onRemove = onRemove;
299
+ this._loadTimeout = loadTimeout;
300
+ this._activationTimeout = activationTimeout;
123
301
  this._pluginsAtom = Atom.make(plugins).pipe(Atom.keepAlive);
124
302
  this._coreAtom = Atom.make(core).pipe(Atom.keepAlive);
125
303
  this._enabledAtom = Atom.make(enabled).pipe(Atom.keepAlive);
@@ -127,8 +305,22 @@ class ManagerImpl implements PluginManager {
127
305
  this._activeAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
128
306
  this._eventsFiredAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
129
307
  this._pendingResetAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
308
+ this._failedAtom = Atom.make<PluginFailure[]>([]).pipe(Atom.keepAlive);
309
+ this._devPluginIdsAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
130
310
  plugins.forEach((plugin) => this._addPlugin(plugin));
131
- void Effect.all([...core, ...enabled].map((id) => this.enable(id))).pipe(runAndForwardErrors);
311
+ // Dedupe before mapping to `enable` — `core` and `enabled` may overlap (an
312
+ // app-supplied plugin can be in both), and concurrent `enable(id)` calls
313
+ // for the same id are not idempotent (each would re-run the lazy resolve
314
+ // and double-register modules). `new Set([...])` preserves first-seen
315
+ // order which matches the natural core-before-enabled precedence.
316
+ const initialIds = [...new Set([...core, ...enabled])];
317
+ void Effect.all(initialIds.map((id) => this.enable(id)))
318
+ .pipe(
319
+ Effect.mapError((cause) => new PluginInitializationError({ cause })),
320
+ Effect.tap(() => Deferred.succeed(this._initialization, undefined)),
321
+ Effect.tapErrorCause((cause) => Deferred.failCause(this._initialization, cause)),
322
+ )
323
+ .pipe(runAndForwardErrors);
132
324
  }
133
325
 
134
326
  get plugins(): Atom.Atom<readonly Plugin.Plugin[]> {
@@ -174,6 +366,20 @@ class ManagerImpl implements PluginManager {
174
366
  return this._pendingResetAtom;
175
367
  }
176
368
 
369
+ /**
370
+ * Plugins that failed to load or activate.
371
+ */
372
+ get failed(): Atom.Atom<readonly PluginFailure[]> {
373
+ return this._failedAtom;
374
+ }
375
+
376
+ /**
377
+ * Ids of currently-registered plugins that came from a dev source.
378
+ */
379
+ get devPluginIds(): Atom.Atom<readonly string[]> {
380
+ return this._devPluginIdsAtom;
381
+ }
382
+
177
383
  getPlugins(): readonly Plugin.Plugin[] {
178
384
  return this._get(this._pluginsAtom);
179
385
  }
@@ -202,16 +408,82 @@ class ManagerImpl implements PluginManager {
202
408
  return this._get(this._pendingResetAtom);
203
409
  }
204
410
 
411
+ getFailed(): readonly PluginFailure[] {
412
+ return this._get(this._failedAtom);
413
+ }
414
+
415
+ getDevPluginIds(): readonly string[] {
416
+ return this._get(this._devPluginIdsAtom);
417
+ }
418
+
419
+ /**
420
+ * Marks `id` as dev-sourced. If the plugin displaced an existing one, pass
421
+ * the shadow snapshot so `remove` can restore it. Repeat calls (e.g. a dev
422
+ * plugin reload) preserve the original shadow target — restoration always
423
+ * unwinds back to the real underlying plugin, never an intermediate dev build.
424
+ */
425
+ private _markDev(id: string, shadow?: { plugin: Plugin.Plugin; wasEnabled: boolean }): void {
426
+ if (this._devPlugins.has(id)) {
427
+ return;
428
+ }
429
+ this._devPlugins.set(id, { shadow });
430
+ this._update(this._devPluginIdsAtom, (ids) => (ids.includes(id) ? ids : [...ids, id]));
431
+ }
432
+
433
+ /** Drops the dev-plugin entry and returns its shadow data (if any) for restoration. */
434
+ private _unmarkDev(id: string): { plugin: Plugin.Plugin; wasEnabled: boolean } | undefined {
435
+ const entry = this._devPlugins.get(id);
436
+ this._devPlugins.delete(id);
437
+ this._update(this._devPluginIdsAtom, (ids) => ids.filter((existing) => existing !== id));
438
+ return entry?.shadow;
439
+ }
440
+
441
+ clearFailure(id: string): boolean {
442
+ const current = this._get(this._failedAtom);
443
+ if (!current.some((failure) => failure.id === id)) {
444
+ return false;
445
+ }
446
+ this._set(
447
+ this._failedAtom,
448
+ current.filter((failure) => failure.id !== id),
449
+ );
450
+ return true;
451
+ }
452
+
205
453
  /**
206
454
  * Adds a plugin to the manager via the plugin loader.
455
+ * The plugin is registered but not enabled; call `enable` separately to activate it.
207
456
  * @param id The id of the plugin.
208
457
  */
209
- add(id: string): Effect.Effect<boolean, Error> {
458
+ add(id: string): Effect.Effect<Plugin.Plugin, Error> {
210
459
  return Effect.gen(this, function* () {
211
460
  log('add plugin', { id });
212
- const plugin = yield* this._pluginLoader(id);
213
- this._addPlugin(plugin);
214
- return yield* this.enable(id);
461
+ const { plugin, dev = false } = yield* this._pluginLoader(id);
462
+ const pluginId = plugin.meta.id;
463
+ const existing = this._getPlugin(pluginId);
464
+
465
+ if (dev && existing && existing !== plugin) {
466
+ // Shadow path: a plugin with this id is already registered (a builtin,
467
+ // a registry install, or a previous dev load). Disable it, stash it,
468
+ // and swap the dev plugin into the same id slot. The dialog will call
469
+ // `enable(pluginId)` next, which activates the dev plugin's modules.
470
+ // `_markDev` is a no-op when the id is already tracked, so a dev-plugin
471
+ // reload (after editing source) keeps the *original* shadow target —
472
+ // removal restores the real underlying plugin, not an intermediate build.
473
+ const wasEnabled = this._get(this._enabledAtom).includes(pluginId);
474
+ if (wasEnabled) {
475
+ yield* this.disable(pluginId);
476
+ }
477
+ this._markDev(pluginId, { plugin: existing, wasEnabled });
478
+ this._update(this._pluginsAtom, (plugins) => plugins.map((p) => (p.meta.id === pluginId ? plugin : p)));
479
+ } else {
480
+ this._addPlugin(plugin);
481
+ if (dev) {
482
+ this._markDev(pluginId);
483
+ }
484
+ }
485
+
486
+ return plugin;
215
487
  });
216
488
  }
217
489
 
@@ -222,11 +494,17 @@ class ManagerImpl implements PluginManager {
222
494
  enable(id: string): Effect.Effect<boolean, Error> {
223
495
  return Effect.gen(this, function* () {
224
496
  log('enable plugin', { id });
225
- const plugin = this._getPlugin(id);
226
- if (!plugin) {
497
+ const stub = this._getPlugin(id);
498
+ if (!stub) {
227
499
  return false;
228
500
  }
229
501
 
502
+ // Clear any prior failure record so a retry starts from a clean slate.
503
+ // The failure stays on the atom only if this attempt also fails.
504
+ this.clearFailure(id);
505
+
506
+ const plugin = yield* this._resolveLazyPlugin(stub);
507
+
230
508
  this._update(this._enabledAtom, (enabled) => (enabled.includes(id) ? enabled : [...enabled, id]));
231
509
 
232
510
  plugin.modules.forEach((module) => {
@@ -244,19 +522,108 @@ class ManagerImpl implements PluginManager {
244
522
  });
245
523
  }
246
524
 
525
+ /**
526
+ * Resolves a lazy plugin stub (returned by {@link Plugin.lazy}) to its
527
+ * loaded form and swaps it into `_pluginsAtom`. Returns the input unchanged
528
+ * when the plugin is already resolved, so callers can `yield*` this
529
+ * unconditionally. The lazy stub carries `meta` synchronously but its
530
+ * `modules` list is empty until the loader resolves; the swap ensures
531
+ * subsequent enable/disable operations see the resolved plugin.
532
+ *
533
+ * Concurrent calls for the same id are coalesced via `_resolvingPlugins`:
534
+ * the first caller starts the resolution, every subsequent caller awaits
535
+ * the same `Deferred`. On failure we publish a `lazy:<id>` error message
536
+ * and skip the atom swap so the failure is observable to the activation
537
+ * subscriber and a retry can be attempted.
538
+ */
539
+ private _resolveLazyPlugin(plugin: Plugin.Plugin): Effect.Effect<Plugin.Plugin, Plugin.LazyPluginError> {
540
+ return Effect.gen(this, function* () {
541
+ if (!Plugin.isLazy(plugin)) {
542
+ return plugin;
543
+ }
544
+ const id = plugin.meta.id;
545
+
546
+ const existing = this._resolvingPlugins.get(id);
547
+ if (existing) {
548
+ return yield* Deferred.await(existing);
549
+ }
550
+ const deferred = yield* Deferred.make<Plugin.Plugin, Plugin.LazyPluginError>();
551
+ this._resolvingPlugins.set(id, deferred);
552
+
553
+ return yield* Effect.gen(this, function* () {
554
+ log('resolving lazy plugin', { id });
555
+ yield* PubSub.publish(this.activation, { event: '', state: 'activating', module: `lazy:${id}` });
556
+ const resolvedPlugin = yield* Plugin.resolveLazy(plugin).pipe(
557
+ // Cap how long a remote import can hang. Without this the host can
558
+ // sit on a pending dynamic `import()` indefinitely if the plugin's
559
+ // server is unreachable, which stalls every caller awaiting
560
+ // `enable(id)` and (transitively) the manager's initialization.
561
+ Effect.timeoutFail({
562
+ duration: this._loadTimeout,
563
+ onTimeout: () =>
564
+ new Plugin.LazyPluginError({
565
+ context: { id, reason: 'load-failed' },
566
+ cause: new PluginTimeoutError({ context: { id, phase: 'load' as PluginFailurePhase } }),
567
+ }),
568
+ }),
569
+ );
570
+ this._update(this._pluginsAtom, (plugins) => plugins.map((p) => (p.meta.id === id ? resolvedPlugin : p)));
571
+ yield* PubSub.publish(this.activation, { event: '', state: 'activated', module: `lazy:${id}` });
572
+ return resolvedPlugin;
573
+ }).pipe(
574
+ Effect.tapError((error) =>
575
+ Effect.gen(this, function* () {
576
+ yield* PubSub.publish(this.activation, { event: '', state: 'error', module: `lazy:${id}`, error });
577
+ this._recordFailure(id, 'load', error);
578
+ this._scheduleAutoDisable(id);
579
+ }),
580
+ ),
581
+ Effect.tap((value) => Deferred.succeed(deferred, value)),
582
+ Effect.tapErrorCause((cause) => Deferred.failCause(deferred, cause)),
583
+ Effect.ensuring(Effect.sync(() => this._resolvingPlugins.delete(id))),
584
+ );
585
+ });
586
+ }
587
+
247
588
  /**
248
589
  * Removes a plugin from the manager.
249
590
  * @param id The id of the plugin.
250
591
  */
251
- remove(id: string): boolean {
252
- log('remove plugin', { id });
253
- const result = this.disable(id);
254
- if (!result) {
255
- return false;
256
- }
592
+ remove(id: string): Effect.Effect<boolean, Error> {
593
+ return Effect.gen(this, function* () {
594
+ log('remove plugin', { id });
595
+ const wasDev = this._devPlugins.has(id);
596
+ const disabled = yield* this.disable(id);
597
+ if (!disabled) {
598
+ return false;
599
+ }
257
600
 
258
- this._removePlugin(id);
259
- return true;
601
+ this._removePlugin(id);
602
+ if (this._onRemove) {
603
+ this._runForkedFiber(
604
+ this._onRemove(id).pipe(
605
+ Effect.tapError((error) => Effect.sync(() => log.warn('plugin remove hook failed', { id, error }))),
606
+ Effect.ignore,
607
+ ),
608
+ );
609
+ }
610
+
611
+ // If a dev plugin was shadowing an existing plugin, reinstate the
612
+ // original now that the dev plugin is gone. Re-enable only if the
613
+ // original was enabled at shadow time — preserving the user's intent
614
+ // for plugins they had explicitly disabled before iterating on a dev
615
+ // build.
616
+ if (wasDev) {
617
+ const shadow = this._unmarkDev(id);
618
+ if (shadow) {
619
+ this._addPlugin(shadow.plugin);
620
+ if (shadow.wasEnabled) {
621
+ yield* this.enable(id);
622
+ }
623
+ }
624
+ }
625
+ return true;
626
+ });
260
627
  }
261
628
 
262
629
  /**
@@ -300,155 +667,36 @@ class ManagerImpl implements PluginManager {
300
667
  ): Effect.Effect<boolean, Error> {
301
668
  const key = typeof event === 'string' ? event : ActivationEvent.eventKey(event);
302
669
  return Effect.gen(this, function* () {
303
- log('activating', { key, ...params });
304
- yield* Ref.update(this._activatingEvents, (activating) => Array.append(activating, key));
305
- const pendingIndex = this._get(this._pendingResetAtom).findIndex((event) => event === key);
306
- if (pendingIndex !== -1) {
307
- this._update(this._pendingResetAtom, (pending) => pending.filter((event) => event !== key));
308
- }
309
-
310
- const activatingEvents = yield* this._activatingEvents;
311
- const activatingModules = yield* this._activatingModules;
312
- const modules = this._getInactiveModulesByEvent(key).filter((module) => {
313
- const allOf = ActivationEvent.isAllOf(module.activatesOn);
314
- if (!allOf) {
315
- return true;
316
- }
317
-
318
- // Check to see if all of the events in the `allOf` have been fired.
319
- // An event can be considered "fired" if it is in the `eventsFired` list or if it is currently being activated.
320
- const events = ActivationEvent.getEvents(module.activatesOn).filter(
321
- (event) => ActivationEvent.eventKey(event) !== key,
322
- );
323
- return (
324
- events.every(
325
- (event) =>
326
- this._get(this._eventsFiredAtom).includes(ActivationEvent.eventKey(event)) ||
327
- activatingEvents.includes(ActivationEvent.eventKey(event)),
328
- ) && !activatingModules.includes(module.id)
329
- );
330
- });
331
- yield* Ref.update(this._activatingModules, (activating) =>
332
- Array.appendAll(
333
- activating,
334
- modules.map((module) => module.id),
335
- ),
336
- );
337
- if (modules.length === 0) {
338
- log('no modules to activate', { key });
339
- if (!this._get(this._eventsFiredAtom).includes(key)) {
340
- this._update(this._eventsFiredAtom, (events) => [...events, key]);
341
- }
670
+ if (yield* this._isShuttingDown()) {
671
+ log('skipping activation during shutdown', { key, ...params });
342
672
  return false;
343
673
  }
344
674
 
345
- log('activating modules', { key, modules: modules.map((module) => module.id) });
346
- yield* PubSub.publish(this.activation, { event: key, state: 'activating' });
347
-
348
- // Fire activatesBefore events.
349
- const beforeEvents = Function.pipe(
350
- modules,
351
- Array.flatMap((module) => module.activatesBefore ?? []),
352
- HashSet.fromIterable,
353
- HashSet.toValues,
354
- Array.filter((event) => !activatingEvents.includes(ActivationEvent.eventKey(event))),
355
- );
356
- yield* Function.pipe(
357
- beforeEvents,
358
- Array.map((event) => this.activate(event, { before: key })),
359
- Effect.allWith({ concurrency: 'unbounded' }),
360
- together(
361
- Effect.sleep(Duration.seconds(10)).pipe(
362
- Effect.andThen(
363
- Effect.sync(() =>
364
- log.warn('activatesBefore is taking a long time', {
365
- event: key,
366
- beforeEvents: beforeEvents.map(ActivationEvent.eventKey),
367
- }),
368
- ),
369
- ),
370
- ),
371
- ),
372
- );
373
-
374
- // Concurrently triggers loading of lazy capabilities.
375
- const getCapabilities = yield* Function.pipe(
376
- modules,
377
- Array.map((mod) => this._loadModule(mod)),
378
- Effect.allWith({ concurrency: 'unbounded' }),
379
- Effect.catchAll((error) => {
380
- return Effect.gen(this, function* () {
381
- yield* PubSub.publish(this.activation, { event: key, state: 'error', error });
382
- return yield* Effect.fail(error);
383
- });
384
- }),
385
- );
675
+ // Wait for the constructor's core/enabled `enable()` chain including
676
+ // any async dynamic imports for lazy plugins — to finish registering
677
+ // modules. Without this, dispatching to an empty module set is the
678
+ // observable symptom of the race.
679
+ yield* Deferred.await(this._initialization);
386
680
 
387
- // Contribute the capabilities from the activated modules.
388
- yield* Function.pipe(
389
- modules,
390
- Array.zip(getCapabilities),
391
- Array.map(([module, capabilities]) => this._contributeCapabilities(module, capabilities)),
392
- // TODO(wittjosiah): This currently can't be run in parallel.
393
- // Running this with concurrency causes races with `allOf` activation events.
394
- Effect.all,
395
- );
396
-
397
- // Fire activatesAfter events.
398
- const afterEvents = Function.pipe(
399
- modules,
400
- Array.flatMap((module) => module.activatesAfter ?? []),
401
- HashSet.fromIterable,
402
- HashSet.toValues,
403
- Array.filter((event) => !activatingEvents.includes(ActivationEvent.eventKey(event))),
404
- );
405
- yield* Function.pipe(
406
- afterEvents,
407
- Array.map((event) => this.activate(event, { after: key })),
408
- Effect.allWith({ concurrency: 'unbounded' }),
409
- together(
410
- Effect.sleep(Duration.seconds(10)).pipe(
411
- Effect.andThen(
412
- Effect.sync(() =>
413
- log.warn('activatesAfter is taking a long time', {
414
- event: key,
415
- afterEvents: afterEvents.map(ActivationEvent.eventKey),
416
- }),
417
- ),
681
+ return yield* Effect.withFiberRuntime<boolean, Error>((fiber) =>
682
+ this._activateEvent(key, params, fiber).pipe(
683
+ together(
684
+ Effect.sleep(Duration.seconds(15)).pipe(
685
+ Effect.andThen(Effect.sync(() => log.warn('event activation is taking a long time', { event: key }))),
418
686
  ),
419
687
  ),
688
+ Performance.addTrackEntry({
689
+ name: typeof event === 'string' ? event : ActivationEvent.eventKey(event),
690
+ devtools: {
691
+ dataType: 'track-entry',
692
+ track: 'Event Activation',
693
+ trackGroup: 'Composer',
694
+ color: 'primary',
695
+ },
696
+ }),
420
697
  ),
421
698
  );
422
-
423
- yield* Ref.update(this._activatingEvents, (activating) => Array.filter(activating, (event) => event !== key));
424
- yield* Ref.update(this._activatingModules, (activating) =>
425
- Array.filter(activating, (module) => !modules.map((module) => module.id).includes(module)),
426
- );
427
-
428
- if (!this._get(this._eventsFiredAtom).includes(key)) {
429
- this._update(this._eventsFiredAtom, (events) => [...events, key]);
430
- }
431
-
432
- yield* PubSub.publish(this.activation, { event: key, state: 'activated' });
433
- log('activated', { key });
434
-
435
- return true;
436
- }).pipe(
437
- together(
438
- Effect.sleep(Duration.seconds(15)).pipe(
439
- Effect.andThen(Effect.sync(() => log.warn('event activation is taking a long time', { event: key }))),
440
- ),
441
- ),
442
- Performance.addTrackEntry({
443
- name: typeof event === 'string' ? event : ActivationEvent.eventKey(event),
444
- devtools: {
445
- dataType: 'track-entry',
446
- track: 'Event Activation',
447
- trackGroup: 'Composer',
448
- color: 'primary',
449
- },
450
- }),
451
- );
699
+ });
452
700
  }
453
701
 
454
702
  /**
@@ -495,6 +743,40 @@ class ManagerImpl implements PluginManager {
495
743
  });
496
744
  }
497
745
 
746
+ shutdown(): Effect.Effect<boolean, Error> {
747
+ return this._shutdownSemaphore.withPermits(1)(
748
+ Effect.gen(this, function* () {
749
+ yield* Ref.set(this._shuttingDown, true);
750
+ log('shutdown');
751
+
752
+ yield* this._interruptInFlightActivations();
753
+
754
+ const activeIds = [...this._get(this._activeAtom)].reverse();
755
+ const allModules = this._get(this._modulesAtom);
756
+ const modulesToDeactivate = activeIds
757
+ .map((id) => allModules.find((module) => module.id === id))
758
+ .filter((module): module is Plugin.PluginModule => module != null);
759
+
760
+ for (const module of modulesToDeactivate) {
761
+ yield* this._deactivateModule(module);
762
+ }
763
+
764
+ this._set(this._eventsFiredAtom, []);
765
+ this._set(this._pendingResetAtom, []);
766
+ this._moduleMemoMap.clear();
767
+ yield* Ref.set(this._activatingEvents, []);
768
+ yield* Ref.set(this._activatingModules, []);
769
+
770
+ log('shutdown complete');
771
+ return true;
772
+ }).pipe(Effect.ensuring(Ref.set(this._shuttingDown, false))),
773
+ );
774
+ }
775
+
776
+ //
777
+ // State helpers
778
+ //
779
+
498
780
  private _get<T>(atom: Atom.Atom<T>): T {
499
781
  return this.registry.get(atom);
500
782
  }
@@ -507,30 +789,52 @@ class ManagerImpl implements PluginManager {
507
789
  this._set(atom, updater(this._get(atom)));
508
790
  }
509
791
 
510
- private _addPlugin(plugin: Plugin.Plugin): void {
511
- log('add plugin', { id: plugin.meta.id });
512
- // TODO(wittjosiah): Find a way to add a warning for duplicate plugins that doesn't cause log spam.
513
- this._update(this._pluginsAtom, (plugins) => (plugins.includes(plugin) ? plugins : [...plugins, plugin]));
792
+ private _isShuttingDown(): Effect.Effect<boolean> {
793
+ return Ref.get(this._shuttingDown);
514
794
  }
515
795
 
516
- private _removePlugin(id: string): void {
517
- log('remove plugin', { id });
518
- this._update(this._pluginsAtom, (plugins) => plugins.filter((plugin) => plugin.meta.id !== id));
796
+ private _getPlugin(id: string): Plugin.Plugin | undefined {
797
+ return this._get(this._pluginsAtom).find((plugin) => plugin.meta.id === id);
519
798
  }
520
799
 
521
- private _addModule(module: Plugin.PluginModule): void {
522
- log('add module', { id: module.id });
523
- // TODO(wittjosiah): Find a way to add a warning for duplicate modules that doesn't cause log spam.
524
- this._update(this._modulesAtom, (modules) => (modules.includes(module) ? modules : [...modules, module]));
800
+ private _getPluginIdForModule(moduleId: string): string | undefined {
801
+ return this._get(this._pluginsAtom).find((plugin) => plugin.modules.some((module) => module.id === moduleId))?.meta
802
+ .id;
525
803
  }
526
804
 
527
- private _removeModule(id: string): void {
528
- log('remove module', { id });
529
- this._update(this._modulesAtom, (modules) => modules.filter((module) => module.id !== id));
805
+ /**
806
+ * Records a failure for a plugin. Latest failure wins so the registry UI
807
+ * always sees the most recent reason. Walks the `cause` chain when checking
808
+ * for timeouts: lazy-load timeouts arrive wrapped in `LazyPluginError` (the
809
+ * timeout is the cause), but the operator-visible reason should still be
810
+ * `'timeout'`.
811
+ */
812
+ private _recordFailure(id: string, phase: PluginFailurePhase, error: Error): void {
813
+ const reason: PluginFailureReason = isTimeoutCause(error) ? 'timeout' : 'error';
814
+ const failure: PluginFailure = { id, phase, reason, error, timestamp: Date.now() };
815
+ log.warn('plugin failed', { id, phase, reason, error: error.message });
816
+ this._update(this._failedAtom, (current) => [...current.filter((entry) => entry.id !== id), failure]);
530
817
  }
531
818
 
532
- private _getPlugin(id: string): Plugin.Plugin | undefined {
533
- return this._get(this._pluginsAtom).find((plugin) => plugin.meta.id === id);
819
+ /**
820
+ * Fire-and-forget disable of a failed plugin. Forked because a failure can
821
+ * happen mid-activation chain — yielding a `disable` inline would deadlock
822
+ * on the shared semaphores. Core plugins are skipped (the host opted into
823
+ * them being non-removable; the failure record is enough signal).
824
+ */
825
+ private _scheduleAutoDisable(id: string): void {
826
+ if (this._get(this._coreAtom).includes(id)) {
827
+ return;
828
+ }
829
+ if (!this._get(this._enabledAtom).includes(id)) {
830
+ return;
831
+ }
832
+ this._runForkedFiber(
833
+ this.disable(id).pipe(
834
+ Effect.tapError((error) => Effect.sync(() => log.warn('auto-disable failed', { id, error }))),
835
+ Effect.ignore,
836
+ ),
837
+ );
534
838
  }
535
839
 
536
840
  private _getActiveModules(): Plugin.PluginModule[] {
@@ -570,6 +874,273 @@ class ManagerImpl implements PluginManager {
570
874
  }
571
875
  }
572
876
 
877
+ private _clearPendingReset(key: string): void {
878
+ const pendingIndex = this._get(this._pendingResetAtom).findIndex((event) => event === key);
879
+ if (pendingIndex !== -1) {
880
+ this._update(this._pendingResetAtom, (pending) => pending.filter((event) => event !== key));
881
+ }
882
+ }
883
+
884
+ //
885
+ // Fiber helpers
886
+ //
887
+
888
+ private _interruptInFlightActivations(): Effect.Effect<void> {
889
+ return Effect.gen(this, function* () {
890
+ const inFlightFibers = yield* Ref.get(this._inFlightFibers);
891
+ yield* Effect.forEach(inFlightFibers, (fiber) => Fiber.interrupt(fiber), {
892
+ concurrency: 'unbounded',
893
+ });
894
+ });
895
+ }
896
+
897
+ private _trackFiber(
898
+ ref: Ref.Ref<Array<Fiber.Fiber<unknown, unknown>>>,
899
+ fiber: Fiber.Fiber<unknown, unknown>,
900
+ ): Effect.Effect<void> {
901
+ return Ref.update(ref, (fibers) => [...fibers, fiber]);
902
+ }
903
+
904
+ private _untrackFiber(
905
+ ref: Ref.Ref<Array<Fiber.Fiber<unknown, unknown>>>,
906
+ fiber: Fiber.Fiber<unknown, unknown>,
907
+ ): Effect.Effect<void> {
908
+ return Ref.update(ref, (fibers) => fibers.filter((trackedFiber) => trackedFiber !== fiber));
909
+ }
910
+
911
+ /**
912
+ * Spawns an effect on the default runtime and registers the resulting fiber in
913
+ * `_inFlightFibers` so {@link shutdown} can interrupt it. Used from sync entry
914
+ * points like {@link remove} where there is no enclosing Effect to fork from;
915
+ * inside an Effect chain prefer the existing track/await/untrack pattern.
916
+ */
917
+ private _runForkedFiber<E>(effect: Effect.Effect<void, E>): void {
918
+ const fiber = Effect.runFork(effect);
919
+ Effect.runSync(this._trackFiber(this._inFlightFibers, fiber));
920
+ Effect.runFork(Fiber.await(fiber).pipe(Effect.andThen(() => this._untrackFiber(this._inFlightFibers, fiber))));
921
+ }
922
+
923
+ //
924
+ // Registration helpers
925
+ //
926
+
927
+ private _addPlugin(plugin: Plugin.Plugin): void {
928
+ log('add plugin', { id: plugin.meta.id });
929
+ // TODO(wittjosiah): Find a way to add a warning for duplicate plugins that doesn't cause log spam.
930
+ this._update(this._pluginsAtom, (plugins) => (plugins.includes(plugin) ? plugins : [...plugins, plugin]));
931
+ }
932
+
933
+ private _removePlugin(id: string): void {
934
+ log('remove plugin', { id });
935
+ this._update(this._pluginsAtom, (plugins) => plugins.filter((plugin) => plugin.meta.id !== id));
936
+ }
937
+
938
+ private _addModule(module: Plugin.PluginModule): void {
939
+ log('add module', { id: module.id });
940
+ // TODO(wittjosiah): Find a way to add a warning for duplicate modules that doesn't cause log spam.
941
+ this._update(this._modulesAtom, (modules) => (modules.includes(module) ? modules : [...modules, module]));
942
+ }
943
+
944
+ private _removeModule(id: string): void {
945
+ log('remove module', { id });
946
+ this._update(this._modulesAtom, (modules) => modules.filter((module) => module.id !== id));
947
+ }
948
+
949
+ //
950
+ // Activation helpers
951
+ //
952
+
953
+ private _activateEvent(
954
+ key: string,
955
+ params: { before?: string; after?: string } | undefined,
956
+ fiber: Fiber.Fiber<unknown, unknown>,
957
+ ): Effect.Effect<boolean, Error> {
958
+ return Effect.gen(this, function* () {
959
+ yield* this._trackFiber(this._inFlightFibers, fiber);
960
+ log('activating', { key, ...params });
961
+ yield* Ref.update(this._activatingEvents, (activating) => Array.append(activating, key));
962
+ this._clearPendingReset(key);
963
+
964
+ const activatingEvents = yield* this._activatingEvents;
965
+ const activatingModules = yield* this._activatingModules;
966
+ const modules = this._getModulesForActivation(key, activatingEvents, activatingModules);
967
+ if (modules.length === 0) {
968
+ log('no modules to activate', { key });
969
+ if (!this._get(this._eventsFiredAtom).includes(key)) {
970
+ this._update(this._eventsFiredAtom, (events) => [...events, key]);
971
+ }
972
+ return false;
973
+ }
974
+
975
+ return yield* this._activateModulesForEvent(key, modules, activatingEvents);
976
+ }).pipe(
977
+ Effect.ensuring(
978
+ Effect.all([
979
+ this._untrackFiber(this._inFlightFibers, fiber),
980
+ Ref.update(this._activatingEvents, (activating) => Array.filter(activating, (event) => event !== key)),
981
+ ]),
982
+ ),
983
+ );
984
+ }
985
+
986
+ private _activateModulesForEvent(
987
+ key: string,
988
+ modules: Plugin.PluginModule[],
989
+ activatingEvents: string[],
990
+ ): Effect.Effect<boolean, Error> {
991
+ const activatingModuleIds = modules.map((module) => module.id);
992
+ return Effect.gen(this, function* () {
993
+ yield* Ref.update(this._activatingModules, (activating) => Array.appendAll(activating, activatingModuleIds));
994
+
995
+ log('activating modules', { key, modules: activatingModuleIds });
996
+ performance.mark(`event:${key}:start`);
997
+ yield* PubSub.publish(this.activation, { event: key, state: 'activating' });
998
+
999
+ yield* this._activateRelatedEvents(key, this._getBeforeEvents(modules, activatingEvents), 'before');
1000
+
1001
+ const capabilities = yield* this._loadCapabilitiesForModules(key, modules);
1002
+ yield* this._contributeCapabilitiesForModules(modules, capabilities);
1003
+
1004
+ yield* this._activateRelatedEvents(key, this._getAfterEvents(modules, activatingEvents), 'after');
1005
+
1006
+ if (!this._get(this._eventsFiredAtom).includes(key)) {
1007
+ this._update(this._eventsFiredAtom, (events) => [...events, key]);
1008
+ }
1009
+
1010
+ performance.mark(`event:${key}:end`);
1011
+ performance.measure(`event:${key}`, `event:${key}:start`, `event:${key}:end`);
1012
+ yield* PubSub.publish(this.activation, { event: key, state: 'activated' });
1013
+ log('activated', { key });
1014
+
1015
+ return true;
1016
+ }).pipe(
1017
+ Effect.ensuring(
1018
+ Ref.update(this._activatingModules, (activating) =>
1019
+ Array.filter(activating, (module) => !activatingModuleIds.includes(module)),
1020
+ ),
1021
+ ),
1022
+ );
1023
+ }
1024
+
1025
+ private _getModulesForActivation(
1026
+ key: string,
1027
+ activatingEvents: string[],
1028
+ activatingModules: string[],
1029
+ ): Plugin.PluginModule[] {
1030
+ return this._getInactiveModulesByEvent(key).filter((module) => {
1031
+ const allOf = ActivationEvent.isAllOf(module.activatesOn);
1032
+ if (!allOf) {
1033
+ return true;
1034
+ }
1035
+
1036
+ // Check to see if all of the events in the `allOf` have been fired.
1037
+ // An event can be considered "fired" if it is in the `eventsFired` list or if it is currently being activated.
1038
+ const events = ActivationEvent.getEvents(module.activatesOn).filter(
1039
+ (event) => ActivationEvent.eventKey(event) !== key,
1040
+ );
1041
+ return (
1042
+ events.every(
1043
+ (event) =>
1044
+ this._get(this._eventsFiredAtom).includes(ActivationEvent.eventKey(event)) ||
1045
+ activatingEvents.includes(ActivationEvent.eventKey(event)),
1046
+ ) && !activatingModules.includes(module.id)
1047
+ );
1048
+ });
1049
+ }
1050
+
1051
+ private _getBeforeEvents(
1052
+ modules: Plugin.PluginModule[],
1053
+ activatingEvents: string[],
1054
+ ): ActivationEvent.ActivationEvent[] {
1055
+ return Function.pipe(
1056
+ modules,
1057
+ Array.flatMap((module) => module.firesBeforeActivation ?? []),
1058
+ HashSet.fromIterable,
1059
+ HashSet.toValues,
1060
+ Array.filter((event) => !activatingEvents.includes(ActivationEvent.eventKey(event))),
1061
+ );
1062
+ }
1063
+
1064
+ private _getAfterEvents(
1065
+ modules: Plugin.PluginModule[],
1066
+ activatingEvents: string[],
1067
+ ): ActivationEvent.ActivationEvent[] {
1068
+ return Function.pipe(
1069
+ modules,
1070
+ Array.flatMap((module) => module.firesAfterActivation ?? []),
1071
+ HashSet.fromIterable,
1072
+ HashSet.toValues,
1073
+ Array.filter((event) => !activatingEvents.includes(ActivationEvent.eventKey(event))),
1074
+ );
1075
+ }
1076
+
1077
+ private _activateRelatedEvents(
1078
+ key: string,
1079
+ events: ActivationEvent.ActivationEvent[],
1080
+ phase: 'before' | 'after',
1081
+ ): Effect.Effect<void, Error> {
1082
+ const logLabel = phase === 'before' ? 'firesBeforeActivation' : 'firesAfterActivation';
1083
+ const eventKey = phase === 'before' ? 'beforeEvents' : 'afterEvents';
1084
+ return Function.pipe(
1085
+ events,
1086
+ Array.map((event) => this.activate(event, phase === 'before' ? { before: key } : { after: key })),
1087
+ Effect.allWith({ concurrency: 'unbounded' }),
1088
+ together(
1089
+ Effect.sleep(Duration.seconds(10)).pipe(
1090
+ Effect.andThen(
1091
+ Effect.sync(() =>
1092
+ log.warn(`${logLabel} is taking a long time`, {
1093
+ event: key,
1094
+ [eventKey]: events.map(ActivationEvent.eventKey),
1095
+ }),
1096
+ ),
1097
+ ),
1098
+ ),
1099
+ ),
1100
+ Effect.asVoid,
1101
+ );
1102
+ }
1103
+
1104
+ //
1105
+ // Module lifecycle helpers
1106
+ //
1107
+
1108
+ private _loadCapabilitiesForModules(
1109
+ key: string,
1110
+ modules: Plugin.PluginModule[],
1111
+ ): Effect.Effect<Capability.Any[][], Error> {
1112
+ return Function.pipe(
1113
+ modules,
1114
+ Array.map((mod) => this._loadModule(mod, key)),
1115
+ Effect.allWith({ concurrency: 'unbounded' }),
1116
+ Effect.catchAll((error) => {
1117
+ return Effect.gen(this, function* () {
1118
+ yield* PubSub.publish(this.activation, { event: key, state: 'error', error });
1119
+ return yield* Effect.fail(error);
1120
+ });
1121
+ }),
1122
+ );
1123
+ }
1124
+
1125
+ private _contributeCapabilitiesForModules(
1126
+ modules: Plugin.PluginModule[],
1127
+ capabilities: Capability.Any[][],
1128
+ ): Effect.Effect<void, Error> {
1129
+ return Function.pipe(
1130
+ modules,
1131
+ Array.zip(capabilities),
1132
+ Array.map(([module, capabilitySet]) => this._contributeCapabilities(module, capabilitySet)),
1133
+ // TODO(wittjosiah): This currently can't be run in parallel, and inserting
1134
+ // any yield between contributions (`Effect.yieldNow()`, `Effect.sleep(0)`)
1135
+ // races the `allOf` activation-event resolver — observed as a System
1136
+ // Error dialog on warm reloads. Contributions must stay strictly
1137
+ // synchronous within an event; React paint slots have to be found at
1138
+ // event boundaries higher up the call chain.
1139
+ Effect.all,
1140
+ Effect.asVoid,
1141
+ );
1142
+ }
1143
+
573
1144
  private _getModuleSemaphore(moduleId: Plugin.PluginModule['id']): Effect.Semaphore {
574
1145
  let semaphore = this._moduleSemaphores.get(moduleId);
575
1146
  if (!semaphore) {
@@ -579,7 +1150,14 @@ class ManagerImpl implements PluginManager {
579
1150
  return semaphore;
580
1151
  }
581
1152
 
582
- private _loadModule = (module: Plugin.PluginModule): Effect.Effect<Capability.Any[], Error> =>
1153
+ // `parentEvent` is the activation event that first triggered this module
1154
+ // load — included in `activating`/`activated` PubSub messages so subscribers
1155
+ // (e.g. the boot loader's status listener) can associate a module with its
1156
+ // triggering event in the trace. The same module may be referenced by
1157
+ // multiple events, but module loads are memoized via `_moduleMemoMap`, so
1158
+ // only the first event to need it will appear here; later events await the
1159
+ // cached deferred without re-publishing.
1160
+ private _loadModule = (module: Plugin.PluginModule, parentEvent: string): Effect.Effect<Capability.Any[], Error> =>
583
1161
  Effect.gen(this, function* () {
584
1162
  const semaphore = this._getModuleSemaphore(module.id);
585
1163
 
@@ -595,18 +1173,34 @@ class ManagerImpl implements PluginManager {
595
1173
  this._moduleMemoMap.set(module.id, deferred);
596
1174
 
597
1175
  const loadEffect = Effect.gen(this, function* () {
598
- log('loading module', { module: module.id });
599
- const [duration, capabilities] = yield* module
600
- .activate()
601
- .pipe(
602
- Effect.provideService(Capability.Service, this.capabilities),
603
- Effect.provideService(Plugin.Service, this),
604
- Effect.timed,
605
- );
1176
+ log('loading module', { module: module.id, parentEvent });
1177
+ performance.mark(`module:${module.id}:start`);
1178
+ yield* PubSub.publish(this.activation, { event: parentEvent, state: 'activating', module: module.id });
1179
+ const pluginId = this._getPluginIdForModule(module.id);
1180
+ const [duration, capabilities] = yield* module.activate().pipe(
1181
+ Effect.provideService(Capability.Service, this.capabilities),
1182
+ Effect.provideService(Plugin.Service, this),
1183
+ // Cap activation so a single misbehaving module can't hold the
1184
+ // event chain open. On timeout the failure is recorded against
1185
+ // the plugin and surfaced as `PluginTimeoutError`.
1186
+ Effect.timeoutFail({
1187
+ duration: this._activationTimeout,
1188
+ onTimeout: () =>
1189
+ new PluginTimeoutError({
1190
+ context: { id: pluginId ?? module.id, module: module.id, phase: 'activation' as PluginFailurePhase },
1191
+ }),
1192
+ }),
1193
+ Effect.timed,
1194
+ );
606
1195
  const normalized = capabilities == null ? [] : Array.isArray(capabilities) ? capabilities : [capabilities];
1196
+ const elapsed = Duration.toMillis(duration);
1197
+ performance.mark(`module:${module.id}:end`);
1198
+ performance.measure(`module:${module.id}`, `module:${module.id}:start`, `module:${module.id}:end`);
1199
+ yield* PubSub.publish(this.activation, { event: parentEvent, state: 'activated', module: module.id });
607
1200
  log('loaded module', {
608
1201
  module: module.id,
609
- elapsed: Duration.toMillis(duration),
1202
+ parentEvent,
1203
+ elapsed,
610
1204
  failed: false,
611
1205
  });
612
1206
  return normalized as Capability.Any[];
@@ -631,7 +1225,7 @@ class ManagerImpl implements PluginManager {
631
1225
  );
632
1226
 
633
1227
  // Fork the load to run in background, completing the deferred when done.
634
- yield* Effect.forkDaemon(
1228
+ const fiber = yield* Effect.forkDaemon(
635
1229
  loadEffect.pipe(
636
1230
  Effect.tap((result) => Deferred.succeed(deferred, result)),
637
1231
  Effect.catchAllCause((cause) => {
@@ -642,10 +1236,20 @@ class ManagerImpl implements PluginManager {
642
1236
  stack: error instanceof Error ? error.stack : undefined,
643
1237
  isDefect: !Cause.isFailure(cause),
644
1238
  });
645
- return Deferred.fail(deferred, error instanceof Error ? error : new Error(String(error)));
1239
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
1240
+ const pluginId = this._getPluginIdForModule(module.id);
1241
+ if (pluginId !== undefined) {
1242
+ this._recordFailure(pluginId, 'activation', normalizedError);
1243
+ this._scheduleAutoDisable(pluginId);
1244
+ }
1245
+ return Deferred.fail(deferred, normalizedError);
646
1246
  }),
647
1247
  ),
648
1248
  );
1249
+ yield* this._trackFiber(this._inFlightFibers, fiber);
1250
+ yield* Effect.forkDaemon(
1251
+ Fiber.await(fiber).pipe(Effect.andThen(() => this._untrackFiber(this._inFlightFibers, fiber))),
1252
+ );
649
1253
 
650
1254
  return deferred;
651
1255
  }).pipe(semaphore.withPermits(1));
@@ -699,6 +1303,22 @@ class ManagerImpl implements PluginManager {
699
1303
  */
700
1304
  export const make = (options: ManagerOptions): PluginManager => new ManagerImpl(options);
701
1305
 
1306
+ /**
1307
+ * True when `error` (or anything along its `cause` chain) is a
1308
+ * {@link PluginTimeoutError}. Lazy-load timeouts wrap the timeout inside
1309
+ * `LazyPluginError`, so a shallow check on the outer error misses them.
1310
+ * Bounded depth so a circular chain can't loop forever.
1311
+ */
1312
+ const isTimeoutCause = (error: unknown, depth = 0): boolean => {
1313
+ if (depth > 5 || !(error instanceof Error)) {
1314
+ return false;
1315
+ }
1316
+ if (PluginTimeoutError.is(error)) {
1317
+ return true;
1318
+ }
1319
+ return isTimeoutCause((error as Error & { cause?: unknown }).cause, depth + 1);
1320
+ };
1321
+
702
1322
  /**
703
1323
  * Runs an effect concurrently with another effect.
704
1324
  * If the first effect completes, the second effect is interrupted.