@dxos/app-framework 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/.storybook/main.mts +1 -1
  2. package/.storybook/preview.mts +2 -2
  3. package/CHANGELOG.md +120 -0
  4. package/TASKS.md +11 -0
  5. package/dist/lib/chunk-SurfaceManager.mjs +960 -0
  6. package/dist/lib/chunk-SurfaceManager.mjs.map +1 -0
  7. package/dist/lib/chunk-capability.mjs +15 -0
  8. package/dist/lib/chunk-capability.mjs.map +1 -0
  9. package/dist/lib/{browser/chunk-FJ4765WW.mjs → chunk-context.mjs} +4 -5
  10. package/dist/lib/chunk-context.mjs.map +1 -0
  11. package/dist/lib/chunk-history-tracker.mjs +160 -0
  12. package/dist/lib/chunk-history-tracker.mjs.map +1 -0
  13. package/dist/lib/chunk-plugin-manager.mjs +1390 -0
  14. package/dist/lib/chunk-plugin-manager.mjs.map +1 -0
  15. package/dist/lib/chunk-plugin-manifest.mjs +125 -0
  16. package/dist/lib/chunk-plugin-manifest.mjs.map +1 -0
  17. package/dist/lib/chunk-process-manager-capability.mjs +126 -0
  18. package/dist/lib/chunk-process-manager-capability.mjs.map +1 -0
  19. package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
  20. package/dist/lib/chunk-url-loader.mjs +347 -0
  21. package/dist/lib/chunk-url-loader.mjs.map +1 -0
  22. package/dist/lib/cli.mjs +78 -0
  23. package/dist/lib/cli.mjs.map +1 -0
  24. package/dist/lib/common/activation-events.mjs +35 -0
  25. package/dist/lib/common/activation-events.mjs.map +1 -0
  26. package/dist/lib/common/capabilities.mjs +184 -0
  27. package/dist/lib/common/capabilities.mjs.map +1 -0
  28. package/dist/lib/config.mjs +2 -0
  29. package/dist/lib/core/activation-event.mjs +55 -0
  30. package/dist/lib/core/activation-event.mjs.map +1 -0
  31. package/dist/lib/core/capability.mjs +189 -0
  32. package/dist/lib/core/capability.mjs.map +1 -0
  33. package/dist/lib/core/plugin-manager.mjs +2 -0
  34. package/dist/lib/core/plugin.mjs +340 -0
  35. package/dist/lib/core/plugin.mjs.map +1 -0
  36. package/dist/lib/core/url-loader.mjs +2 -0
  37. package/dist/lib/index.mjs +134 -0
  38. package/dist/lib/index.mjs.map +1 -0
  39. package/dist/lib/testing/react.mjs +70 -0
  40. package/dist/lib/testing/react.mjs.map +1 -0
  41. package/dist/lib/testing.mjs +399 -0
  42. package/dist/lib/testing.mjs.map +1 -0
  43. package/dist/lib/ui.mjs +636 -0
  44. package/dist/lib/ui.mjs.map +1 -0
  45. package/dist/lib/vite-plugin.mjs +986 -0
  46. package/dist/lib/vite-plugin.mjs.map +1 -0
  47. package/dist/plugin/node-esm/index.mjs +4 -3
  48. package/dist/plugin/node-esm/index.mjs.map +3 -3
  49. package/dist/plugin/node-esm/meta.json +1 -1
  50. package/dist/types/src/common/Role.d.ts +31 -0
  51. package/dist/types/src/common/Role.d.ts.map +1 -0
  52. package/dist/types/src/common/Role.test.d.ts +2 -0
  53. package/dist/types/src/common/Role.test.d.ts.map +1 -0
  54. package/dist/types/src/common/capabilities.d.ts +11 -2
  55. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  56. package/dist/types/src/common/index.d.ts +1 -0
  57. package/dist/types/src/common/index.d.ts.map +1 -1
  58. package/dist/types/src/core/capability-manager.d.ts +1 -1
  59. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  60. package/dist/types/src/core/capability.d.ts +38 -5
  61. package/dist/types/src/core/capability.d.ts.map +1 -1
  62. package/dist/types/src/core/plugin-manager.d.ts +1 -1
  63. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  64. package/dist/types/src/core/plugin.d.ts +14 -1
  65. package/dist/types/src/core/plugin.d.ts.map +1 -1
  66. package/dist/types/src/core/registry.d.ts +1 -1
  67. package/dist/types/src/core/registry.d.ts.map +1 -1
  68. package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -1
  69. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -1
  70. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
  71. package/dist/types/src/testing/StorybookErrorFallback.d.ts +15 -0
  72. package/dist/types/src/testing/StorybookErrorFallback.d.ts.map +1 -0
  73. package/dist/types/src/testing/harness.d.ts +1 -1
  74. package/dist/types/src/testing/harness.d.ts.map +1 -1
  75. package/dist/types/src/testing/index.d.ts +2 -0
  76. package/dist/types/src/testing/index.d.ts.map +1 -1
  77. package/dist/types/src/testing/react.d.ts +4 -4
  78. package/dist/types/src/testing/react.d.ts.map +1 -1
  79. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  80. package/dist/types/src/testing/withPluginManager.stories.d.ts +6 -0
  81. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  82. package/dist/types/src/testing/withSurfaceDebug.d.ts +10 -0
  83. package/dist/types/src/testing/withSurfaceDebug.d.ts.map +1 -0
  84. package/dist/types/src/ui/components/App/App.d.ts +0 -32
  85. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  86. package/dist/types/src/ui/components/App/index.d.ts +1 -0
  87. package/dist/types/src/ui/components/App/index.d.ts.map +1 -1
  88. package/dist/types/src/ui/components/App/loader.d.ts +35 -0
  89. package/dist/types/src/ui/components/App/loader.d.ts.map +1 -0
  90. package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts +26 -0
  91. package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts.map +1 -0
  92. package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts +15 -0
  93. package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts.map +1 -0
  94. package/dist/types/src/ui/components/HomeSection/index.d.ts +2 -0
  95. package/dist/types/src/ui/components/HomeSection/index.d.ts.map +1 -0
  96. package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts +14 -0
  97. package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts.map +1 -0
  98. package/dist/types/src/ui/components/NamePopover/index.d.ts +2 -0
  99. package/dist/types/src/ui/components/NamePopover/index.d.ts.map +1 -0
  100. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +21 -9
  101. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  102. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts +4 -1
  103. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  104. package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts +2 -0
  105. package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts.map +1 -0
  106. package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts +40 -0
  107. package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts.map +1 -0
  108. package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts +23 -0
  109. package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts.map +1 -0
  110. package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts +11 -0
  111. package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts.map +1 -0
  112. package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts +72 -0
  113. package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts.map +1 -0
  114. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
  115. package/dist/types/src/ui/components/Surface/index.d.ts +20 -25
  116. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  117. package/dist/types/src/ui/components/Surface/types.d.ts +56 -52
  118. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  119. package/dist/types/src/ui/components/index.d.ts +2 -0
  120. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  121. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  122. package/dist/types/tsconfig.tsbuildinfo +1 -1
  123. package/moon.yml +25 -18
  124. package/package.json +58 -73
  125. package/src/common/Role.test.ts +41 -0
  126. package/src/common/Role.ts +54 -0
  127. package/src/common/capabilities.ts +17 -2
  128. package/src/common/index.ts +1 -0
  129. package/src/core/capability-manager.test.ts +1 -1
  130. package/src/core/capability-manager.ts +1 -1
  131. package/src/core/capability.ts +45 -6
  132. package/src/core/plugin-manager.test.ts +38 -3
  133. package/src/core/plugin-manager.ts +18 -1
  134. package/src/core/plugin.ts +16 -1
  135. package/src/core/registry.ts +1 -1
  136. package/src/plugin-process-manager/process-manager-capability.ts +25 -2
  137. package/src/testing/StorybookErrorFallback.tsx +40 -0
  138. package/src/testing/harness.ts +1 -1
  139. package/src/testing/index.ts +2 -0
  140. package/src/testing/react.test.tsx +2 -1
  141. package/src/testing/react.tsx +14 -8
  142. package/src/testing/withPluginManager.stories.tsx +16 -1
  143. package/src/testing/withPluginManager.tsx +13 -2
  144. package/src/testing/withSurfaceDebug.tsx +22 -0
  145. package/src/ui/components/App/App.tsx +7 -35
  146. package/src/ui/components/App/index.ts +1 -0
  147. package/src/ui/components/App/loader.ts +39 -0
  148. package/src/ui/components/HomeSection/HomeSection.stories.tsx +43 -0
  149. package/src/ui/components/HomeSection/HomeSection.tsx +69 -0
  150. package/src/ui/components/HomeSection/index.ts +5 -0
  151. package/src/ui/components/NamePopover/NamePopover.tsx +70 -0
  152. package/src/ui/components/NamePopover/index.ts +5 -0
  153. package/src/ui/components/Surface/DESIGN.md +136 -0
  154. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +70 -41
  155. package/src/ui/components/Surface/SurfaceComponent.test.tsx +403 -0
  156. package/src/ui/components/Surface/SurfaceComponent.tsx +209 -189
  157. package/src/ui/components/Surface/SurfaceDebug.tsx +435 -0
  158. package/src/ui/components/Surface/SurfaceManager.ts +95 -0
  159. package/src/ui/components/Surface/SurfaceManagerContext.ts +23 -0
  160. package/src/ui/components/Surface/SurfaceMetrics.ts +202 -0
  161. package/src/ui/components/Surface/SurfaceProfilerContext.tsx +12 -2
  162. package/src/ui/components/Surface/index.ts +24 -22
  163. package/src/ui/components/Surface/types.test.ts +47 -54
  164. package/src/ui/components/Surface/types.ts +66 -91
  165. package/src/ui/components/index.ts +2 -0
  166. package/src/ui/hooks/useApp.tsx +7 -3
  167. package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +1 -1
  168. package/tsconfig.json +3 -0
  169. package/vite.config.ts +26 -0
  170. package/dist/lib/browser/capability-S44TE2H7.mjs +0 -35
  171. package/dist/lib/browser/capability-S44TE2H7.mjs.map +0 -7
  172. package/dist/lib/browser/chunk-3NQLTQCP.mjs +0 -47
  173. package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +0 -7
  174. package/dist/lib/browser/chunk-53HJHMS5.mjs +0 -7
  175. package/dist/lib/browser/chunk-53HJHMS5.mjs.map +0 -7
  176. package/dist/lib/browser/chunk-FJ4765WW.mjs.map +0 -7
  177. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  178. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  179. package/dist/lib/browser/chunk-KRJ4KK2W.mjs +0 -505
  180. package/dist/lib/browser/chunk-KRJ4KK2W.mjs.map +0 -7
  181. package/dist/lib/browser/chunk-LUH4Y5F2.mjs +0 -430
  182. package/dist/lib/browser/chunk-LUH4Y5F2.mjs.map +0 -7
  183. package/dist/lib/browser/chunk-NKVRSMFN.mjs +0 -95
  184. package/dist/lib/browser/chunk-NKVRSMFN.mjs.map +0 -7
  185. package/dist/lib/browser/chunk-OO53M5UK.mjs +0 -260
  186. package/dist/lib/browser/chunk-OO53M5UK.mjs.map +0 -7
  187. package/dist/lib/browser/chunk-OPTS3ZSN.mjs +0 -83
  188. package/dist/lib/browser/chunk-OPTS3ZSN.mjs.map +0 -7
  189. package/dist/lib/browser/chunk-Q2GLJTVV.mjs +0 -12
  190. package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +0 -7
  191. package/dist/lib/browser/chunk-SYXIYT6T.mjs +0 -143
  192. package/dist/lib/browser/chunk-SYXIYT6T.mjs.map +0 -7
  193. package/dist/lib/browser/chunk-U5MESPM5.mjs +0 -603
  194. package/dist/lib/browser/chunk-U5MESPM5.mjs.map +0 -7
  195. package/dist/lib/browser/chunk-UGYCLOXE.mjs +0 -1471
  196. package/dist/lib/browser/chunk-UGYCLOXE.mjs.map +0 -7
  197. package/dist/lib/browser/chunk-VF37YTXJ.mjs +0 -28
  198. package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +0 -7
  199. package/dist/lib/browser/chunk-YUVQIOTB.mjs +0 -60
  200. package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +0 -7
  201. package/dist/lib/browser/cli/index.mjs +0 -74
  202. package/dist/lib/browser/cli/index.mjs.map +0 -7
  203. package/dist/lib/browser/common/activation-events.mjs +0 -20
  204. package/dist/lib/browser/common/activation-events.mjs.map +0 -7
  205. package/dist/lib/browser/common/capabilities.mjs +0 -56
  206. package/dist/lib/browser/common/capabilities.mjs.map +0 -7
  207. package/dist/lib/browser/config/index.mjs +0 -8
  208. package/dist/lib/browser/config/index.mjs.map +0 -7
  209. package/dist/lib/browser/core/activation-event.mjs +0 -20
  210. package/dist/lib/browser/core/activation-event.mjs.map +0 -7
  211. package/dist/lib/browser/core/capability.mjs +0 -32
  212. package/dist/lib/browser/core/capability.mjs.map +0 -7
  213. package/dist/lib/browser/core/plugin-manager.mjs +0 -19
  214. package/dist/lib/browser/core/plugin-manager.mjs.map +0 -7
  215. package/dist/lib/browser/core/plugin.mjs +0 -51
  216. package/dist/lib/browser/core/plugin.mjs.map +0 -7
  217. package/dist/lib/browser/core/url-loader.mjs +0 -24
  218. package/dist/lib/browser/core/url-loader.mjs.map +0 -7
  219. package/dist/lib/browser/index.mjs +0 -98
  220. package/dist/lib/browser/index.mjs.map +0 -7
  221. package/dist/lib/browser/meta.json +0 -1
  222. package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs +0 -132
  223. package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs.map +0 -7
  224. package/dist/lib/browser/testing/index.mjs +0 -359
  225. package/dist/lib/browser/testing/index.mjs.map +0 -7
  226. package/dist/lib/browser/testing/react.mjs +0 -78
  227. package/dist/lib/browser/testing/react.mjs.map +0 -7
  228. package/dist/lib/browser/ui/index.mjs +0 -60
  229. package/dist/lib/browser/ui/index.mjs.map +0 -7
  230. package/dist/lib/node-esm/capability-3YRF77LV.mjs +0 -36
  231. package/dist/lib/node-esm/capability-3YRF77LV.mjs.map +0 -7
  232. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +0 -9
  233. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +0 -7
  234. package/dist/lib/node-esm/chunk-37Z53PXZ.mjs +0 -10
  235. package/dist/lib/node-esm/chunk-37Z53PXZ.mjs.map +0 -7
  236. package/dist/lib/node-esm/chunk-4SE7SX7N.mjs +0 -261
  237. package/dist/lib/node-esm/chunk-4SE7SX7N.mjs.map +0 -7
  238. package/dist/lib/node-esm/chunk-BMW6P6QA.mjs +0 -48
  239. package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +0 -7
  240. package/dist/lib/node-esm/chunk-COHOWGGJ.mjs +0 -144
  241. package/dist/lib/node-esm/chunk-COHOWGGJ.mjs.map +0 -7
  242. package/dist/lib/node-esm/chunk-DRT75WGD.mjs +0 -604
  243. package/dist/lib/node-esm/chunk-DRT75WGD.mjs.map +0 -7
  244. package/dist/lib/node-esm/chunk-FKMVORZN.mjs +0 -431
  245. package/dist/lib/node-esm/chunk-FKMVORZN.mjs.map +0 -7
  246. package/dist/lib/node-esm/chunk-FPW45EZH.mjs +0 -14
  247. package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +0 -7
  248. package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs +0 -84
  249. package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs.map +0 -7
  250. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  251. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  252. package/dist/lib/node-esm/chunk-IL7O7IRX.mjs +0 -1472
  253. package/dist/lib/node-esm/chunk-IL7O7IRX.mjs.map +0 -7
  254. package/dist/lib/node-esm/chunk-ND2HETHD.mjs +0 -506
  255. package/dist/lib/node-esm/chunk-ND2HETHD.mjs.map +0 -7
  256. package/dist/lib/node-esm/chunk-TLKYZKPW.mjs +0 -29
  257. package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +0 -7
  258. package/dist/lib/node-esm/chunk-VCPGX7WN.mjs +0 -62
  259. package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +0 -7
  260. package/dist/lib/node-esm/chunk-XSZMX53L.mjs +0 -96
  261. package/dist/lib/node-esm/chunk-XSZMX53L.mjs.map +0 -7
  262. package/dist/lib/node-esm/cli/index.mjs +0 -75
  263. package/dist/lib/node-esm/cli/index.mjs.map +0 -7
  264. package/dist/lib/node-esm/common/activation-events.mjs +0 -21
  265. package/dist/lib/node-esm/common/activation-events.mjs.map +0 -7
  266. package/dist/lib/node-esm/common/capabilities.mjs +0 -57
  267. package/dist/lib/node-esm/common/capabilities.mjs.map +0 -7
  268. package/dist/lib/node-esm/config/index.mjs +0 -9
  269. package/dist/lib/node-esm/config/index.mjs.map +0 -7
  270. package/dist/lib/node-esm/core/activation-event.mjs +0 -21
  271. package/dist/lib/node-esm/core/activation-event.mjs.map +0 -7
  272. package/dist/lib/node-esm/core/capability.mjs +0 -33
  273. package/dist/lib/node-esm/core/capability.mjs.map +0 -7
  274. package/dist/lib/node-esm/core/plugin-manager.mjs +0 -20
  275. package/dist/lib/node-esm/core/plugin-manager.mjs.map +0 -7
  276. package/dist/lib/node-esm/core/plugin.mjs +0 -52
  277. package/dist/lib/node-esm/core/plugin.mjs.map +0 -7
  278. package/dist/lib/node-esm/core/url-loader.mjs +0 -25
  279. package/dist/lib/node-esm/core/url-loader.mjs.map +0 -7
  280. package/dist/lib/node-esm/index.mjs +0 -99
  281. package/dist/lib/node-esm/index.mjs.map +0 -7
  282. package/dist/lib/node-esm/meta.json +0 -1
  283. package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs +0 -133
  284. package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs.map +0 -7
  285. package/dist/lib/node-esm/testing/index.mjs +0 -360
  286. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  287. package/dist/lib/node-esm/testing/react.mjs +0 -79
  288. package/dist/lib/node-esm/testing/react.mjs.map +0 -7
  289. package/dist/lib/node-esm/ui/index.mjs +0 -61
  290. package/dist/lib/node-esm/ui/index.mjs.map +0 -7
  291. package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts +0 -11
  292. package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts.map +0 -1
  293. package/src/ui/components/Surface/SurfaceInfo.tsx +0 -106
  294. package/vitest.config.ts +0 -14
