@dxos/app-framework 0.8.4-main.74a063c4e0 → 0.8.4-main.765dc60934

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 (307) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/{capability-OP63CD5N.mjs → capability-HRFPP52W.mjs} +11 -11
  4. package/dist/lib/browser/capability-HRFPP52W.mjs.map +7 -0
  5. package/dist/lib/browser/{capability-BBBBAPDI.mjs → capability-VQ236TY5.mjs} +10 -10
  6. package/dist/lib/browser/{chunk-T3Y4AEKX.mjs → chunk-23D4SJUE.mjs} +3 -3
  7. package/dist/lib/browser/{chunk-T3Y4AEKX.mjs.map → chunk-23D4SJUE.mjs.map} +1 -1
  8. package/dist/lib/browser/{chunk-2CKCJ6PN.mjs → chunk-3JWJXGLK.mjs} +1 -1
  9. package/dist/lib/browser/{chunk-2CKCJ6PN.mjs.map → chunk-3JWJXGLK.mjs.map} +1 -1
  10. package/dist/lib/browser/{chunk-I34GF4NG.mjs → chunk-45CHLTBV.mjs} +2 -2
  11. package/dist/lib/browser/{chunk-QSXYHXCE.mjs → chunk-66IXTIVK.mjs} +1 -1
  12. package/dist/lib/browser/{chunk-QSXYHXCE.mjs.map → chunk-66IXTIVK.mjs.map} +2 -2
  13. package/dist/lib/browser/chunk-6GZYCP73.mjs +422 -0
  14. package/dist/lib/browser/chunk-6GZYCP73.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-7VZJR2OA.mjs +581 -0
  16. package/dist/lib/browser/chunk-7VZJR2OA.mjs.map +7 -0
  17. package/dist/lib/browser/{chunk-TGX63LTL.mjs → chunk-FJ4765WW.mjs} +1 -1
  18. package/dist/lib/browser/{chunk-TGX63LTL.mjs.map → chunk-FJ4765WW.mjs.map} +2 -2
  19. package/dist/lib/browser/chunk-FO3IYSLV.mjs +68 -0
  20. package/dist/lib/browser/chunk-FO3IYSLV.mjs.map +7 -0
  21. package/dist/lib/browser/{chunk-FU4GAFUQ.mjs → chunk-G6Q2M2M5.mjs} +85 -15
  22. package/dist/lib/browser/chunk-G6Q2M2M5.mjs.map +7 -0
  23. package/dist/lib/browser/chunk-J43JDWAV.mjs +1445 -0
  24. package/dist/lib/browser/chunk-J43JDWAV.mjs.map +7 -0
  25. package/dist/lib/browser/{chunk-JKWMHZP6.mjs → chunk-WBHCSOBW.mjs} +2 -2
  26. package/dist/lib/browser/chunk-WBHCSOBW.mjs.map +7 -0
  27. package/dist/lib/browser/chunk-XLYULXAO.mjs +469 -0
  28. package/dist/lib/browser/chunk-XLYULXAO.mjs.map +7 -0
  29. package/dist/lib/browser/{chunk-F7FW2RK2.mjs → chunk-ZGJAZSNE.mjs} +7 -32
  30. package/dist/lib/browser/chunk-ZGJAZSNE.mjs.map +7 -0
  31. package/dist/lib/browser/cli/index.mjs +14 -28
  32. package/dist/lib/browser/cli/index.mjs.map +3 -3
  33. package/dist/lib/browser/common/activation-events.mjs +7 -7
  34. package/dist/lib/browser/common/capabilities.mjs +7 -7
  35. package/dist/lib/browser/core/activation-event.mjs +1 -1
  36. package/dist/lib/browser/core/capability.mjs +1 -1
  37. package/dist/lib/browser/core/plugin-manager.mjs +8 -4
  38. package/dist/lib/browser/core/plugin.mjs +12 -2
  39. package/dist/lib/browser/core/url-loader.mjs +17 -5
  40. package/dist/lib/browser/index.mjs +38 -21
  41. package/dist/lib/browser/index.mjs.map +3 -3
  42. package/dist/lib/browser/{invoker-capability-H5PPENOC.mjs → invoker-capability-CEL2L6GX.mjs} +12 -11
  43. package/dist/lib/browser/invoker-capability-CEL2L6GX.mjs.map +7 -0
  44. package/dist/lib/browser/meta.json +1 -1
  45. package/dist/lib/browser/testing/index.mjs +149 -28
  46. package/dist/lib/browser/testing/index.mjs.map +4 -4
  47. package/dist/lib/browser/testing/react.mjs +78 -0
  48. package/dist/lib/browser/testing/react.mjs.map +7 -0
  49. package/dist/lib/browser/ui/index.mjs +18 -14
  50. package/dist/lib/node-esm/{capability-AWBEMRYR.mjs → capability-GS3XJH74.mjs} +10 -10
  51. package/dist/lib/node-esm/{capability-WFEG6CIZ.mjs → capability-I5P7ACFL.mjs} +11 -11
  52. package/dist/lib/node-esm/capability-I5P7ACFL.mjs.map +7 -0
  53. package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs → chunk-37Z53PXZ.mjs} +1 -1
  54. package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs.map → chunk-37Z53PXZ.mjs.map} +2 -2
  55. package/dist/lib/node-esm/chunk-4RJL37GN.mjs +423 -0
  56. package/dist/lib/node-esm/chunk-4RJL37GN.mjs.map +7 -0
  57. package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs → chunk-6XW6LET6.mjs} +2 -2
  58. package/dist/lib/node-esm/{chunk-URWHJQT2.mjs → chunk-D347W3KO.mjs} +7 -32
  59. package/dist/lib/node-esm/chunk-D347W3KO.mjs.map +7 -0
  60. package/dist/lib/node-esm/chunk-DW45NFLW.mjs +1446 -0
  61. package/dist/lib/node-esm/chunk-DW45NFLW.mjs.map +7 -0
  62. package/dist/lib/node-esm/chunk-EI4LYXFY.mjs +470 -0
  63. package/dist/lib/node-esm/chunk-EI4LYXFY.mjs.map +7 -0
  64. package/dist/lib/node-esm/{chunk-ULUEXB7Q.mjs → chunk-KIFOQXD4.mjs} +85 -15
  65. package/dist/lib/node-esm/chunk-KIFOQXD4.mjs.map +7 -0
  66. package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs → chunk-SBS2YMPT.mjs} +3 -3
  67. package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs.map → chunk-SBS2YMPT.mjs.map} +1 -1
  68. package/dist/lib/node-esm/{chunk-42KBWDE4.mjs → chunk-SDJ4B2LU.mjs} +1 -1
  69. package/dist/lib/node-esm/{chunk-42KBWDE4.mjs.map → chunk-SDJ4B2LU.mjs.map} +1 -1
  70. package/dist/lib/node-esm/chunk-V24UWT36.mjs +582 -0
  71. package/dist/lib/node-esm/chunk-V24UWT36.mjs.map +7 -0
  72. package/dist/lib/node-esm/{chunk-G3RTFSNG.mjs → chunk-WFSRZKBP.mjs} +2 -2
  73. package/dist/lib/node-esm/chunk-WFSRZKBP.mjs.map +7 -0
  74. package/dist/lib/node-esm/chunk-WK7OIQKI.mjs +70 -0
  75. package/dist/lib/node-esm/chunk-WK7OIQKI.mjs.map +7 -0
  76. package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs → chunk-XOCUANHO.mjs} +1 -1
  77. package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs.map → chunk-XOCUANHO.mjs.map} +2 -2
  78. package/dist/lib/node-esm/cli/index.mjs +14 -28
  79. package/dist/lib/node-esm/cli/index.mjs.map +3 -3
  80. package/dist/lib/node-esm/common/activation-events.mjs +7 -7
  81. package/dist/lib/node-esm/common/capabilities.mjs +7 -7
  82. package/dist/lib/node-esm/core/activation-event.mjs +1 -1
  83. package/dist/lib/node-esm/core/capability.mjs +1 -1
  84. package/dist/lib/node-esm/core/plugin-manager.mjs +8 -4
  85. package/dist/lib/node-esm/core/plugin.mjs +12 -2
  86. package/dist/lib/node-esm/core/url-loader.mjs +17 -5
  87. package/dist/lib/node-esm/index.mjs +38 -21
  88. package/dist/lib/node-esm/index.mjs.map +3 -3
  89. package/dist/lib/node-esm/{invoker-capability-S3ZA527J.mjs → invoker-capability-QKUKN2MF.mjs} +12 -11
  90. package/dist/lib/node-esm/invoker-capability-QKUKN2MF.mjs.map +7 -0
  91. package/dist/lib/node-esm/meta.json +1 -1
  92. package/dist/lib/node-esm/testing/index.mjs +149 -28
  93. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  94. package/dist/lib/node-esm/testing/react.mjs +79 -0
  95. package/dist/lib/node-esm/testing/react.mjs.map +7 -0
  96. package/dist/lib/node-esm/ui/index.mjs +18 -14
  97. package/dist/plugin/node-esm/index.mjs +542 -33
  98. package/dist/plugin/node-esm/index.mjs.map +4 -4
  99. package/dist/plugin/node-esm/meta.json +1 -1
  100. package/dist/types/src/common/annotations.d.ts +1 -0
  101. package/dist/types/src/common/annotations.d.ts.map +1 -0
  102. package/dist/types/src/common/capabilities.d.ts +2 -1
  103. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  104. package/dist/types/src/common/operations.d.ts +1 -1
  105. package/dist/types/src/common/operations.d.ts.map +1 -1
  106. package/dist/types/src/core/activation-event.d.ts +4 -4
  107. package/dist/types/src/core/activation-event.d.ts.map +1 -1
  108. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  109. package/dist/types/src/core/capability.d.ts +2 -2
  110. package/dist/types/src/core/capability.d.ts.map +1 -1
  111. package/dist/types/src/core/edge-registry-plugin-provider.d.ts +30 -0
  112. package/dist/types/src/core/edge-registry-plugin-provider.d.ts.map +1 -0
  113. package/dist/types/src/core/index.d.ts +5 -0
  114. package/dist/types/src/core/index.d.ts.map +1 -1
  115. package/dist/types/src/core/plugin-asset-cache.d.ts +71 -0
  116. package/dist/types/src/core/plugin-asset-cache.d.ts.map +1 -0
  117. package/dist/types/src/core/plugin-manager.d.ts +230 -5
  118. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  119. package/dist/types/src/core/plugin-manifest.d.ts +101 -0
  120. package/dist/types/src/core/plugin-manifest.d.ts.map +1 -0
  121. package/dist/types/src/core/plugin-manifest.test.d.ts +2 -0
  122. package/dist/types/src/core/plugin-manifest.test.d.ts.map +1 -0
  123. package/dist/types/src/core/plugin.d.ts +164 -6
  124. package/dist/types/src/core/plugin.d.ts.map +1 -1
  125. package/dist/types/src/core/registry.d.ts +107 -0
  126. package/dist/types/src/core/registry.d.ts.map +1 -0
  127. package/dist/types/src/core/url-loader.d.ts +105 -3
  128. package/dist/types/src/core/url-loader.d.ts.map +1 -1
  129. package/dist/types/src/helpers.d.ts.map +1 -1
  130. package/dist/types/src/plugin-operation/history/capability.d.ts.map +1 -1
  131. package/dist/types/src/plugin-operation/history/errors.d.ts +6 -6
  132. package/dist/types/src/plugin-operation/history/errors.d.ts.map +1 -1
  133. package/dist/types/src/plugin-operation/history/history-tracker.d.ts +1 -1
  134. package/dist/types/src/plugin-operation/history/history-tracker.d.ts.map +1 -1
  135. package/dist/types/src/plugin-operation/history/types.d.ts +1 -1
  136. package/dist/types/src/plugin-operation/history/types.d.ts.map +1 -1
  137. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts +1 -1
  138. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts.map +1 -1
  139. package/dist/types/src/plugin-operation/history/undo-registry.d.ts +1 -1
  140. package/dist/types/src/plugin-operation/history/undo-registry.d.ts.map +1 -1
  141. package/dist/types/src/plugin-operation/invoker-capability.d.ts +1 -1
  142. package/dist/types/src/plugin-operation/invoker-capability.d.ts.map +1 -1
  143. package/dist/types/src/plugin-operation/meta.d.ts.map +1 -1
  144. package/dist/types/src/plugin-operation/testing.d.ts +2 -1
  145. package/dist/types/src/plugin-operation/testing.d.ts.map +1 -1
  146. package/dist/types/src/plugin-runtime/capability.d.ts +1 -1
  147. package/dist/types/src/plugin-runtime/capability.d.ts.map +1 -1
  148. package/dist/types/src/plugin-runtime/meta.d.ts.map +1 -1
  149. package/dist/types/src/testing/harness.d.ts +67 -0
  150. package/dist/types/src/testing/harness.d.ts.map +1 -0
  151. package/dist/types/src/testing/index.d.ts +1 -0
  152. package/dist/types/src/testing/index.d.ts.map +1 -1
  153. package/dist/types/src/testing/react.d.ts +27 -0
  154. package/dist/types/src/testing/react.d.ts.map +1 -0
  155. package/dist/types/src/testing/react.test.d.ts +2 -0
  156. package/dist/types/src/testing/react.test.d.ts.map +1 -0
  157. package/dist/types/src/testing/service.d.ts.map +1 -1
  158. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  159. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  160. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  161. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  162. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +64 -0
  163. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +1 -0
  164. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +19 -0
  165. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +1 -0
  166. package/dist/types/src/ui/components/Placeholder/index.d.ts +2 -0
  167. package/dist/types/src/ui/components/Placeholder/index.d.ts.map +1 -0
  168. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
  169. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +16 -4
  170. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  171. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  172. package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts.map +1 -1
  173. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
  174. package/dist/types/src/ui/components/Surface/index.d.ts +16 -6
  175. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  176. package/dist/types/src/ui/components/Surface/types.d.ts +110 -9
  177. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  178. package/dist/types/src/ui/components/Surface/types.test.d.ts +2 -0
  179. package/dist/types/src/ui/components/Surface/types.test.d.ts.map +1 -0
  180. package/dist/types/src/ui/components/index.d.ts +1 -0
  181. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  182. package/dist/types/src/ui/hooks/useApp.d.ts +30 -3
  183. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  184. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  185. package/dist/types/src/ui/hooks/useLoading.d.ts.map +1 -1
  186. package/dist/types/src/ui/hooks/useSettingsState.d.ts.map +1 -1
  187. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +34 -0
  188. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +1 -0
  189. package/dist/types/src/vite-plugin/boot-loader/index.d.ts +2 -0
  190. package/dist/types/src/vite-plugin/boot-loader/index.d.ts.map +1 -0
  191. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts +51 -0
  192. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts.map +1 -0
  193. package/dist/types/src/vite-plugin/composer/index.d.ts +34 -0
  194. package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -0
  195. package/dist/types/src/vite-plugin/import-map/index.d.ts +28 -0
  196. package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -0
  197. package/dist/types/src/vite-plugin/index.d.ts +4 -2
  198. package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
  199. package/dist/types/src/vite-plugin/manifest.d.ts +41 -0
  200. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -0
  201. package/dist/types/src/vite-plugin/manifest.test.d.ts +2 -0
  202. package/dist/types/src/vite-plugin/manifest.test.d.ts.map +1 -0
  203. package/dist/types/src/vite-plugin/packages.d.ts +10 -4
  204. package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
  205. package/dist/types/tsconfig.tsbuildinfo +1 -1
  206. package/moon.yml +1 -0
  207. package/package.json +36 -60
  208. package/src/cli/cli.ts +1 -1
  209. package/src/common/annotations.ts +3 -0
  210. package/src/common/capabilities.ts +2 -1
  211. package/src/common/operations.ts +1 -1
  212. package/src/core/capability.ts +1 -1
  213. package/src/core/edge-registry-plugin-provider.ts +92 -0
  214. package/src/core/index.ts +5 -0
  215. package/src/core/plugin-asset-cache.ts +60 -0
  216. package/src/core/plugin-manager.test.ts +793 -7
  217. package/src/core/plugin-manager.ts +846 -44
  218. package/src/core/plugin-manifest.test.ts +75 -0
  219. package/src/core/plugin-manifest.ts +134 -0
  220. package/src/core/plugin.ts +170 -10
  221. package/src/core/registry.ts +163 -0
  222. package/src/core/url-loader.test.ts +148 -6
  223. package/src/core/url-loader.ts +284 -44
  224. package/src/plugin-operation/OperationPlugin.ts +2 -2
  225. package/src/plugin-operation/history/capability.ts +1 -1
  226. package/src/plugin-operation/history/history-tracker.test.ts +2 -1
  227. package/src/plugin-operation/history/history-tracker.ts +1 -1
  228. package/src/plugin-operation/history/types.ts +1 -1
  229. package/src/plugin-operation/history/undo-mapping.ts +1 -1
  230. package/src/plugin-operation/history/undo-registry.ts +1 -1
  231. package/src/plugin-operation/invoker-capability.ts +2 -1
  232. package/src/plugin-operation/meta.ts +1 -0
  233. package/src/plugin-operation/testing.ts +2 -1
  234. package/src/plugin-runtime/RuntimePlugin.ts +2 -2
  235. package/src/plugin-runtime/meta.ts +1 -0
  236. package/src/testing/harness.ts +229 -0
  237. package/src/testing/index.ts +1 -0
  238. package/src/testing/react.test.tsx +48 -0
  239. package/src/testing/react.tsx +113 -0
  240. package/src/testing/service.ts +1 -1
  241. package/src/testing/withPluginManager.stories.tsx +1 -1
  242. package/src/testing/withPluginManager.tsx +0 -2
  243. package/src/ui/components/App/App.stories.tsx +2 -6
  244. package/src/ui/components/App/App.tsx +25 -2
  245. package/src/ui/components/Placeholder/Placeholder.stories.tsx +77 -0
  246. package/src/ui/components/Placeholder/Placeholder.tsx +155 -0
  247. package/src/ui/components/Placeholder/index.ts +5 -0
  248. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +4 -2
  249. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
  250. package/src/ui/components/Surface/SurfaceComponent.tsx +85 -48
  251. package/src/ui/components/Surface/SurfaceInfo.tsx +0 -1
  252. package/src/ui/components/Surface/index.ts +20 -1
  253. package/src/ui/components/Surface/types.test.ts +126 -0
  254. package/src/ui/components/Surface/types.ts +164 -12
  255. package/src/ui/components/index.ts +1 -0
  256. package/src/ui/hooks/useApp.test.tsx +1 -1
  257. package/src/ui/hooks/useApp.tsx +177 -42
  258. package/src/ui/hooks/useLoading.tsx +14 -6
  259. package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +270 -0
  260. package/src/vite-plugin/boot-loader/boot-loader.css +321 -0
  261. package/src/vite-plugin/boot-loader/boot-loader.js +325 -0
  262. package/src/vite-plugin/boot-loader/index.ts +5 -0
  263. package/src/vite-plugin/boot-loader/loader.ts +123 -0
  264. package/src/vite-plugin/composer/index.ts +306 -0
  265. package/src/vite-plugin/import-map/index.ts +527 -0
  266. package/src/vite-plugin/index.ts +6 -2
  267. package/src/vite-plugin/manifest.test.ts +46 -0
  268. package/src/vite-plugin/manifest.ts +57 -0
  269. package/src/vite-plugin/packages.ts +168 -10
  270. package/tsconfig.json +16 -1
  271. package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
  272. package/dist/lib/browser/capability-OP63CD5N.mjs.map +0 -7
  273. package/dist/lib/browser/chunk-F7FW2RK2.mjs.map +0 -7
  274. package/dist/lib/browser/chunk-FU4GAFUQ.mjs.map +0 -7
  275. package/dist/lib/browser/chunk-GX4TUNM6.mjs +0 -963
  276. package/dist/lib/browser/chunk-GX4TUNM6.mjs.map +0 -7
  277. package/dist/lib/browser/chunk-JKWMHZP6.mjs.map +0 -7
  278. package/dist/lib/browser/chunk-LVJW5EFU.mjs +0 -157
  279. package/dist/lib/browser/chunk-LVJW5EFU.mjs.map +0 -7
  280. package/dist/lib/browser/chunk-RFSO3JRG.mjs +0 -1
  281. package/dist/lib/browser/chunk-RFSO3JRG.mjs.map +0 -7
  282. package/dist/lib/browser/chunk-WPE6AL7I.mjs +0 -905
  283. package/dist/lib/browser/chunk-WPE6AL7I.mjs.map +0 -7
  284. package/dist/lib/browser/invoker-capability-H5PPENOC.mjs.map +0 -7
  285. package/dist/lib/node-esm/capability-WFEG6CIZ.mjs.map +0 -7
  286. package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs +0 -158
  287. package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs.map +0 -7
  288. package/dist/lib/node-esm/chunk-BCEOLX47.mjs +0 -964
  289. package/dist/lib/node-esm/chunk-BCEOLX47.mjs.map +0 -7
  290. package/dist/lib/node-esm/chunk-EL3R25OQ.mjs +0 -2
  291. package/dist/lib/node-esm/chunk-EL3R25OQ.mjs.map +0 -7
  292. package/dist/lib/node-esm/chunk-G3RTFSNG.mjs.map +0 -7
  293. package/dist/lib/node-esm/chunk-LQKOTNJW.mjs +0 -906
  294. package/dist/lib/node-esm/chunk-LQKOTNJW.mjs.map +0 -7
  295. package/dist/lib/node-esm/chunk-ULUEXB7Q.mjs.map +0 -7
  296. package/dist/lib/node-esm/chunk-URWHJQT2.mjs.map +0 -7
  297. package/dist/lib/node-esm/invoker-capability-S3ZA527J.mjs.map +0 -7
  298. package/dist/types/src/vite-plugin/composer-plugin.d.ts +0 -18
  299. package/dist/types/src/vite-plugin/composer-plugin.d.ts.map +0 -1
  300. package/dist/types/src/vite-plugin/import-map-plugin.d.ts +0 -16
  301. package/dist/types/src/vite-plugin/import-map-plugin.d.ts.map +0 -1
  302. package/src/vite-plugin/composer-plugin.ts +0 -128
  303. package/src/vite-plugin/import-map-plugin.ts +0 -314
  304. /package/dist/lib/browser/{capability-BBBBAPDI.mjs.map → capability-VQ236TY5.mjs.map} +0 -0
  305. /package/dist/lib/browser/{chunk-I34GF4NG.mjs.map → chunk-45CHLTBV.mjs.map} +0 -0
  306. /package/dist/lib/node-esm/{capability-AWBEMRYR.mjs.map → capability-GS3XJH74.mjs.map} +0 -0
  307. /package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs.map → chunk-6XW6LET6.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,12 +81,54 @@ 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
