@dxos/app-framework 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.mts +1 -1
- package/.storybook/preview.mts +2 -2
- package/CHANGELOG.md +93 -0
- package/TASKS.md +11 -0
- package/dist/lib/chunk-SurfaceManager.mjs +960 -0
- package/dist/lib/chunk-SurfaceManager.mjs.map +1 -0
- package/dist/lib/chunk-capability.mjs +15 -0
- package/dist/lib/chunk-capability.mjs.map +1 -0
- package/dist/lib/{browser/chunk-FJ4765WW.mjs → chunk-context.mjs} +4 -5
- package/dist/lib/chunk-context.mjs.map +1 -0
- package/dist/lib/chunk-history-tracker.mjs +160 -0
- package/dist/lib/chunk-history-tracker.mjs.map +1 -0
- package/dist/lib/chunk-plugin-manager.mjs +1390 -0
- package/dist/lib/chunk-plugin-manager.mjs.map +1 -0
- package/dist/lib/chunk-plugin-manifest.mjs +125 -0
- package/dist/lib/chunk-plugin-manifest.mjs.map +1 -0
- package/dist/lib/chunk-process-manager-capability.mjs +126 -0
- package/dist/lib/chunk-process-manager-capability.mjs.map +1 -0
- package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
- package/dist/lib/chunk-url-loader.mjs +347 -0
- package/dist/lib/chunk-url-loader.mjs.map +1 -0
- package/dist/lib/cli.mjs +78 -0
- package/dist/lib/cli.mjs.map +1 -0
- package/dist/lib/common/activation-events.mjs +35 -0
- package/dist/lib/common/activation-events.mjs.map +1 -0
- package/dist/lib/common/capabilities.mjs +184 -0
- package/dist/lib/common/capabilities.mjs.map +1 -0
- package/dist/lib/config.mjs +2 -0
- package/dist/lib/core/activation-event.mjs +55 -0
- package/dist/lib/core/activation-event.mjs.map +1 -0
- package/dist/lib/core/capability.mjs +189 -0
- package/dist/lib/core/capability.mjs.map +1 -0
- package/dist/lib/core/plugin-manager.mjs +2 -0
- package/dist/lib/core/plugin.mjs +340 -0
- package/dist/lib/core/plugin.mjs.map +1 -0
- package/dist/lib/core/url-loader.mjs +2 -0
- package/dist/lib/index.mjs +134 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing/react.mjs +70 -0
- package/dist/lib/testing/react.mjs.map +1 -0
- package/dist/lib/testing.mjs +399 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/ui.mjs +636 -0
- package/dist/lib/ui.mjs.map +1 -0
- package/dist/lib/vite-plugin.mjs +986 -0
- package/dist/lib/vite-plugin.mjs.map +1 -0
- package/dist/plugin/node-esm/index.mjs +4 -3
- package/dist/plugin/node-esm/index.mjs.map +3 -3
- package/dist/plugin/node-esm/meta.json +1 -1
- package/dist/types/src/common/Role.d.ts +31 -0
- package/dist/types/src/common/Role.d.ts.map +1 -0
- package/dist/types/src/common/Role.test.d.ts +2 -0
- package/dist/types/src/common/Role.test.d.ts.map +1 -0
- package/dist/types/src/common/capabilities.d.ts +11 -2
- package/dist/types/src/common/capabilities.d.ts.map +1 -1
- package/dist/types/src/common/index.d.ts +1 -0
- package/dist/types/src/common/index.d.ts.map +1 -1
- package/dist/types/src/core/capability-manager.d.ts +1 -1
- package/dist/types/src/core/capability-manager.d.ts.map +1 -1
- package/dist/types/src/core/capability.d.ts +38 -5
- package/dist/types/src/core/capability.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manager.d.ts +1 -1
- package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin.d.ts +14 -1
- package/dist/types/src/core/plugin.d.ts.map +1 -1
- package/dist/types/src/core/registry.d.ts +1 -1
- package/dist/types/src/core/registry.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -1
- package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -1
- package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
- package/dist/types/src/testing/StorybookErrorFallback.d.ts +15 -0
- package/dist/types/src/testing/StorybookErrorFallback.d.ts.map +1 -0
- package/dist/types/src/testing/harness.d.ts +1 -1
- package/dist/types/src/testing/harness.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/react.d.ts +4 -4
- package/dist/types/src/testing/react.d.ts.map +1 -1
- package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
- package/dist/types/src/testing/withPluginManager.stories.d.ts +6 -0
- package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
- package/dist/types/src/testing/withSurfaceDebug.d.ts +10 -0
- package/dist/types/src/testing/withSurfaceDebug.d.ts.map +1 -0
- package/dist/types/src/ui/components/App/App.d.ts +0 -32
- package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/index.d.ts +1 -0
- package/dist/types/src/ui/components/App/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/loader.d.ts +35 -0
- package/dist/types/src/ui/components/App/loader.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts +26 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts +15 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/index.d.ts +2 -0
- package/dist/types/src/ui/components/HomeSection/index.d.ts.map +1 -0
- package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts +14 -0
- package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts.map +1 -0
- package/dist/types/src/ui/components/NamePopover/index.d.ts +2 -0
- package/dist/types/src/ui/components/NamePopover/index.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +21 -9
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts +4 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts +2 -0
- package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts +40 -0
- package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts +23 -0
- package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts +11 -0
- package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts +72 -0
- package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/index.d.ts +20 -25
- package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.d.ts +56 -52
- package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
- package/dist/types/src/ui/components/index.d.ts +2 -0
- package/dist/types/src/ui/components/index.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/moon.yml +25 -18
- package/package.json +58 -73
- package/src/common/Role.test.ts +41 -0
- package/src/common/Role.ts +54 -0
- package/src/common/capabilities.ts +17 -2
- package/src/common/index.ts +1 -0
- package/src/core/capability-manager.test.ts +1 -1
- package/src/core/capability-manager.ts +1 -1
- package/src/core/capability.ts +45 -6
- package/src/core/plugin-manager.test.ts +38 -3
- package/src/core/plugin-manager.ts +18 -1
- package/src/core/plugin.ts +16 -1
- package/src/core/registry.ts +1 -1
- package/src/plugin-process-manager/process-manager-capability.ts +25 -2
- package/src/testing/StorybookErrorFallback.tsx +40 -0
- package/src/testing/harness.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/react.test.tsx +2 -1
- package/src/testing/react.tsx +14 -8
- package/src/testing/withPluginManager.stories.tsx +16 -1
- package/src/testing/withPluginManager.tsx +13 -2
- package/src/testing/withSurfaceDebug.tsx +22 -0
- package/src/ui/components/App/App.tsx +7 -35
- package/src/ui/components/App/index.ts +1 -0
- package/src/ui/components/App/loader.ts +39 -0
- package/src/ui/components/HomeSection/HomeSection.stories.tsx +43 -0
- package/src/ui/components/HomeSection/HomeSection.tsx +69 -0
- package/src/ui/components/HomeSection/index.ts +5 -0
- package/src/ui/components/NamePopover/NamePopover.tsx +70 -0
- package/src/ui/components/NamePopover/index.ts +5 -0
- package/src/ui/components/Surface/DESIGN.md +136 -0
- package/src/ui/components/Surface/SurfaceComponent.stories.tsx +70 -41
- package/src/ui/components/Surface/SurfaceComponent.test.tsx +403 -0
- package/src/ui/components/Surface/SurfaceComponent.tsx +209 -189
- package/src/ui/components/Surface/SurfaceDebug.tsx +435 -0
- package/src/ui/components/Surface/SurfaceManager.ts +95 -0
- package/src/ui/components/Surface/SurfaceManagerContext.ts +23 -0
- package/src/ui/components/Surface/SurfaceMetrics.ts +202 -0
- package/src/ui/components/Surface/SurfaceProfilerContext.tsx +12 -2
- package/src/ui/components/Surface/index.ts +24 -22
- package/src/ui/components/Surface/types.test.ts +47 -54
- package/src/ui/components/Surface/types.ts +66 -91
- package/src/ui/components/index.ts +2 -0
- package/src/ui/hooks/useApp.tsx +7 -3
- package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +1 -1
- package/tsconfig.json +3 -0
- package/vite.config.ts +26 -0
- package/dist/lib/browser/capability-S44TE2H7.mjs +0 -35
- package/dist/lib/browser/capability-S44TE2H7.mjs.map +0 -7
- package/dist/lib/browser/chunk-3NQLTQCP.mjs +0 -47
- package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +0 -7
- package/dist/lib/browser/chunk-53HJHMS5.mjs +0 -7
- package/dist/lib/browser/chunk-53HJHMS5.mjs.map +0 -7
- package/dist/lib/browser/chunk-FJ4765WW.mjs.map +0 -7
- package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/browser/chunk-KRJ4KK2W.mjs +0 -505
- package/dist/lib/browser/chunk-KRJ4KK2W.mjs.map +0 -7
- package/dist/lib/browser/chunk-LUH4Y5F2.mjs +0 -430
- package/dist/lib/browser/chunk-LUH4Y5F2.mjs.map +0 -7
- package/dist/lib/browser/chunk-NKVRSMFN.mjs +0 -95
- package/dist/lib/browser/chunk-NKVRSMFN.mjs.map +0 -7
- package/dist/lib/browser/chunk-OO53M5UK.mjs +0 -260
- package/dist/lib/browser/chunk-OO53M5UK.mjs.map +0 -7
- package/dist/lib/browser/chunk-OPTS3ZSN.mjs +0 -83
- package/dist/lib/browser/chunk-OPTS3ZSN.mjs.map +0 -7
- package/dist/lib/browser/chunk-Q2GLJTVV.mjs +0 -12
- package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +0 -7
- package/dist/lib/browser/chunk-SYXIYT6T.mjs +0 -143
- package/dist/lib/browser/chunk-SYXIYT6T.mjs.map +0 -7
- package/dist/lib/browser/chunk-U5MESPM5.mjs +0 -603
- package/dist/lib/browser/chunk-U5MESPM5.mjs.map +0 -7
- package/dist/lib/browser/chunk-UGYCLOXE.mjs +0 -1471
- package/dist/lib/browser/chunk-UGYCLOXE.mjs.map +0 -7
- package/dist/lib/browser/chunk-VF37YTXJ.mjs +0 -28
- package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +0 -7
- package/dist/lib/browser/chunk-YUVQIOTB.mjs +0 -60
- package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +0 -7
- package/dist/lib/browser/cli/index.mjs +0 -74
- package/dist/lib/browser/cli/index.mjs.map +0 -7
- package/dist/lib/browser/common/activation-events.mjs +0 -20
- package/dist/lib/browser/common/activation-events.mjs.map +0 -7
- package/dist/lib/browser/common/capabilities.mjs +0 -56
- package/dist/lib/browser/common/capabilities.mjs.map +0 -7
- package/dist/lib/browser/config/index.mjs +0 -8
- package/dist/lib/browser/config/index.mjs.map +0 -7
- package/dist/lib/browser/core/activation-event.mjs +0 -20
- package/dist/lib/browser/core/activation-event.mjs.map +0 -7
- package/dist/lib/browser/core/capability.mjs +0 -32
- package/dist/lib/browser/core/capability.mjs.map +0 -7
- package/dist/lib/browser/core/plugin-manager.mjs +0 -19
- package/dist/lib/browser/core/plugin-manager.mjs.map +0 -7
- package/dist/lib/browser/core/plugin.mjs +0 -51
- package/dist/lib/browser/core/plugin.mjs.map +0 -7
- package/dist/lib/browser/core/url-loader.mjs +0 -24
- package/dist/lib/browser/core/url-loader.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -98
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs +0 -132
- package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs.map +0 -7
- package/dist/lib/browser/testing/index.mjs +0 -359
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/browser/testing/react.mjs +0 -78
- package/dist/lib/browser/testing/react.mjs.map +0 -7
- package/dist/lib/browser/ui/index.mjs +0 -60
- package/dist/lib/browser/ui/index.mjs.map +0 -7
- package/dist/lib/node-esm/capability-3YRF77LV.mjs +0 -36
- package/dist/lib/node-esm/capability-3YRF77LV.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +0 -9
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-37Z53PXZ.mjs +0 -10
- package/dist/lib/node-esm/chunk-37Z53PXZ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-4SE7SX7N.mjs +0 -261
- package/dist/lib/node-esm/chunk-4SE7SX7N.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-BMW6P6QA.mjs +0 -48
- package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-COHOWGGJ.mjs +0 -144
- package/dist/lib/node-esm/chunk-COHOWGGJ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-DRT75WGD.mjs +0 -604
- package/dist/lib/node-esm/chunk-DRT75WGD.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FKMVORZN.mjs +0 -431
- package/dist/lib/node-esm/chunk-FKMVORZN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FPW45EZH.mjs +0 -14
- package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs +0 -84
- package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-IL7O7IRX.mjs +0 -1472
- package/dist/lib/node-esm/chunk-IL7O7IRX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-ND2HETHD.mjs +0 -506
- package/dist/lib/node-esm/chunk-ND2HETHD.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-TLKYZKPW.mjs +0 -29
- package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-VCPGX7WN.mjs +0 -62
- package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XSZMX53L.mjs +0 -96
- package/dist/lib/node-esm/chunk-XSZMX53L.mjs.map +0 -7
- package/dist/lib/node-esm/cli/index.mjs +0 -75
- package/dist/lib/node-esm/cli/index.mjs.map +0 -7
- package/dist/lib/node-esm/common/activation-events.mjs +0 -21
- package/dist/lib/node-esm/common/activation-events.mjs.map +0 -7
- package/dist/lib/node-esm/common/capabilities.mjs +0 -57
- package/dist/lib/node-esm/common/capabilities.mjs.map +0 -7
- package/dist/lib/node-esm/config/index.mjs +0 -9
- package/dist/lib/node-esm/config/index.mjs.map +0 -7
- package/dist/lib/node-esm/core/activation-event.mjs +0 -21
- package/dist/lib/node-esm/core/activation-event.mjs.map +0 -7
- package/dist/lib/node-esm/core/capability.mjs +0 -33
- package/dist/lib/node-esm/core/capability.mjs.map +0 -7
- package/dist/lib/node-esm/core/plugin-manager.mjs +0 -20
- package/dist/lib/node-esm/core/plugin-manager.mjs.map +0 -7
- package/dist/lib/node-esm/core/plugin.mjs +0 -52
- package/dist/lib/node-esm/core/plugin.mjs.map +0 -7
- package/dist/lib/node-esm/core/url-loader.mjs +0 -25
- package/dist/lib/node-esm/core/url-loader.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -99
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs +0 -133
- package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs.map +0 -7
- package/dist/lib/node-esm/testing/index.mjs +0 -360
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/testing/react.mjs +0 -79
- package/dist/lib/node-esm/testing/react.mjs.map +0 -7
- package/dist/lib/node-esm/ui/index.mjs +0 -61
- package/dist/lib/node-esm/ui/index.mjs.map +0 -7
- package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts +0 -11
- package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts.map +0 -1
- package/src/ui/components/Surface/SurfaceInfo.tsx +0 -106
- 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
|
+
});
|