@@ -0,0 +1,403 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { act } from '@testing-library/react';
6
+ import * as Effect from 'effect/Effect';
7
+ import React, { Profiler, useState } from 'react';
8
+ import { describe, test } from 'vitest';
9
+
10
+ import { DXN } from '@dxos/keys';
11
+ import { Position } from '@dxos/util';
12
+
13
+ import { ActivationEvents, Capabilities } from '../../../common';
14
+ import * as Role from '../../../common/Role';
15
+ import { Capability, Plugin } from '../../../core';
16
+ import { createTestApp } from '../../../testing/harness';
17
+ import { render } from '../../../testing/react';
18
+ import { SurfaceComponent, useIsSurfaceAvailable, useSurfaces } from './SurfaceComponent';
19
+ import { setSurfaceDebug } from './SurfaceDebug';
20
+ import { surfaceMetrics } from './SurfaceMetrics';
21
+ import { type Definition, create, makeFilter } from './types';
22
+
23
+ // Flush the metrics store's rAF-batched notification (the actual signal it uses), not a fixed delay.
24
+ const flushMetrics = () =>
25
+ act(async () => {
26
+ await new Promise<void>((resolve) =>
27
+ typeof requestAnimationFrame === 'function' ? requestAnimationFrame(() => resolve()) : queueMicrotask(resolve),
28
+ );
29
+ });
30
+
31
+ const RoleA = Role.make<Record<string, unknown>>('org.dxos.test.role.alpha');
32
+ const RoleB = Role.make<Record<string, unknown>>('org.dxos.test.role.beta');
33
+
34
+ const testMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test.surfacePerf'), name: 'SurfacePerfTest' });
35
+
36
+ const TestPlugin = Plugin.define(testMeta).pipe(
37
+ Plugin.addModule({
38
+ id: 'surfaces',
39
+ activatesOn: ActivationEvents.SetupReactSurface,
40
+ activate: () =>
41
+ Effect.succeed(
42
+ Capability.contributes(Capabilities.ReactSurface, [
43
+ create({ id: 'alpha', filter: makeFilter(RoleA), component: () => <span data-testid='a' /> }),
44
+ create({ id: 'beta', filter: makeFilter(RoleB), component: () => <span data-testid='b' /> }),
45
+ ]),
46
+ ),
47
+ }),
48
+ Plugin.make,
49
+ );
50
+
51
+ // Exercises `props`: `Plain` takes its own props, so registering it needs no adapter component.
52
+ const RoleSubject = Role.make<{ subject: string }>('org.dxos.test.role.subject');
53
+ const RoleEnvelope = Role.make<{ subject: string }>('org.dxos.test.role.envelope');
54
+
55
+ const Plain = ({ label }: { label: string }) => <span data-testid='mapped'>{label}</span>;
56
+
57
+ const mappedPropsMeta = Plugin.makeMeta({
58
+ key: DXN.make('org.dxos.plugin.test.surfaceMappedProps'),
59
+ name: 'SurfaceMappedPropsTest',
60
+ });
61
+
62
+ const MappedPropsPlugin = Plugin.define(mappedPropsMeta).pipe(
63
+ Plugin.addModule({
64
+ id: 'surfaces',
65
+ activatesOn: ActivationEvents.SetupReactSurface,
66
+ activate: () =>
67
+ Effect.succeed(
68
+ Capability.contributes(Capabilities.ReactSurface, [
69
+ create({
70
+ id: 'mapped',
71
+ filter: makeFilter(RoleSubject),
72
+ component: Plain,
73
+ props: ({ data: { subject } }) => ({ label: subject }),
74
+ }),
75
+ create({
76
+ id: 'envelope',
77
+ filter: makeFilter(RoleEnvelope),
78
+ component: ({ data: { subject } }) => <span data-testid='envelope'>{subject}</span>,
79
+ }),
80
+ ]),
81
+ ),
82
+ }),
83
+ Plugin.make,
84
+ );
85
+
86
+ const invalidIdMeta = Plugin.makeMeta({
87
+ key: DXN.make('org.dxos.plugin.test.surfaceInvalidId'),
88
+ name: 'SurfaceInvalidIdTest',
89
+ });
90
+
91
+ // Contributes a single surface with a hyphenated (invalid) local id for role A.
92
+ const InvalidIdPlugin = Plugin.define(invalidIdMeta).pipe(
93
+ Plugin.addModule({
94
+ id: 'surfaces',
95
+ activatesOn: ActivationEvents.SetupReactSurface,
96
+ activate: () =>
97
+ Effect.succeed(
98
+ Capability.contributes(Capabilities.ReactSurface, [
99
+ create({ id: 'gallery-article', filter: makeFilter(RoleA), component: () => <span data-testid='invalid' /> }),
100
+ ]),
101
+ ),
102
+ }),
103
+ Plugin.make,
104
+ );
105
+
106
+ // A counter increments once per commit of the wrapped Surface subtree (mount or update).
107
+ const probe = (counts: { value: number }) => () => {
108
+ counts.value++;
109
+ };
110
+
111
+ describe('SurfaceComponent dispatch', () => {
112
+ test('limit={0} renders nothing', async ({ expect }) => {
113
+ await using harness = await createTestApp({ plugins: [TestPlugin()] });
114
+ // Dispatch is synchronous (render() flushes effects in act), so the surface either rendered or it didn't.
115
+ const view = render(harness, <SurfaceComponent type={RoleA} limit={0} />);
116
+ expect(view.queryByTestId('a')).toBeNull();
117
+ });
118
+
119
+ test('drops a surface with an invalid id rather than dispatching it', async ({ expect }) => {
120
+ await using harness = await createTestApp({ plugins: [InvalidIdPlugin()] });
121
+ const view = render(harness, <SurfaceComponent type={RoleA} />);
122
+ expect(view.queryByTestId('invalid')).toBeNull();
123
+ });
124
+
125
+ test('`props` maps the surface props onto a plain component', async ({ expect }) => {
126
+ await using harness = await createTestApp({ plugins: [MappedPropsPlugin()] });
127
+ const view = render(harness, <SurfaceComponent type={RoleSubject} data={{ subject: 'mapped' }} />);
128
+ // The component receives only `{ label }`, never the surface's `data` envelope.
129
+ expect((await view.findByTestId('mapped')).textContent).toBe('mapped');
130
+ });
131
+
132
+ test('a surface without `props` still receives the full surface props', async ({ expect }) => {
133
+ await using harness = await createTestApp({ plugins: [MappedPropsPlugin()] });
134
+ const view = render(harness, <SurfaceComponent type={RoleEnvelope} data={{ subject: 'envelope' }} />);
135
+ expect((await view.findByTestId('envelope')).textContent).toBe('envelope');
136
+ });
137
+ });
138
+
139
+ describe('useIsSurfaceAvailable', () => {
140
+ const IsAvailableProbe = ({ role, testId }: { role: Role.Role<any>; testId: string }) => {
141
+ const isSurfaceAvailable = useIsSurfaceAvailable();
142
+ return <span data-testid={testId}>{String(isSurfaceAvailable({ type: role }))}</span>;
143
+ };
144
+
145
+ test('reports false when no surface is contributed for the role', async ({ expect }) => {
146
+ await using harness = await createTestApp({ plugins: [] });
147
+ const view = render(harness, <IsAvailableProbe role={RoleA} testId='result-none' />);
148
+ expect((await view.findByTestId('result-none')).textContent).toBe('false');
149
+ });
150
+
151
+ test('reports true once a matching surface is contributed', async ({ expect }) => {
152
+ await using harness = await createTestApp({ plugins: [TestPlugin()] });
153
+ const view = render(harness, <IsAvailableProbe role={RoleA} testId='result-valid' />);
154
+ expect((await view.findByTestId('result-valid')).textContent).toBe('true');
155
+ });
156
+
157
+ test('reports false when the only contribution for the role has an invalid id', async ({ expect }) => {
158
+ await using harness = await createTestApp({ plugins: [InvalidIdPlugin()] });
159
+ const view = render(harness, <IsAvailableProbe role={RoleA} testId='result-invalid' />);
160
+ expect((await view.findByTestId('result-invalid')).textContent).toBe('false');
161
+ });
162
+ });
163
+
164
+ describe('SurfaceComponent per-role subscription', () => {
165
+ test('contributions to one role do not re-render surfaces of other roles', async ({ expect }) => {
166
+ await using harness = await createTestApp({ plugins: [TestPlugin()] });
167
+ const a = { value: 0 };
168
+ const b = { value: 0 };
169
+
170
+ const view = render(
171
+ harness,
172
+ <>
173
+ <Profiler id='a' onRender={probe(a)}>
174
+ <SurfaceComponent type={RoleA} />
175
+ </Profiler>
176
+ <Profiler id='b' onRender={probe(b)}>
177
+ <SurfaceComponent type={RoleB} />
178
+ </Profiler>
179
+ </>,
180
+ );
181
+ await view.findByTestId('a');
182
+ await view.findByTestId('b');
183
+
184
+ const aBaseline = a.value;
185
+ const bBaseline = b.value;
186
+
187
+ // Contribute a new surface for role B only.
188
+ act(() => {
189
+ harness.manager.capabilities.contribute({
190
+ module: 'late',
191
+ interface: Capabilities.ReactSurface,
192
+ implementation: create({ id: 'beta2', filter: makeFilter(RoleB, () => false), component: () => null }),
193
+ });
194
+ });
195
+
196
+ // Role A was untouched; role B's bucket changed and re-rendered.
197
+ expect(a.value).toBe(aBaseline);
198
+ expect(b.value).toBeGreaterThan(bBaseline);
199
+ });
200
+
201
+ test('cost of unrelated contributions is independent of other roles (scales flat)', async ({ expect }) => {
202
+ await using harness = await createTestApp({ plugins: [TestPlugin()] });
203
+ const a = { value: 0 };
204
+
205
+ const view = render(
206
+ harness,
207
+ <Profiler id='a' onRender={probe(a)}>
208
+ <SurfaceComponent type={RoleA} />
209
+ </Profiler>,
210
+ );
211
+ await view.findByTestId('a');
212
+
213
+ const aBaseline = a.value;
214
+ const rounds = 50;
215
+ const start = performance.now();
216
+ for (let i = 0; i < rounds; i++) {
217
+ act(() => {
218
+ harness.manager.capabilities.contribute({
219
+ module: `late-${i}`,
220
+ interface: Capabilities.ReactSurface,
221
+ implementation: create({
222
+ id: `beta${i}`,
223
+ filter: makeFilter(RoleB, () => false),
224
+ component: () => null,
225
+ }),
226
+ });
227
+ });
228
+ }
229
+ const elapsed = performance.now() - start;
230
+
231
+ // Role A never re-renders despite 50 unrelated contributions.
232
+ expect(a.value).toBe(aBaseline);
233
+ // eslint-disable-next-line no-console
234
+ console.log(
235
+ `[surface-bench] ${rounds} unrelated contributions: roleA re-renders=${a.value - aBaseline}, ${elapsed.toFixed(1)}ms`,
236
+ );
237
+ });
238
+ });
239
+
240
+ // Reproduces the pre-change global subscription + per-render scan/sort dispatch.
241
+ const EMPTY_DATA: Record<string, unknown> = {};
242
+ const legacyFindCandidates = (surfaces: Definition[], role: string): Definition[] =>
243
+ surfaces
244
+ .filter((definition) =>
245
+ Array.isArray(definition.role) ? definition.role.includes(role) : definition.role === role,
246
+ )
247
+ .filter(({ filter }) => (filter ? filter(EMPTY_DATA, role) : true))
248
+ .toSorted(Position.compare);
249
+
250
+ const LegacySurface = ({ role }: { role: string }) => {
251
+ // Subscribes to ALL surface contributions (the pre-change behaviour).
252
+ const surfaces = useSurfaces();
253
+ const candidates = legacyFindCandidates(surfaces, role);
254
+ return <span data-count={candidates.length} />;
255
+ };
256
+
257
+ describe('SurfaceComponent quantified comparison (per-role vs global subscription)', () => {
258
+ const ROLE_COUNT = 10;
259
+ const SURFACES_PER_ROLE = 10;
260
+ const ROUNDS = ROLE_COUNT; // one contribution per role.
261
+
262
+ const roles = Array.from({ length: ROLE_COUNT }, (_, i) =>
263
+ Role.make<Record<string, unknown>>(`org.dxos.test.role.r${i}`),
264
+ );
265
+
266
+ const benchMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test.surfaceBench'), name: 'SurfaceBenchTest' });
267
+ const BenchPlugin = Plugin.define(benchMeta).pipe(
268
+ Plugin.addModule({
269
+ id: 'surfaces',
270
+ activatesOn: ActivationEvents.SetupReactSurface,
271
+ activate: () =>
272
+ Effect.succeed(
273
+ Capability.contributes(
274
+ Capabilities.ReactSurface,
275
+ roles.flatMap((role, ri) =>
276
+ Array.from({ length: SURFACES_PER_ROLE }, (_, si) =>
277
+ create({ id: `r${ri}s${si}`, filter: makeFilter(role), component: () => <span /> }),
278
+ ),
279
+ ),
280
+ ),
281
+ ),
282
+ }),
283
+ Plugin.make,
284
+ );
285
+
286
+ test('per-role subscription scales re-renders by role count, not fleet size', async ({ expect }) => {
287
+ const fleet = ROLE_COUNT * SURFACES_PER_ROLE;
288
+
289
+ const measure = async (mode: 'new' | 'legacy') => {
290
+ await using harness = await createTestApp({ plugins: [BenchPlugin()] });
291
+ const commits = { value: 0 };
292
+ const tree = roles.flatMap((role, ri) =>
293
+ Array.from({ length: SURFACES_PER_ROLE }, (_, si) => (
294
+ <Profiler key={`${ri}.${si}`} id={`${ri}.${si}`} onRender={() => commits.value++}>
295
+ {mode === 'new' ? <SurfaceComponent type={role} /> : <LegacySurface role={role.role} />}
296
+ </Profiler>
297
+ )),
298
+ );
299
+ render(harness, <>{tree}</>);
300
+
301
+ const baseline = commits.value;
302
+ const start = performance.now();
303
+ for (let round = 0; round < ROUNDS; round++) {
304
+ const role = roles[round % ROLE_COUNT];
305
+ act(() => {
306
+ harness.manager.capabilities.contribute({
307
+ module: `extra-${round}`,
308
+ interface: Capabilities.ReactSurface,
309
+ implementation: create({
310
+ id: `extra${round}`,
311
+ filter: makeFilter(role, () => false),
312
+ component: () => null,
313
+ }),
314
+ });
315
+ });
316
+ }
317
+ const elapsed = performance.now() - start;
318
+ return { reRenders: commits.value - baseline, elapsed };
319
+ };
320
+
321
+ const legacy = await measure('legacy');
322
+ const next = await measure('new');
323
+
324
+ const legacyPerContribution = legacy.reRenders / ROUNDS;
325
+ const nextPerContribution = next.reRenders / ROUNDS;
326
+ // eslint-disable-next-line no-console
327
+ console.log(
328
+ `[surface-bench] fleet=${fleet} surfaces across ${ROLE_COUNT} roles, ${ROUNDS} contributions (1/role):\n` +
329
+ ` legacy (global subscription): ${legacy.reRenders} re-renders (${legacyPerContribution}/contribution), ${legacy.elapsed.toFixed(1)}ms\n` +
330
+ ` per-role subscription: ${next.reRenders} re-renders (${nextPerContribution}/contribution), ${next.elapsed.toFixed(1)}ms\n` +
331
+ ` reduction: ${(legacy.reRenders / Math.max(next.reRenders, 1)).toFixed(1)}x`,
332
+ );
333
+
334
+ // Legacy: the entire fleet re-renders on every contribution.
335
+ expect(legacyPerContribution).toBe(fleet);
336
+ // Per-role: re-renders per contribution are bounded by the affected role's surfaces
337
+ // (plus a small fleet-independent commit-phase constant) — NOT the fleet size.
338
+ expect(nextPerContribution).toBeLessThanOrEqual(3 * SURFACES_PER_ROLE);
339
+ expect(next.reRenders).toBeLessThan(legacy.reRenders / 2);
340
+ });
341
+ });
342
+
343
+ describe('SurfaceComponent dev metrics', () => {
344
+ test('records dispatch + candidate count and flags unstable data', async ({ expect }) => {
345
+ setSurfaceDebug(true);
346
+ surfaceMetrics.clear();
347
+ try {
348
+ await using harness = await createTestApp({ plugins: [TestPlugin()] });
349
+
350
+ // Host passes a fresh `data` object of identical content on every render — the unstable-prop footgun.
351
+ let bump: () => void = () => {};
352
+ const Host = () => {
353
+ const [, setN] = useState(0);
354
+ bump = () => setN((n) => n + 1);
355
+ return <SurfaceComponent type={RoleA} data={{ k: 1 }} />;
356
+ };
357
+
358
+ const view = render(harness, <Host />);
359
+ await view.findByTestId('a');
360
+ for (let i = 0; i < 5; i++) {
361
+ act(() => bump());
362
+ }
363
+ await flushMetrics();
364
+
365
+ const metric = surfaceMetrics.getSnapshot().find((entry) => entry.surfaceId === 'alpha');
366
+ expect(metric?.candidates).toBe(1);
367
+ expect(metric?.dispatches).toBeGreaterThan(1);
368
+ expect(metric?.mounts).toBe(1);
369
+ expect(metric?.dataUnstable).toBe(true);
370
+ } finally {
371
+ setSurfaceDebug(false);
372
+ }
373
+ });
374
+
375
+ test('stable data is not flagged unstable', async ({ expect }) => {
376
+ setSurfaceDebug(true);
377
+ surfaceMetrics.clear();
378
+ try {
379
+ await using harness = await createTestApp({ plugins: [TestPlugin()] });
380
+
381
+ // Same `data` reference across renders.
382
+ const STABLE = { k: 1 };
383
+ let bump: () => void = () => {};
384
+ const Host = () => {
385
+ const [, setN] = useState(0);
386
+ bump = () => setN((n) => n + 1);
387
+ return <SurfaceComponent type={RoleA} data={STABLE} />;
388
+ };
389
+
390
+ const view = render(harness, <Host />);
391
+ await view.findByTestId('a');
392
+ for (let i = 0; i < 5; i++) {
393
+ act(() => bump());
394
+ }
395
+ await flushMetrics();
396
+
397
+ const metric = surfaceMetrics.getSnapshot().find((entry) => entry.surfaceId === 'alpha');
398
+ expect(metric?.dataUnstable).toBe(false);
399
+ } finally {
400
+ setSurfaceDebug(false);
401
+ }
402
+ });
403
+ });