134
  export type ActivationMessage = {
@@ -53,6 +147,13 @@ export interface PluginManager {
53
147
  readonly activation: PubSub.PubSub<ActivationMessage>;
54
148
  readonly capabilities: CapabilityManager.CapabilityManager;
55
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;
56
157
 
57
158
  readonly plugins: Atom.Atom<readonly Plugin.Plugin[]>;
58
159
  readonly core: Atom.Atom<readonly string[]>;
@@ -61,6 +162,19 @@ export interface PluginManager {
61
162
  readonly active: Atom.Atom<readonly string[]>;
62
163
  readonly eventsFired: Atom.Atom<readonly string[]>;
63
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[]>;
64
178
 
65
179
  getPlugins(): readonly Plugin.Plugin[];
66
180
  getCore(): readonly string[];
@@ -69,11 +183,77 @@ export interface PluginManager {
69
183
  getActive(): readonly string[];
70
184
  getEventsFired(): readonly string[];
71
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;
72
194
 
73
- add(id: string): Effect.Effect<boolean, Error>;
74
- enable(id: string): Effect.Effect<boolean, Error>;
75
- remove(id: string): boolean;
76
- disable(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>;
201
+
202
+ /**
203
+ * Enables a plugin.
204
+ *
205
+ * Default behavior auto-resolves the plugin's declared `dependsOn` closure:
206
+ * missing entries that exist in the plugin registry catalog are installed via
207
+ * {@link add}, then enabled in dependency-first order. Set `resolveDependencies`
208
+ * to `false` to enable only the named plugin and skip the closure walk
209
+ * entirely — useful when substituting an alternative plugin that satisfies
210
+ * the dependent's capability needs in its own way.
211
+ */
212
+ enable(id: string, opts?: { resolveDependencies?: boolean }): Effect.Effect<boolean, Error>;
213
+
214
+ /**
215
+ * Removes a plugin from the manager (disables then unregisters).
216
+ *
217
+ * Honors the same cascade option as {@link disable}.
218
+ */
219
+ remove(id: string, opts?: { cascade?: boolean }): Effect.Effect<boolean, Error>;
220
+
221
+ /**
222
+ * Disables a plugin.
223
+ *
224
+ * By default, cascades to currently-enabled dependents (transitively, leaves
225
+ * first) so disabling a depended-upon plugin never leaves its dependents
226
+ * stranded. Pass `cascade: false` to disable only the named plugin and leave
227
+ * its dependents enabled-but-broken — VS Code-style disable parity for
228
+ * callers that want the escape hatch (e.g. when swapping in an alternative
229
+ * implementation that satisfies the dependents' needs in its own way).
230
+ *
231
+ * Fails with {@link Plugin.PluginDependencyError} (`reason: 'core-dependent'`)
232
+ * when cascading would require disabling a core plugin; UI flows should
233
+ * surface their own confirmation before calling `disable` with the default.
234
+ */
235
+ disable(id: string, opts?: { cascade?: boolean }): Effect.Effect<boolean, Error>;
236
+
237
+ /**
238
+ * Returns the plugin ids that the given plugin declares as dependencies.
239
+ *
240
+ * Walks `meta.dependsOn` from both registered plugins and the plugin registry
241
+ * catalog so callers can preview the closure for a plugin that isn't yet
242
+ * installed. With `transitive: true` (default), returns the full dependency
243
+ * closure in dependency-first order (deps before dependents). Without it,
244
+ * returns the direct declarations only.
245
+ */
246
+ getDependencies(id: string, opts?: { transitive?: boolean }): readonly string[];
247
+
248
+ /**
249
+ * Returns the plugin ids that declare the given plugin as a dependency.
250
+ *
251
+ * Walks `meta.dependsOn` over registered plugins. With `transitive: true`
252
+ * (default), returns the full reverse closure. With `enabledOnly: true`,
253
+ * filters to currently-enabled dependents — used by UI flows to preview what
254
+ * a cascading disable would touch.
255
+ */
256
+ getDependents(id: string, opts?: { transitive?: boolean; enabledOnly?: boolean }): readonly string[];
77
257
  // TODO(wittjosiah): Improve error typing.
78
258
  activate(
79
259
  event: ActivationEvent.ActivationEvent | string,
@@ -105,6 +285,7 @@ class ManagerImpl implements PluginManager {
105
285
  readonly activation = Effect.runSync(PubSub.unbounded<ActivationMessage>());
106
286
  readonly capabilities: CapabilityManager.CapabilityManager;
107
287
  readonly registry: Registry.Registry;
288
+ readonly pluginRegistry: PluginRegistry.Manager;
108
289
 
109
290
  private readonly _pluginsAtom: Atom.Writable<Plugin.Plugin[]>;
110
291
  private readonly _coreAtom: Atom.Writable<string[]>;
@@ -113,15 +294,41 @@ class ManagerImpl implements PluginManager {
113
294
  private readonly _activeAtom: Atom.Writable<string[]>;
114
295
  private readonly _eventsFiredAtom: Atom.Writable<string[]>;
115
296
  private readonly _pendingResetAtom: Atom.Writable<string[]>;
297
+ private readonly _failedAtom: Atom.Writable<PluginFailure[]>;
116
298
  private readonly _pluginLoader: ManagerOptions['pluginLoader'];
299
+ private readonly _onRemove: ManagerOptions['onRemove'];
300
+ private readonly _loadTimeout: Duration.DurationInput;
301
+ private readonly _activationTimeout: Duration.DurationInput;
117
302
  private readonly _capabilities = new Map<string, Capability.Any[]>();
118
303
  private readonly _moduleMemoMap = new Map<Plugin.PluginModule['id'], Deferred.Deferred<Capability.Any[], Error>>();
119
304
  private readonly _moduleSemaphores = new Map<Plugin.PluginModule['id'], Effect.Semaphore>();
305
+ // Coalesces concurrent `_resolveLazyPlugin` calls per plugin id. Without
306
+ // this, two callers entering `enable(id)` before the swap completes would
307
+ // each invoke `mod.default(options)` and produce distinct module objects,
308
+ // defeating `_addModule`'s reference-equality dedupe and racing the
309
+ // `_pluginsAtom` swap.
310
+ private readonly _resolvingPlugins = new Map<string, Deferred.Deferred<Plugin.Plugin, Plugin.LazyPluginError>>();
311
+ // Tracks dev-source plugins (loaded via a Vite dev server) keyed by id.
312
+ // When `shadow` is present, the entry has displaced an existing plugin —
313
+ // `remove` reinstates it and re-enables iff `wasEnabled`. Entries without a
314
+ // shadow are dev plugins with no underlying registry/builtin to restore.
315
+ // The atom mirrors the map's keys for UI subscribers (they don't need the
316
+ // shadow internals); the two stay in sync via {@link _markDev}/{@link _unmarkDev}.
317
+ private readonly _devPlugins = new Map<string, { shadow?: { plugin: Plugin.Plugin; wasEnabled: boolean } }>();
318
+ private readonly _devPluginIdsAtom: Atom.Writable<string[]>;
120
319
  private readonly _activatingEvents = Effect.runSync(Ref.make<string[]>([]));
121
320
  private readonly _activatingModules = Effect.runSync(Ref.make<string[]>([]));
122
321
  private readonly _inFlightFibers = Effect.runSync(Ref.make<Array<Fiber.Fiber<unknown, unknown>>>([]));
123
322
  private readonly _shutdownSemaphore = Effect.runSync(Effect.makeSemaphore(1));
124
323
  private readonly _shuttingDown = Effect.runSync(Ref.make(false));
324
+ // Tracks the constructor-launched core/enabled `enable()` calls so that
325
+ // `activate` can wait for module registration before dispatching events.
326
+ // Lazy plugins make `enable` asynchronous (a dynamic `import()` happens
327
+ // inside it), so without this synchronization an `activate` triggered
328
+ // immediately after `make` could fire on an empty module set. Failures
329
+ // are wrapped in `PluginInitializationError` so awaiters get a tagged
330
+ // error rather than the wide `Error` produced by the underlying chain.
331
+ private readonly _initialization = Effect.runSync(Deferred.make<void, PluginInitializationError>());
125
332
 
126
333
  constructor({
127
334
  pluginLoader,
@@ -129,13 +336,21 @@ class ManagerImpl implements PluginManager {
129
336
  core = plugins.map(({ meta }) => meta.id),
130
337
  enabled = [],
131
338
  registry,
339
+ pluginRegistryProvider,
340
+ onRemove,
341
+ loadTimeout = DEFAULT_LOAD_TIMEOUT,
342
+ activationTimeout = DEFAULT_ACTIVATION_TIMEOUT,
132
343
  }: ManagerOptions) {
133
344
  this.registry = registry ?? Registry.make();
134
345
  this.capabilities = CapabilityManager.make({
135
346
  registry: this.registry,
136
347
  });
348
+ this.pluginRegistry = new PluginRegistry.Manager(pluginRegistryProvider, this.registry);
137
349
 
138
350
  this._pluginLoader = pluginLoader;
351
+ this._onRemove = onRemove;
352
+ this._loadTimeout = loadTimeout;
353
+ this._activationTimeout = activationTimeout;
139
354
  this._pluginsAtom = Atom.make(plugins).pipe(Atom.keepAlive);
140
355
  this._coreAtom = Atom.make(core).pipe(Atom.keepAlive);
141
356
  this._enabledAtom = Atom.make(enabled).pipe(Atom.keepAlive);
@@ -143,8 +358,22 @@ class ManagerImpl implements PluginManager {
143
358
  this._activeAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
144
359
  this._eventsFiredAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
145
360
  this._pendingResetAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
361
+ this._failedAtom = Atom.make<PluginFailure[]>([]).pipe(Atom.keepAlive);
362
+ this._devPluginIdsAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
146
363
  plugins.forEach((plugin) => this._addPlugin(plugin));
147
- void Effect.all([...core, ...enabled].map((id) => this.enable(id))).pipe(runAndForwardErrors);
364
+ // Dedupe before mapping to `enable` — `core` and `enabled` may overlap (an
365
+ // app-supplied plugin can be in both), and concurrent `enable(id)` calls
366
+ // for the same id are not idempotent (each would re-run the lazy resolve
367
+ // and double-register modules). `new Set([...])` preserves first-seen
368
+ // order which matches the natural core-before-enabled precedence.
369
+ const initialIds = [...new Set([...core, ...enabled])];
370
+ void Effect.all(initialIds.map((id) => this.enable(id)))
371
+ .pipe(
372
+ Effect.mapError((cause) => new PluginInitializationError({ cause })),
373
+ Effect.tap(() => Deferred.succeed(this._initialization, undefined)),
374
+ Effect.tapErrorCause((cause) => Deferred.failCause(this._initialization, cause)),
375
+ )
376
+ .pipe(runAndForwardErrors);
148
377
  }
149
378
 
150
379
  get plugins(): Atom.Atom<readonly Plugin.Plugin[]> {
@@ -190,6 +419,20 @@ class ManagerImpl implements PluginManager {
190
419
  return this._pendingResetAtom;
191
420
  }
192
421
 
422
+ /**
423
+ * Plugins that failed to load or activate.
424
+ */
425
+ get failed(): Atom.Atom<readonly PluginFailure[]> {
426
+ return this._failedAtom;
427
+ }
428
+
429
+ /**
430
+ * Ids of currently-registered plugins that came from a dev source.
431
+ */
432
+ get devPluginIds(): Atom.Atom<readonly string[]> {
433
+ return this._devPluginIdsAtom;
434
+ }
435
+
193
436
  getPlugins(): readonly Plugin.Plugin[] {
194
437
  return this._get(this._pluginsAtom);
195
438
  }
@@ -218,31 +461,229 @@ class ManagerImpl implements PluginManager {
218
461
  return this._get(this._pendingResetAtom);
219
462
  }
220
463
 
464
+ getFailed(): readonly PluginFailure[] {
465
+ return this._get(this._failedAtom);
466
+ }
467
+
468
+ getDevPluginIds(): readonly string[] {
469
+ return this._get(this._devPluginIdsAtom);
470
+ }
471
+
472
+ /**
473
+ * Marks `id` as dev-sourced. If the plugin displaced an existing one, pass
474
+ * the shadow snapshot so `remove` can restore it. Repeat calls (e.g. a dev
475
+ * plugin reload) preserve the original shadow target — restoration always
476
+ * unwinds back to the real underlying plugin, never an intermediate dev build.
477
+ */
478
+ private _markDev(id: string, shadow?: { plugin: Plugin.Plugin; wasEnabled: boolean }): void {
479
+ if (this._devPlugins.has(id)) {
480
+ return;
481
+ }
482
+ this._devPlugins.set(id, { shadow });
483
+ this._update(this._devPluginIdsAtom, (ids) => (ids.includes(id) ? ids : [...ids, id]));
484
+ }
485
+
486
+ /** Drops the dev-plugin entry and returns its shadow data (if any) for restoration. */
487
+ private _unmarkDev(id: string): { plugin: Plugin.Plugin; wasEnabled: boolean } | undefined {
488
+ const entry = this._devPlugins.get(id);
489
+ this._devPlugins.delete(id);
490
+ this._update(this._devPluginIdsAtom, (ids) => ids.filter((existing) => existing !== id));
491
+ return entry?.shadow;
492
+ }
493
+
494
+ getDependencies(id: string, opts?: { transitive?: boolean }): readonly string[] {
495
+ const transitive = opts?.transitive !== false;
496
+ if (!transitive) {
497
+ return this._directDependencies(id);
498
+ }
499
+ const walk = this._computeDependencyClosure(id);
500
+ // Drop the target itself; callers asked for its dependencies, not the
501
+ // closure including the root.
502
+ return walk.order.filter((depId) => depId !== id);
503
+ }
504
+
505
+ getDependents(id: string, opts?: { transitive?: boolean; enabledOnly?: boolean }): readonly string[] {
506
+ return this._collectDependents(id, {
507
+ transitive: opts?.transitive !== false,
508
+ enabledOnly: opts?.enabledOnly === true,
509
+ });
510
+ }
511
+
512
+ clearFailure(id: string): boolean {
513
+ const current = this._get(this._failedAtom);
514
+ if (!current.some((failure) => failure.id === id)) {
515
+ return false;
516
+ }
517
+ this._set(
518
+ this._failedAtom,
519
+ current.filter((failure) => failure.id !== id),
520
+ );
521
+ return true;
522
+ }
523
+
221
524
  /**
222
525
  * Adds a plugin to the manager via the plugin loader.
526
+ * The plugin is registered but not enabled; call `enable` separately to activate it.
223
527
  * @param id The id of the plugin.
224
528
  */
225
- add(id: string): Effect.Effect<boolean, Error> {
529
+ add(id: string): Effect.Effect<Plugin.Plugin, Error> {
226
530
  return Effect.gen(this, function* () {
227
531
  log('add plugin', { id });
228
- const plugin = yield* this._pluginLoader(id);
229
- this._addPlugin(plugin);
230
- return yield* this.enable(plugin.meta.id);
532
+ const { plugin, dev = false } = yield* this._pluginLoader(id);
533
+ const pluginId = plugin.meta.id;
534
+ const existing = this._getPlugin(pluginId);
535
+
536
+ if (dev && existing && existing !== plugin) {
537
+ // Shadow path: a plugin with this id is already registered (a builtin,
538
+ // a registry install, or a previous dev load). Disable it, stash it,
539
+ // and swap the dev plugin into the same id slot. The dialog will call
540
+ // `enable(pluginId)` next, which activates the dev plugin's modules.
541
+ // `_markDev` is a no-op when the id is already tracked, so a dev-plugin
542
+ // reload (after editing source) keeps the *original* shadow target —
543
+ // removal restores the real underlying plugin, not an intermediate build.
544
+ const wasEnabled = this._get(this._enabledAtom).includes(pluginId);
545
+ if (wasEnabled) {
546
+ yield* this.disable(pluginId);
547
+ }
548
+ this._markDev(pluginId, { plugin: existing, wasEnabled });
549
+ this._update(this._pluginsAtom, (plugins) => plugins.map((p) => (p.meta.id === pluginId ? plugin : p)));
550
+ } else {
551
+ this._addPlugin(plugin);
552
+ if (dev) {
553
+ this._markDev(pluginId);
554
+ }
555
+ }
556
+
557
+ return plugin;
231
558
  });
232
559
  }
233
560
 
234
561
  /**
235
562
  * Enables a plugin.
236
563
  * @param id The id of the plugin.
564
+ * @param opts See {@link PluginManager.enable}.
237
565
  */
238
- enable(id: string): Effect.Effect<boolean, Error> {
566
+ enable(id: string, opts?: { resolveDependencies?: boolean }): Effect.Effect<boolean, Error> {
567
+ const resolveDependencies = opts?.resolveDependencies !== false;
239
568
  return Effect.gen(this, function* () {
240
- log('enable plugin', { id });
241
- const plugin = this._getPlugin(id);
242
- if (!plugin) {
569
+ log('enable plugin', { id, resolveDependencies });
570
+
571
+ if (!resolveDependencies) {
572
+ return yield* this._enableOne(id);
573
+ }
574
+
575
+ // If the root id is unknown to both the registered set and the catalog,
576
+ // fall back to the silent `_enableOne` path (which returns `false`).
577
+ // This preserves the prior contract for persisted `enabled` entries
578
+ // whose plugins are no longer bundled, instead of recording a confusing
579
+ // "missing self-dependency" failure.
580
+ if (!this._getPlugin(id) && !this._getCatalogEntry(id)) {
581
+ return yield* this._enableOne(id);
582
+ }
583
+
584
+ // Compute the transitive closure across registered plugins and catalog
585
+ // entries. Missing or cyclic entries are recorded as failures and the
586
+ // target plugin is left disabled.
587
+ const walk = this._computeDependencyClosure(id);
588
+ if (walk.cycle) {
589
+ this._recordFailure(
590
+ id,
591
+ 'load',
592
+ new Plugin.PluginDependencyError({ context: { id, reason: 'cycle', path: walk.cycle } }),
593
+ );
594
+ return false;
595
+ }
596
+ if (walk.missing.length > 0) {
597
+ this._recordFailure(
598
+ id,
599
+ 'load',
600
+ new Plugin.PluginDependencyError({ context: { id, reason: 'missing', missing: walk.missing } }),
601
+ );
602
+ return false;
603
+ }
604
+
605
+ // Install any catalog-only entries before enabling them. `add` may also
606
+ // discover further declared deps once the plugin's real meta is loaded;
607
+ // we re-walk after each install to absorb those.
608
+ let queue = walk.toInstall.slice();
609
+ const installed = new Set<string>();
610
+ while (queue.length > 0) {
611
+ const next = queue.shift()!;
612
+ if (installed.has(next) || this._getPlugin(next)) {
613
+ continue;
614
+ }
615
+ const installResult = yield* this.add(next).pipe(Effect.either);
616
+ if (installResult._tag === 'Left') {
617
+ this._recordFailure(
618
+ id,
619
+ 'load',
620
+ new Plugin.PluginDependencyError({
621
+ context: { id, reason: 'install-failed', dependency: next },
622
+ cause: installResult.left,
623
+ }),
624
+ );
625
+ return false;
626
+ }
627
+ installed.add(next);
628
+ const rewalk = this._computeDependencyClosure(id);
629
+ if (rewalk.cycle) {
630
+ this._recordFailure(
631
+ id,
632
+ 'load',
633
+ new Plugin.PluginDependencyError({ context: { id, reason: 'cycle', path: rewalk.cycle } }),
634
+ );
635
+ return false;
636
+ }
637
+ if (rewalk.missing.length > 0) {
638
+ this._recordFailure(
639
+ id,
640
+ 'load',
641
+ new Plugin.PluginDependencyError({ context: { id, reason: 'missing', missing: rewalk.missing } }),
642
+ );
643
+ return false;
644
+ }
645
+ queue = rewalk.toInstall.filter((depId) => !installed.has(depId));
646
+ }
647
+
648
+ // Enable in dependency-first order. `_enableOne` is idempotent on the
649
+ // enabled atom so previously-enabled deps short-circuit.
650
+ const order = this._computeDependencyClosure(id).order;
651
+ let succeeded = false;
652
+ for (const depId of order) {
653
+ const ok = yield* this._enableOne(depId);
654
+ if (depId === id) {
655
+ succeeded = ok;
656
+ }
657
+ }
658
+ return succeeded;
659
+ });
660
+ }
661
+
662
+ /**
663
+ * Enables a single plugin without consulting its declared dependencies.
664
+ * Used by {@link enable} as the leaf step after closure resolution, and
665
+ * directly when callers pass `{ resolveDependencies: false }`.
666
+ *
667
+ * The underlying operations (`_addModule`, `_setPendingResetByModule`,
668
+ * `activate`) are all idempotent, so this method is safe to call multiple
669
+ * times for the same id. The constructor's bootstrap path relies on this:
670
+ * the persisted `enabled` ids are written into `_enabledAtom` up front, so
671
+ * the very first `enable(id)` for those plugins sees `alreadyEnabled`-style
672
+ * state but still needs to perform the module registration and activation.
673
+ */
674
+ private _enableOne(id: string): Effect.Effect<boolean, Error> {
675
+ return Effect.gen(this, function* () {
676
+ const stub = this._getPlugin(id);
677
+ if (!stub) {
243
678
  return false;
244
679
  }
245
680
 
681
+ // Clear any prior failure record so a retry starts from a clean slate.
682
+ // The failure stays on the atom only if this attempt also fails.
683
+ this.clearFailure(id);
684
+
685
+ const plugin = yield* this._resolveLazyPlugin(stub);
686
+
246
687
  this._update(this._enabledAtom, (enabled) => (enabled.includes(id) ? enabled : [...enabled, id]));
247
688
 
248
689
  plugin.modules.forEach((module) => {
@@ -260,28 +701,119 @@ class ManagerImpl implements PluginManager {
260
701
  });
261
702
  }
262
703
 
704
+ /**
705
+ * Resolves a lazy plugin stub (returned by {@link Plugin.lazy}) to its
706
+ * loaded form and swaps it into `_pluginsAtom`. Returns the input unchanged
707
+ * when the plugin is already resolved, so callers can `yield*` this
708
+ * unconditionally. The lazy stub carries `meta` synchronously but its
709
+ * `modules` list is empty until the loader resolves; the swap ensures
710
+ * subsequent enable/disable operations see the resolved plugin.
711
+ *
712
+ * Concurrent calls for the same id are coalesced via `_resolvingPlugins`:
713
+ * the first caller starts the resolution, every subsequent caller awaits
714
+ * the same `Deferred`. On failure we publish a `lazy:<id>` error message
715
+ * and skip the atom swap so the failure is observable to the activation
716
+ * subscriber and a retry can be attempted.
717
+ */
718
+ private _resolveLazyPlugin(plugin: Plugin.Plugin): Effect.Effect<Plugin.Plugin, Plugin.LazyPluginError> {
719
+ return Effect.gen(this, function* () {
720
+ if (!Plugin.isLazy(plugin)) {
721
+ return plugin;
722
+ }
723
+ const id = plugin.meta.id;
724
+
725
+ const existing = this._resolvingPlugins.get(id);
726
+ if (existing) {
727
+ return yield* Deferred.await(existing);
728
+ }
729
+ const deferred = yield* Deferred.make<Plugin.Plugin, Plugin.LazyPluginError>();
730
+ this._resolvingPlugins.set(id, deferred);
731
+
732
+ return yield* Effect.gen(this, function* () {
733
+ log('resolving lazy plugin', { id });
734
+ yield* PubSub.publish(this.activation, { event: '', state: 'activating', module: `lazy:${id}` });
735
+ const resolvedPlugin = yield* Plugin.resolveLazy(plugin).pipe(
736
+ // Cap how long a remote import can hang. Without this the host can
737
+ // sit on a pending dynamic `import()` indefinitely if the plugin's
738
+ // server is unreachable, which stalls every caller awaiting
739
+ // `enable(id)` and (transitively) the manager's initialization.
740
+ Effect.timeoutFail({
741
+ duration: this._loadTimeout,
742
+ onTimeout: () =>
743
+ new Plugin.LazyPluginError({
744
+ context: { id, reason: 'load-failed' },
745
+ cause: new PluginTimeoutError({ context: { id, phase: 'load' as PluginFailurePhase } }),
746
+ }),
747
+ }),
748
+ );
749
+ this._update(this._pluginsAtom, (plugins) => plugins.map((p) => (p.meta.id === id ? resolvedPlugin : p)));
750
+ yield* PubSub.publish(this.activation, { event: '', state: 'activated', module: `lazy:${id}` });
751
+ return resolvedPlugin;
752
+ }).pipe(
753
+ Effect.tapError((error) =>
754
+ Effect.gen(this, function* () {
755
+ yield* PubSub.publish(this.activation, { event: '', state: 'error', module: `lazy:${id}`, error });
756
+ this._recordFailure(id, 'load', error);
757
+ this._scheduleAutoDisable(id);
758
+ }),
759
+ ),
760
+ Effect.tap((value) => Deferred.succeed(deferred, value)),
761
+ Effect.tapErrorCause((cause) => Deferred.failCause(deferred, cause)),
762
+ Effect.ensuring(Effect.sync(() => this._resolvingPlugins.delete(id))),
763
+ );
764
+ });
765
+ }
766
+
263
767
  /**
264
768
  * Removes a plugin from the manager.
265
769
  * @param id The id of the plugin.
770
+ * @param opts See {@link PluginManager.remove}.
266
771
  */
267
- remove(id: string): boolean {
268
- log('remove plugin', { id });
269
- const result = this.disable(id);
270
- if (!result) {
271
- return false;
272
- }
772
+ remove(id: string, opts?: { cascade?: boolean }): Effect.Effect<boolean, Error> {
773
+ return Effect.gen(this, function* () {
774
+ log('remove plugin', { id });
775
+ const wasDev = this._devPlugins.has(id);
776
+ const disabled = yield* this.disable(id, opts);
777
+ if (!disabled) {
778
+ return false;
779
+ }
273
780
 
274
- this._removePlugin(id);
275
- return true;
781
+ this._removePlugin(id);
782
+ if (this._onRemove) {
783
+ this._runForkedFiber(
784
+ this._onRemove(id).pipe(
785
+ Effect.tapError((error) => Effect.sync(() => log.warn('plugin remove hook failed', { id, error }))),
786
+ Effect.ignore,
787
+ ),
788
+ );
789
+ }
790
+
791
+ // If a dev plugin was shadowing an existing plugin, reinstate the
792
+ // original now that the dev plugin is gone. Re-enable only if the
793
+ // original was enabled at shadow time — preserving the user's intent
794
+ // for plugins they had explicitly disabled before iterating on a dev
795
+ // build.
796
+ if (wasDev) {
797
+ const shadow = this._unmarkDev(id);
798
+ if (shadow) {
799
+ this._addPlugin(shadow.plugin);
800
+ if (shadow.wasEnabled) {
801
+ yield* this.enable(id);
802
+ }
803
+ }
804
+ }
805
+ return true;
806
+ });
276
807
  }
277
808
 
278
809
  /**
279
810
  * Disables a plugin.
280
811
  * @param id The id of the plugin.
812
+ * @param opts See {@link PluginManager.disable}.
281
813
  */
282
- disable(id: string): Effect.Effect<boolean, Error> {
814
+ disable(id: string, { cascade = true }: { cascade?: boolean } = {}): Effect.Effect<boolean, Error> {
283
815
  return Effect.gen(this, function* () {
284
- log('disable plugin', { id });
816
+ log('disable plugin', { id, cascade });
285
817
  if (this._get(this._coreAtom).includes(id)) {
286
818
  return false;
287
819
  }
@@ -291,16 +823,55 @@ class ManagerImpl implements PluginManager {
291
823
  return false;
292
824
  }
293
825
 
826
+ if (cascade) {
827
+ const enabledDependents = this._collectDependents(id, { transitive: true, enabledOnly: true });
828
+ if (enabledDependents.length > 0) {
829
+ const coreDependent = enabledDependents.find((dependentId) =>
830
+ this._get(this._coreAtom).includes(dependentId),
831
+ );
832
+ if (coreDependent) {
833
+ return yield* Effect.fail(
834
+ new Plugin.PluginDependencyError({
835
+ context: { id, reason: 'core-dependent', coreDependent },
836
+ }),
837
+ );
838
+ }
839
+ // Disable transitive dependents first (leaves before root). The
840
+ // walk returns them in dependents-before-deps order — exactly what
841
+ // we want for teardown.
842
+ for (const dependentId of enabledDependents) {
843
+ yield* this._disableOne(dependentId);
844
+ }
845
+ }
846
+ }
847
+
848
+ yield* this._disableOne(id);
849
+ return true;
850
+ });
851
+ }
852
+
853
+ /**
854
+ * Disables a single plugin without consulting its dependents. Used by
855
+ * {@link disable} after the dependents pass has run (or been skipped via
856
+ * `cascade: false`).
857
+ */
858
+ private _disableOne(id: string): Effect.Effect<boolean, Error> {
859
+ return Effect.gen(this, function* () {
860
+ if (this._get(this._coreAtom).includes(id)) {
861
+ return false;
862
+ }
863
+ const plugin = this._getPlugin(id);
864
+ if (!plugin) {
865
+ return false;
866
+ }
294
867
  const enabledIndex = this._get(this._enabledAtom).findIndex((enabled) => enabled === id);
295
868
  if (enabledIndex !== -1) {
296
869
  this._update(this._enabledAtom, (enabled) => enabled.filter((item) => item !== id));
297
870
  yield* this.deactivate(id);
298
-
299
871
  plugin.modules.forEach((module) => {
300
872
  this._removeModule(module.id);
301
873
  });
302
874
  }
303
-
304
875
  return true;
305
876
  });
306
877
  }
@@ -321,6 +892,12 @@ class ManagerImpl implements PluginManager {
321
892
  return false;
322
893
  }
323
894
 
895
+ // Wait for the constructor's core/enabled `enable()` chain — including
896
+ // any async dynamic imports for lazy plugins — to finish registering
897
+ // modules. Without this, dispatching to an empty module set is the
898
+ // observable symptom of the race.
899
+ yield* Deferred.await(this._initialization);
900
+
324
901
  return yield* Effect.withFiberRuntime<boolean, Error>((fiber) =>
325
902
  this._activateEvent(key, params, fiber).pipe(
326
903
  together(
@@ -440,6 +1017,176 @@ class ManagerImpl implements PluginManager {
440
1017
  return this._get(this._pluginsAtom).find((plugin) => plugin.meta.id === id);
441
1018
  }
442
1019
 
1020
+ private _getPluginIdForModule(moduleId: string): string | undefined {
1021
+ return this._get(this._pluginsAtom).find((plugin) => plugin.modules.some((module) => module.id === moduleId))?.meta
1022
+ .id;
1023
+ }
1024
+
1025
+ /** Looks up an id in the cached registry catalog, returning the entry or `undefined`. */
1026
+ private _getCatalogEntry(id: string): PluginRegistry.Plugin | undefined {
1027
+ return this._get(this.pluginRegistry.plugins).entries.find((entry) => entry.id === id);
1028
+ }
1029
+
1030
+ /**
1031
+ * Returns the direct `dependsOn` declarations for an id, drawing from the
1032
+ * registered plugin's meta when available and falling back to the registry
1033
+ * catalog entry. Unknown ids return an empty list (callers detect "missing"
1034
+ * separately).
1035
+ */
1036
+ private _directDependencies(id: string): string[] {
1037
+ const plugin = this._getPlugin(id);
1038
+ if (plugin) {
1039
+ return [...(plugin.meta.dependsOn ?? [])];
1040
+ }
1041
+ const catalog = this._getCatalogEntry(id);
1042
+ return catalog?.dependsOn ? [...catalog.dependsOn] : [];
1043
+ }
1044
+
1045
+ /**
1046
+ * Computes the transitive dependency closure for an id.
1047
+ *
1048
+ * Walks {@link _directDependencies} (registered plugins ∪ catalog entries).
1049
+ * Returns:
1050
+ * - `order`: closure including the root in dependency-first topological order.
1051
+ * - `missing`: ids in the closure that are neither registered nor in the catalog.
1052
+ * - `toInstall`: ids in the closure that are in the catalog but not yet registered.
1053
+ * - `cycle`: when a cycle is detected, the cycle path; otherwise `undefined`.
1054
+ */
1055
+ private _computeDependencyClosure(id: string): {
1056
+ order: string[];
1057
+ missing: string[];
1058
+ toInstall: string[];
1059
+ cycle?: string[];
1060
+ } {
1061
+ const order: string[] = [];
1062
+ const visited = new Set<string>();
1063
+ const onStack = new Set<string>();
1064
+ const stackPath: string[] = [];
1065
+ const missing: string[] = [];
1066
+ const toInstall: string[] = [];
1067
+ let cycle: string[] | undefined;
1068
+
1069
+ const knownIds = new Set<string>([
1070
+ ...this._get(this._pluginsAtom).map((plugin) => plugin.meta.id),
1071
+ ...this._get(this.pluginRegistry.plugins).entries.map((entry) => entry.id),
1072
+ ]);
1073
+
1074
+ const visit = (currentId: string): void => {
1075
+ if (cycle) {
1076
+ return;
1077
+ }
1078
+ if (visited.has(currentId)) {
1079
+ return;
1080
+ }
1081
+ if (onStack.has(currentId)) {
1082
+ const cycleStart = stackPath.indexOf(currentId);
1083
+ cycle = [...stackPath.slice(cycleStart), currentId];
1084
+ return;
1085
+ }
1086
+ onStack.add(currentId);
1087
+ stackPath.push(currentId);
1088
+
1089
+ if (!knownIds.has(currentId)) {
1090
+ missing.push(currentId);
1091
+ } else if (!this._getPlugin(currentId)) {
1092
+ toInstall.push(currentId);
1093
+ }
1094
+
1095
+ for (const depId of this._directDependencies(currentId)) {
1096
+ visit(depId);
1097
+ if (cycle) {
1098
+ break;
1099
+ }
1100
+ }
1101
+
1102
+ onStack.delete(currentId);
1103
+ stackPath.pop();
1104
+ if (!cycle) {
1105
+ visited.add(currentId);
1106
+ order.push(currentId);
1107
+ }
1108
+ };
1109
+
1110
+ visit(id);
1111
+ return { order, missing, toInstall, cycle };
1112
+ }
1113
+
1114
+ /**
1115
+ * Walks the reverse `dependsOn` edges across registered plugins. With
1116
+ * `enabledOnly`, filters the result to currently-enabled ids. Returns
1117
+ * dependents in dependents-before-deps order so callers (cascade-disable)
1118
+ * can iterate and tear down leaves first.
1119
+ */
1120
+ private _collectDependents(id: string, opts: { transitive: boolean; enabledOnly: boolean }): string[] {
1121
+ const direct = this._get(this._pluginsAtom)
1122
+ .filter((plugin) => plugin.meta.dependsOn?.includes(id))
1123
+ .map((plugin) => plugin.meta.id);
1124
+
1125
+ if (!opts.transitive) {
1126
+ return opts.enabledOnly
1127
+ ? direct.filter((dependentId) => this._get(this._enabledAtom).includes(dependentId))
1128
+ : direct;
1129
+ }
1130
+
1131
+ const result: string[] = [];
1132
+ const visited = new Set<string>();
1133
+ const visit = (currentId: string): void => {
1134
+ if (visited.has(currentId)) {
1135
+ return;
1136
+ }
1137
+ visited.add(currentId);
1138
+ const parents = this._get(this._pluginsAtom)
1139
+ .filter((plugin) => plugin.meta.dependsOn?.includes(currentId))
1140
+ .map((plugin) => plugin.meta.id);
1141
+ for (const parentId of parents) {
1142
+ visit(parentId);
1143
+ if (parentId !== id && !result.includes(parentId)) {
1144
+ result.push(parentId);
1145
+ }
1146
+ }
1147
+ };
1148
+ visit(id);
1149
+
1150
+ return opts.enabledOnly
1151
+ ? result.filter((dependentId) => this._get(this._enabledAtom).includes(dependentId))
1152
+ : result;
1153
+ }
1154
+
1155
+ /**
1156
+ * Records a failure for a plugin. Latest failure wins so the registry UI
1157
+ * always sees the most recent reason. Walks the `cause` chain when checking
1158
+ * for timeouts: lazy-load timeouts arrive wrapped in `LazyPluginError` (the
1159
+ * timeout is the cause), but the operator-visible reason should still be
1160
+ * `'timeout'`.
1161
+ */
1162
+ private _recordFailure(id: string, phase: PluginFailurePhase, error: Error): void {
1163
+ const reason: PluginFailureReason = isTimeoutCause(error) ? 'timeout' : 'error';
1164
+ const failure: PluginFailure = { id, phase, reason, error, timestamp: Date.now() };
1165
+ log.warn('plugin failed', { id, phase, reason, error: error.message });
1166
+ this._update(this._failedAtom, (current) => [...current.filter((entry) => entry.id !== id), failure]);
1167
+ }
1168
+
1169
+ /**
1170
+ * Fire-and-forget disable of a failed plugin. Forked because a failure can
1171
+ * happen mid-activation chain — yielding a `disable` inline would deadlock
1172
+ * on the shared semaphores. Core plugins are skipped (the host opted into
1173
+ * them being non-removable; the failure record is enough signal).
1174
+ */
1175
+ private _scheduleAutoDisable(id: string): void {
1176
+ if (this._get(this._coreAtom).includes(id)) {
1177
+ return;
1178
+ }
1179
+ if (!this._get(this._enabledAtom).includes(id)) {
1180
+ return;
1181
+ }
1182
+ this._runForkedFiber(
1183
+ this.disable(id).pipe(
1184
+ Effect.tapError((error) => Effect.sync(() => log.warn('auto-disable failed', { id, error }))),
1185
+ Effect.ignore,
1186
+ ),
1187
+ );
1188
+ }
1189
+
443
1190
  private _getActiveModules(): Plugin.PluginModule[] {
444
1191
  const active = this._get(this._activeAtom);
445
1192
  return this._get(this._modulesAtom).filter((module) => active.includes(module.id));
@@ -511,6 +1258,18 @@ class ManagerImpl implements PluginManager {
511
1258
  return Ref.update(ref, (fibers) => fibers.filter((trackedFiber) => trackedFiber !== fiber));
512
1259
  }
513
1260
 
1261
+ /**
1262
+ * Spawns an effect on the default runtime and registers the resulting fiber in
1263
+ * `_inFlightFibers` so {@link shutdown} can interrupt it. Used from sync entry
1264
+ * points like {@link remove} where there is no enclosing Effect to fork from;
1265
+ * inside an Effect chain prefer the existing track/await/untrack pattern.
1266
+ */
1267
+ private _runForkedFiber<E>(effect: Effect.Effect<void, E>): void {
1268
+ const fiber = Effect.runFork(effect);
1269
+ Effect.runSync(this._trackFiber(this._inFlightFibers, fiber));
1270
+ Effect.runFork(Fiber.await(fiber).pipe(Effect.andThen(() => this._untrackFiber(this._inFlightFibers, fiber))));
1271
+ }
1272
+
514
1273
  //
515
1274
  // Registration helpers
516
1275
  //
@@ -645,7 +1404,7 @@ class ManagerImpl implements PluginManager {
645
1404
  ): ActivationEvent.ActivationEvent[] {
646
1405
  return Function.pipe(
647
1406
  modules,
648
- Array.flatMap((module) => module.activatesBefore ?? []),
1407
+ Array.flatMap((module) => module.firesBeforeActivation ?? []),
649
1408
  HashSet.fromIterable,
650
1409
  HashSet.toValues,
651
1410
  Array.filter((event) => !activatingEvents.includes(ActivationEvent.eventKey(event))),
@@ -658,7 +1417,7 @@ class ManagerImpl implements PluginManager {
658
1417
  ): ActivationEvent.ActivationEvent[] {
659
1418
  return Function.pipe(
660
1419
  modules,
661
- Array.flatMap((module) => module.activatesAfter ?? []),
1420
+ Array.flatMap((module) => module.firesAfterActivation ?? []),
662
1421
  HashSet.fromIterable,
663
1422
  HashSet.toValues,
664
1423
  Array.filter((event) => !activatingEvents.includes(ActivationEvent.eventKey(event))),
@@ -670,7 +1429,7 @@ class ManagerImpl implements PluginManager {
670
1429
  events: ActivationEvent.ActivationEvent[],
671
1430
  phase: 'before' | 'after',
672
1431
  ): Effect.Effect<void, Error> {
673
- const logLabel = phase === 'before' ? 'activatesBefore' : 'activatesAfter';
1432
+ const logLabel = phase === 'before' ? 'firesBeforeActivation' : 'firesAfterActivation';
674
1433
  const eventKey = phase === 'before' ? 'beforeEvents' : 'afterEvents';
675
1434
  return Function.pipe(
676
1435
  events,
@@ -702,7 +1461,7 @@ class ManagerImpl implements PluginManager {
702
1461
  ): Effect.Effect<Capability.Any[][], Error> {
703
1462
  return Function.pipe(
704
1463
  modules,
705
- Array.map((mod) => this._loadModule(mod)),
1464
+ Array.map((mod) => this._loadModule(mod, key)),
706
1465
  Effect.allWith({ concurrency: 'unbounded' }),
707
1466
  Effect.catchAll((error) => {
708
1467
  return Effect.gen(this, function* () {
@@ -721,8 +1480,12 @@ class ManagerImpl implements PluginManager {
721
1480
  modules,
722
1481
  Array.zip(capabilities),
723
1482
  Array.map(([module, capabilitySet]) => this._contributeCapabilities(module, capabilitySet)),
724
- // TODO(wittjosiah): This currently can't be run in parallel.
725
- // Running this with concurrency causes races with `allOf` activation events.
1483
+ // TODO(wittjosiah): This currently can't be run in parallel, and inserting
1484
+ // any yield between contributions (`Effect.yieldNow()`, `Effect.sleep(0)`)
1485
+ // races the `allOf` activation-event resolver — observed as a System
1486
+ // Error dialog on warm reloads. Contributions must stay strictly
1487
+ // synchronous within an event; React paint slots have to be found at
1488
+ // event boundaries higher up the call chain.
726
1489
  Effect.all,
727
1490
  Effect.asVoid,
728
1491
  );
@@ -737,7 +1500,14 @@ class ManagerImpl implements PluginManager {
737
1500
  return semaphore;
738
1501
  }
739
1502
 
740
- private _loadModule = (module: Plugin.PluginModule): Effect.Effect<Capability.Any[], Error> =>
1503
+ // `parentEvent` is the activation event that first triggered this module
1504
+ // load — included in `activating`/`activated` PubSub messages so subscribers
1505
+ // (e.g. the boot loader's status listener) can associate a module with its
1506
+ // triggering event in the trace. The same module may be referenced by
1507
+ // multiple events, but module loads are memoized via `_moduleMemoMap`, so
1508
+ // only the first event to need it will appear here; later events await the
1509
+ // cached deferred without re-publishing.
1510
+ private _loadModule = (module: Plugin.PluginModule, parentEvent: string): Effect.Effect<Capability.Any[], Error> =>
741
1511
  Effect.gen(this, function* () {
742
1512
  const semaphore = this._getModuleSemaphore(module.id);
743
1513
 
@@ -753,23 +1523,33 @@ class ManagerImpl implements PluginManager {
753
1523
  this._moduleMemoMap.set(module.id, deferred);
754
1524
 
755
1525
  const loadEffect = Effect.gen(this, function* () {
756
- log('loading module', { module: module.id });
1526
+ log('loading module', { module: module.id, parentEvent });
757
1527
  performance.mark(`module:${module.id}:start`);
758
- yield* PubSub.publish(this.activation, { event: '', state: 'activating', module: module.id });
759
- const [duration, capabilities] = yield* module
760
- .activate()
761
- .pipe(
762
- Effect.provideService(Capability.Service, this.capabilities),
763
- Effect.provideService(Plugin.Service, this),
764
- Effect.timed,
765
- );
1528
+ yield* PubSub.publish(this.activation, { event: parentEvent, state: 'activating', module: module.id });
1529
+ const pluginId = this._getPluginIdForModule(module.id);
1530
+ const [duration, capabilities] = yield* module.activate().pipe(
1531
+ Effect.provideService(Capability.Service, this.capabilities),
1532
+ Effect.provideService(Plugin.Service, this),
1533
+ // Cap activation so a single misbehaving module can't hold the
1534
+ // event chain open. On timeout the failure is recorded against
1535
+ // the plugin and surfaced as `PluginTimeoutError`.
1536
+ Effect.timeoutFail({
1537
+ duration: this._activationTimeout,
1538
+ onTimeout: () =>
1539
+ new PluginTimeoutError({
1540
+ context: { id: pluginId ?? module.id, module: module.id, phase: 'activation' as PluginFailurePhase },
1541
+ }),
1542
+ }),
1543
+ Effect.timed,
1544
+ );
766
1545
  const normalized = capabilities == null ? [] : Array.isArray(capabilities) ? capabilities : [capabilities];
767
1546
  const elapsed = Duration.toMillis(duration);
768
1547
  performance.mark(`module:${module.id}:end`);
769
1548
  performance.measure(`module:${module.id}`, `module:${module.id}:start`, `module:${module.id}:end`);
770
- yield* PubSub.publish(this.activation, { event: '', state: 'activated', module: module.id });
1549
+ yield* PubSub.publish(this.activation, { event: parentEvent, state: 'activated', module: module.id });
771
1550
  log('loaded module', {
772
1551
  module: module.id,
1552
+ parentEvent,
773
1553
  elapsed,
774
1554
  failed: false,
775
1555
  });
@@ -806,7 +1586,13 @@ class ManagerImpl implements PluginManager {
806
1586
  stack: error instanceof Error ? error.stack : undefined,
807
1587
  isDefect: !Cause.isFailure(cause),
808
1588
  });
809
- return Deferred.fail(deferred, error instanceof Error ? error : new Error(String(error)));
1589
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
1590
+ const pluginId = this._getPluginIdForModule(module.id);
1591
+ if (pluginId !== undefined) {
1592
+ this._recordFailure(pluginId, 'activation', normalizedError);
1593
+ this._scheduleAutoDisable(pluginId);
1594
+ }
1595
+ return Deferred.fail(deferred, normalizedError);
810
1596
  }),
811
1597
  ),
812
1598
  );
@@ -867,6 +1653,22 @@ class ManagerImpl implements PluginManager {
867
1653
  */
868
1654
  export const make = (options: ManagerOptions): PluginManager => new ManagerImpl(options);
869
1655
 
1656
+ /**
1657
+ * True when `error` (or anything along its `cause` chain) is a
1658
+ * {@link PluginTimeoutError}. Lazy-load timeouts wrap the timeout inside
1659
+ * `LazyPluginError`, so a shallow check on the outer error misses them.
1660
+ * Bounded depth so a circular chain can't loop forever.
1661
+ */
1662
+ const isTimeoutCause = (error: unknown, depth = 0): boolean => {
1663
+ if (depth > 5 || !(error instanceof Error)) {
1664
+ return false;
1665
+ }
1666
+ if (PluginTimeoutError.is(error)) {
1667
+ return true;
1668
+ }
1669
+ return isTimeoutCause((error as Error & { cause?: unknown }).cause, depth + 1);
1670
+ };
1671
+
870
1672
  /**
871
1673
  * Runs an effect concurrently with another effect.
872
1674
  * If the first effect completes, the second effect is interrupted.