@dxos/app-framework 0.8.4-main.b97322e → 0.8.4-main.dedc0f3

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 (153) hide show
  1. package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
  2. package/.swc/plugins/{v7_linux_x86_64_13.0.0/fce1bdb8e20a094e4af08bad09cc81497ed0e2e7c51223b07d371063cca18429 → linux_x86_64_19.0.0/fce1bdb8e20a094e4af08bad09cc81497ed0e2e7c51223b07d371063cca18429.wasmer-v7} +0 -0
  3. package/dist/lib/browser/{app-graph-builder-LYF7EKNN.mjs → app-graph-builder-AFFC6VB2.mjs} +3 -3
  4. package/dist/lib/browser/app-graph-builder-AFFC6VB2.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-FMN65HSW.mjs → chunk-OZY7HV2A.mjs} +376 -252
  6. package/dist/lib/browser/chunk-OZY7HV2A.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-FO2PH7M3.mjs → chunk-T6M7JB7M.mjs} +186 -130
  8. package/dist/lib/browser/chunk-T6M7JB7M.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +11 -5
  10. package/dist/lib/browser/index.mjs.map +2 -2
  11. package/dist/lib/browser/{intent-dispatcher-LSYQZSEB.mjs → intent-dispatcher-QG7UPGQX.mjs} +2 -2
  12. package/dist/lib/browser/{intent-resolver-ZTNOSO3A.mjs → intent-resolver-4S4PSTM5.mjs} +2 -2
  13. package/dist/lib/browser/intent-resolver-4S4PSTM5.mjs.map +7 -0
  14. package/dist/lib/browser/meta.json +1 -1
  15. package/dist/lib/browser/{store-KML2R4IE.mjs → store-6E33KLGK.mjs} +2 -2
  16. package/dist/lib/browser/{store-KML2R4IE.mjs.map → store-6E33KLGK.mjs.map} +1 -1
  17. package/dist/lib/browser/testing/index.mjs +5 -7
  18. package/dist/lib/browser/testing/index.mjs.map +3 -3
  19. package/dist/lib/browser/worker.mjs +7 -1
  20. package/dist/lib/node-esm/{app-graph-builder-SAOWGJDK.mjs → app-graph-builder-S4OAULX5.mjs} +3 -3
  21. package/dist/lib/node-esm/app-graph-builder-S4OAULX5.mjs.map +7 -0
  22. package/dist/lib/node-esm/{chunk-ZEZ4FVEU.mjs → chunk-F63ZRXMK.mjs} +376 -252
  23. package/dist/lib/node-esm/chunk-F63ZRXMK.mjs.map +7 -0
  24. package/dist/lib/node-esm/{chunk-73HGSHKE.mjs → chunk-HJFU7QOR.mjs} +186 -130
  25. package/dist/lib/node-esm/chunk-HJFU7QOR.mjs.map +7 -0
  26. package/dist/lib/node-esm/index.mjs +11 -5
  27. package/dist/lib/node-esm/index.mjs.map +2 -2
  28. package/dist/lib/node-esm/{intent-dispatcher-6CYNGPSW.mjs → intent-dispatcher-NXBGPJOX.mjs} +2 -2
  29. package/dist/lib/node-esm/{intent-resolver-W7Z7WFFM.mjs → intent-resolver-2ZKXI5ET.mjs} +2 -2
  30. package/dist/lib/node-esm/intent-resolver-2ZKXI5ET.mjs.map +7 -0
  31. package/dist/lib/node-esm/meta.json +1 -1
  32. package/dist/lib/node-esm/{store-QEXGXLWZ.mjs → store-QQUTQHHT.mjs} +2 -2
  33. package/dist/lib/node-esm/{store-QEXGXLWZ.mjs.map → store-QQUTQHHT.mjs.map} +1 -1
  34. package/dist/lib/node-esm/testing/index.mjs +5 -7
  35. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  36. package/dist/lib/node-esm/worker.mjs +7 -1
  37. package/dist/types/src/common/capabilities.d.ts +75 -8
  38. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  39. package/dist/types/src/common/collaboration.d.ts +9 -8
  40. package/dist/types/src/common/collaboration.d.ts.map +1 -1
  41. package/dist/types/src/common/events.d.ts.map +1 -1
  42. package/dist/types/src/common/index.d.ts +1 -1
  43. package/dist/types/src/common/index.d.ts.map +1 -1
  44. package/dist/types/src/common/surface.d.ts +1 -1
  45. package/dist/types/src/common/surface.d.ts.map +1 -1
  46. package/dist/types/src/components/App.d.ts +10 -0
  47. package/dist/types/src/components/App.d.ts.map +1 -0
  48. package/dist/types/src/components/App.stories.d.ts +15 -0
  49. package/dist/types/src/components/App.stories.d.ts.map +1 -0
  50. package/dist/types/src/components/DefaultFallback.d.ts +8 -0
  51. package/dist/types/src/components/DefaultFallback.d.ts.map +1 -0
  52. package/dist/types/src/components/index.d.ts +2 -0
  53. package/dist/types/src/components/index.d.ts.map +1 -0
  54. package/dist/types/src/{App.d.ts → components/useApp.d.ts} +7 -6
  55. package/dist/types/src/components/useApp.d.ts.map +1 -0
  56. package/dist/types/src/components/useLoading.d.ts +19 -0
  57. package/dist/types/src/components/useLoading.d.ts.map +1 -0
  58. package/dist/types/src/core/capabilities.d.ts +4 -1
  59. package/dist/types/src/core/capabilities.d.ts.map +1 -1
  60. package/dist/types/src/core/manager.d.ts +6 -2
  61. package/dist/types/src/core/manager.d.ts.map +1 -1
  62. package/dist/types/src/index.d.ts +1 -1
  63. package/dist/types/src/index.d.ts.map +1 -1
  64. package/dist/types/src/playground/debug/Debug.d.ts +1 -1
  65. package/dist/types/src/playground/generator/Main.d.ts +1 -1
  66. package/dist/types/src/playground/generator/Toolbar.d.ts +1 -1
  67. package/dist/types/src/playground/generator/Toolbar.d.ts.map +1 -1
  68. package/dist/types/src/playground/generator/generator.d.ts.map +1 -1
  69. package/dist/types/src/playground/layout/Layout.d.ts +2 -2
  70. package/dist/types/src/playground/logger/Toolbar.d.ts +1 -1
  71. package/dist/types/src/playground/logger/Toolbar.d.ts.map +1 -1
  72. package/dist/types/src/playground/logger/plugin.d.ts.map +1 -1
  73. package/dist/types/src/playground/playground.stories.d.ts +5 -3
  74. package/dist/types/src/playground/playground.stories.d.ts.map +1 -1
  75. package/dist/types/src/plugin-intent/IntentPlugin.d.ts.map +1 -1
  76. package/dist/types/src/plugin-intent/index.d.ts +1 -0
  77. package/dist/types/src/plugin-intent/index.d.ts.map +1 -1
  78. package/dist/types/src/plugin-intent/intent-dispatcher.d.ts +3 -3
  79. package/dist/types/src/plugin-intent/intent-dispatcher.d.ts.map +1 -1
  80. package/dist/types/src/plugin-settings/SettingsPlugin.d.ts.map +1 -1
  81. package/dist/types/src/plugin-settings/app-graph-builder.d.ts +1 -1
  82. package/dist/types/src/plugin-settings/app-graph-builder.d.ts.map +1 -1
  83. package/dist/types/src/plugin-settings/intent-resolver.d.ts +1 -1
  84. package/dist/types/src/plugin-settings/intent-resolver.d.ts.map +1 -1
  85. package/dist/types/src/plugin-settings/store.d.ts +1 -1
  86. package/dist/types/src/plugin-settings/store.d.ts.map +1 -1
  87. package/dist/types/src/react/ErrorBoundary.d.ts +13 -14
  88. package/dist/types/src/react/ErrorBoundary.d.ts.map +1 -1
  89. package/dist/types/src/react/IntentContext.d.ts.map +1 -1
  90. package/dist/types/src/react/Surface.d.ts.map +1 -1
  91. package/dist/types/src/react/Surface.stories.d.ts +6 -4
  92. package/dist/types/src/react/Surface.stories.d.ts.map +1 -1
  93. package/dist/types/src/react/common.d.ts.map +1 -1
  94. package/dist/types/src/react/useCapabilities.d.ts.map +1 -1
  95. package/dist/types/src/testing/withPluginManager.d.ts +4 -2
  96. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  97. package/dist/types/src/testing/withPluginManager.stories.d.ts +9 -3
  98. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  99. package/dist/types/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +28 -24
  101. package/src/common/capabilities.ts +91 -10
  102. package/src/common/collaboration.ts +5 -8
  103. package/src/common/events.ts +3 -1
  104. package/src/common/index.ts +1 -1
  105. package/src/common/surface.ts +1 -1
  106. package/src/components/App.stories.tsx +35 -0
  107. package/src/components/App.tsx +59 -0
  108. package/src/components/DefaultFallback.tsx +26 -0
  109. package/src/components/index.ts +5 -0
  110. package/src/{App.tsx → components/useApp.tsx} +20 -130
  111. package/src/components/useLoading.tsx +70 -0
  112. package/src/core/capabilities.test.ts +1 -1
  113. package/src/core/capabilities.ts +11 -6
  114. package/src/core/manager.test.ts +4 -3
  115. package/src/core/manager.ts +132 -54
  116. package/src/helpers.test.ts +1 -1
  117. package/src/index.ts +1 -1
  118. package/src/playground/debug/Debug.tsx +1 -1
  119. package/src/playground/generator/Toolbar.tsx +2 -1
  120. package/src/playground/generator/generator.ts +2 -2
  121. package/src/playground/layout/plugin.ts +1 -1
  122. package/src/playground/logger/Toolbar.tsx +2 -1
  123. package/src/playground/logger/plugin.ts +3 -2
  124. package/src/playground/playground.stories.tsx +15 -10
  125. package/src/plugin-intent/IntentPlugin.ts +2 -1
  126. package/src/plugin-intent/index.ts +1 -0
  127. package/src/plugin-intent/intent-dispatcher.test.ts +1 -1
  128. package/src/plugin-intent/intent-dispatcher.ts +10 -8
  129. package/src/plugin-settings/SettingsPlugin.ts +3 -2
  130. package/src/plugin-settings/app-graph-builder.ts +4 -3
  131. package/src/plugin-settings/intent-resolver.ts +3 -2
  132. package/src/plugin-settings/store.ts +1 -1
  133. package/src/react/ErrorBoundary.tsx +24 -15
  134. package/src/react/IntentContext.tsx +3 -2
  135. package/src/react/Surface.stories.tsx +21 -13
  136. package/src/react/Surface.tsx +4 -3
  137. package/src/react/common.ts +2 -1
  138. package/src/react/useCapabilities.ts +2 -1
  139. package/src/testing/withPluginManager.stories.tsx +9 -5
  140. package/src/testing/withPluginManager.tsx +13 -13
  141. package/tsconfig.json +1 -8
  142. package/.swc/plugins/v7_linux_x86_64_13.0.0/f45bdff002284d9e8f9ef3f0be909de12da36c049cbcf261ac78fc00abb09a2d +0 -0
  143. package/dist/lib/browser/app-graph-builder-LYF7EKNN.mjs.map +0 -7
  144. package/dist/lib/browser/chunk-FMN65HSW.mjs.map +0 -7
  145. package/dist/lib/browser/chunk-FO2PH7M3.mjs.map +0 -7
  146. package/dist/lib/browser/intent-resolver-ZTNOSO3A.mjs.map +0 -7
  147. package/dist/lib/node-esm/app-graph-builder-SAOWGJDK.mjs.map +0 -7
  148. package/dist/lib/node-esm/chunk-73HGSHKE.mjs.map +0 -7
  149. package/dist/lib/node-esm/chunk-ZEZ4FVEU.mjs.map +0 -7
  150. package/dist/lib/node-esm/intent-resolver-W7Z7WFFM.mjs.map +0 -7
  151. package/dist/types/src/App.d.ts.map +0 -1
  152. /package/dist/lib/browser/{intent-dispatcher-LSYQZSEB.mjs.map → intent-dispatcher-QG7UPGQX.mjs.map} +0 -0
  153. /package/dist/lib/node-esm/{intent-dispatcher-6CYNGPSW.mjs.map → intent-dispatcher-NXBGPJOX.mjs.map} +0 -0
@@ -0,0 +1,70 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useEffect, useState } from 'react';
6
+
7
+ import { type AppProps } from './App';
8
+
9
+ export enum LoadingState {
10
+ Loading = 0,
11
+ FadeIn = 1,
12
+ FadeOut = 2,
13
+ Done = 3,
14
+ }
15
+
16
+ /**
17
+ * To avoid "flashing" the placeholder, we wait a period of time before starting the loading animation.
18
+ * If loading completes during this time the placehoder is not shown, otherwise is it displayed for a minimum period of time.
19
+ *
20
+ * States:
21
+ * 0: Loading - Wait for a period of time before starting the loading animation.
22
+ * 1: Fade-in - Display a loading animation.
23
+ * 2: Fade-out - Fade out the loading animation.
24
+ * 3: Done - Remove the placeholder.
25
+ */
26
+ export const useLoading = (state: AppProps['state'], debounce = 0) => {
27
+ const [stage, setStage] = useState<LoadingState>(LoadingState.Loading);
28
+ useEffect(() => {
29
+ if (!debounce) {
30
+ return;
31
+ }
32
+
33
+ const i = setInterval(() => {
34
+ setStage((stage) => {
35
+ switch (stage) {
36
+ case LoadingState.Loading: {
37
+ if (!state.ready) {
38
+ return LoadingState.FadeIn;
39
+ } else {
40
+ clearInterval(i);
41
+ return LoadingState.Done;
42
+ }
43
+ }
44
+
45
+ case LoadingState.FadeIn: {
46
+ if (state.ready) {
47
+ return LoadingState.FadeOut;
48
+ }
49
+ break;
50
+ }
51
+
52
+ case LoadingState.FadeOut: {
53
+ clearInterval(i);
54
+ return LoadingState.Done;
55
+ }
56
+ }
57
+
58
+ return stage;
59
+ });
60
+ }, debounce);
61
+
62
+ return () => clearInterval(i);
63
+ }, [debounce]);
64
+
65
+ if (!debounce) {
66
+ return state.ready ? LoadingState.Done : LoadingState.Loading;
67
+ }
68
+
69
+ return stage;
70
+ };
@@ -6,7 +6,7 @@ import { Registry } from '@effect-rx/rx-react';
6
6
  import { Effect } from 'effect';
7
7
  import { describe, expect, it, onTestFinished } from 'vitest';
8
8
 
9
- import { defineCapability, PluginContext } from './capabilities';
9
+ import { PluginContext, defineCapability } from './capabilities';
10
10
 
11
11
  const defaultOptions = {
12
12
  activate: () => Effect.succeed(false),
@@ -22,6 +22,10 @@ export type InterfaceDef<T> = {
22
22
  identifier: string;
23
23
  };
24
24
 
25
+ export namespace InterfaceDef {
26
+ export type Implementation<I extends InterfaceDef<any>> = I extends InterfaceDef<infer T> ? T : never;
27
+ }
28
+
25
29
  /**
26
30
  * Helper to define the interface of a capability.
27
31
  */
@@ -69,16 +73,17 @@ class CapabilityImpl<T> {
69
73
  /**
70
74
  * Helper to define the implementation of a capability.
71
75
  */
72
- export const contributes = <T>(
73
- interfaceDef: Capability<T>['interface'],
74
- implementation: Capability<T>['implementation'],
75
- deactivate?: Capability<T>['deactivate'],
76
- ): Capability<T> => {
77
- return { interface: interfaceDef, implementation, deactivate } satisfies Capability<T>;
76
+ export const contributes = <I extends InterfaceDef<any>>(
77
+ interfaceDef: I,
78
+ implementation: Capability<InterfaceDef.Implementation<I>>['implementation'],
79
+ deactivate?: Capability<InterfaceDef.Implementation<I>>['deactivate'],
80
+ ): Capability<I> => {
81
+ return { interface: interfaceDef, implementation, deactivate } satisfies Capability<I>;
78
82
  };
79
83
 
80
84
  type LoadCapability<T, U> = () => Promise<{ default: (props: T) => MaybePromise<Capability<U>> }>;
81
85
  type LoadCapabilities<T> = () => Promise<{ default: (props: T) => MaybePromise<AnyCapability[]> }>;
86
+
82
87
  // TODO(wittjosiah): Not having the array be `any` causes type errors when using the lazy capability.
83
88
  type LazyCapability<T, U> = (props?: T) => Promise<() => Promise<Capability<U> | AnyCapability[]>>;
84
89
 
@@ -12,11 +12,12 @@ import { registerSignalsRuntime } from '@dxos/echo-signals';
12
12
  import { invariant } from '@dxos/invariant';
13
13
  import { live } from '@dxos/live-object';
14
14
 
15
- import { contributes, defineCapability, type PluginContext } from './capabilities';
15
+ import { Events } from '../common';
16
+
17
+ import { type PluginContext, contributes, defineCapability } from './capabilities';
16
18
  import { allOf, defineEvent, oneOf } from './events';
17
19
  import { PluginManager } from './manager';
18
- import { definePlugin, defineModule, type Plugin } from './plugin';
19
- import { Events } from '../common';
20
+ import { type Plugin, defineModule, definePlugin } from './plugin';
20
21
 
21
22
  registerSignalsRuntime();
22
23
 
@@ -4,16 +4,16 @@
4
4
 
5
5
  import { Registry } from '@effect-rx/rx-react';
6
6
  import { untracked } from '@preact/signals-core';
7
- import { Array as A, Effect, Either, Match, pipe } from 'effect';
7
+ import { Array, Duration, Effect, Fiber, HashSet, Match, Ref, pipe } from 'effect';
8
8
 
9
9
  import { Event } from '@dxos/async';
10
- import { live, type Live } from '@dxos/live-object';
10
+ import { type Live, live } from '@dxos/live-object';
11
11
  import { log } from '@dxos/log';
12
12
  import { type MaybePromise } from '@dxos/util';
13
13
 
14
14
  import { type AnyCapability, PluginContext } from './capabilities';
15
15
  import { type ActivationEvent, eventKey, getEvents, isAllOf } from './events';
16
- import { type PluginModule, type Plugin } from './plugin';
16
+ import { type Plugin, type PluginModule } from './plugin';
17
17
 
18
18
  // TODO(wittjosiah): Factor out?
19
19
  const isPromise = (value: unknown): value is Promise<unknown> => {
@@ -52,6 +52,9 @@ export class PluginManager {
52
52
  private readonly _state: Live<PluginManagerState>;
53
53
  private readonly _pluginLoader: PluginManagerOptions['pluginLoader'];
54
54
  private readonly _capabilities = new Map<string, AnyCapability[]>();
55
+ private readonly _moduleMemoMap = new Map<PluginModule['id'], Promise<AnyCapability[]>>();
56
+ private readonly _activatingEvents = Effect.runSync(Ref.make<string[]>([]));
57
+ private readonly _activatingModules = Effect.runSync(Ref.make<string[]>([]));
55
58
 
56
59
  constructor({
57
60
  pluginLoader,
@@ -74,8 +77,8 @@ export class PluginManager {
74
77
  enabled,
75
78
  modules: [],
76
79
  active: [],
77
- pendingReset: [],
78
80
  eventsFired: [],
81
+ pendingReset: [],
79
82
  });
80
83
  plugins.forEach((plugin) => this._addPlugin(plugin));
81
84
  core.forEach((id) => this.enable(id));
@@ -268,6 +271,7 @@ export class PluginManager {
268
271
  private _addPlugin(plugin: Plugin): void {
269
272
  untracked(() => {
270
273
  log('add plugin', { id: plugin.meta.id });
274
+ // TODO(wittjosiah): Find a way to add a warning for duplicate plugins that doesn't cause log spam.
271
275
  if (!this._state.plugins.includes(plugin)) {
272
276
  this._state.plugins.push(plugin);
273
277
  }
@@ -287,6 +291,7 @@ export class PluginManager {
287
291
  private _addModule(module: PluginModule): void {
288
292
  untracked(() => {
289
293
  log('add module', { id: module.id });
294
+ // TODO(wittjosiah): Find a way to add a warning for duplicate modules that doesn't cause log spam.
290
295
  if (!this._state.modules.includes(module)) {
291
296
  this._state.modules.push(module);
292
297
  }
@@ -329,7 +334,7 @@ export class PluginManager {
329
334
  .map(eventKey)
330
335
  .filter((key) => this._state.eventsFired.includes(key));
331
336
 
332
- const pendingReset = Array.from(new Set(activationEvents)).filter(
337
+ const pendingReset = Array.fromIterable(new Set(activationEvents)).filter(
333
338
  (event) => !this._state.pendingReset.includes(event),
334
339
  );
335
340
  if (pendingReset.length > 0) {
@@ -343,24 +348,42 @@ export class PluginManager {
343
348
  * @internal
344
349
  */
345
350
  // TODO(wittjosiah): Improve error typing.
346
- _activate(event: ActivationEvent | string): Effect.Effect<boolean, Error> {
351
+ _activate(
352
+ event: ActivationEvent | string,
353
+ params?: { before?: string; after?: string },
354
+ ): Effect.Effect<boolean, Error> {
347
355
  return Effect.gen(this, function* () {
348
356
  const key = typeof event === 'string' ? event : eventKey(event);
349
- log('activating', { key });
357
+ log('activating', { key, ...params });
358
+ yield* Ref.update(this._activatingEvents, (activating) => Array.append(activating, key));
350
359
  const pendingIndex = this._state.pendingReset.findIndex((event) => event === key);
351
360
  if (pendingIndex !== -1) {
352
361
  this._state.pendingReset.splice(pendingIndex, 1);
353
362
  }
354
363
 
364
+ const activatingEvents = yield* this._activatingEvents;
365
+ const activatingModules = yield* this._activatingModules;
355
366
  const modules = this._getInactiveModulesByEvent(key).filter((module) => {
356
367
  const allOf = isAllOf(module.activatesOn);
357
368
  if (!allOf) {
358
369
  return true;
359
370
  }
360
371
 
372
+ // Check to see if all of the events in the `allOf` have been fired.
373
+ // An event can be considered "fired" if it is in the `eventsFired` list or if it is currently being activated.
361
374
  const events = module.activatesOn.events.filter((event) => eventKey(event) !== key);
362
- return events.every((event) => this._state.eventsFired.includes(eventKey(event)));
375
+ return (
376
+ events.every(
377
+ (event) => this._state.eventsFired.includes(eventKey(event)) || activatingEvents.includes(eventKey(event)),
378
+ ) && !activatingModules.includes(module.id)
379
+ );
363
380
  });
381
+ yield* Ref.update(this._activatingModules, (activating) =>
382
+ Array.appendAll(
383
+ activating,
384
+ modules.map((module) => module.id),
385
+ ),
386
+ );
364
387
  if (modules.length === 0) {
365
388
  log('no modules to activate', { key });
366
389
  if (!this._state.eventsFired.includes(key)) {
@@ -372,31 +395,53 @@ export class PluginManager {
372
395
  log('activating modules', { key, modules: modules.map((module) => module.id) });
373
396
  this.activation.emit({ event: key, state: 'activating' });
374
397
 
398
+ // Fire activatesBefore events.
399
+ yield* pipe(
400
+ modules,
401
+ Array.flatMap((module) => module.activatesBefore ?? []),
402
+ HashSet.fromIterable,
403
+ HashSet.toValues,
404
+ Array.filter((event) => !activatingEvents.includes(eventKey(event))),
405
+ Array.map((event) => this._activate(event, { before: key })),
406
+ Effect.allWith({ concurrency: 'unbounded' }),
407
+ );
408
+
375
409
  // Concurrently triggers loading of lazy capabilities.
376
- const getCapabilities = yield* Effect.all(
377
- modules.map(({ activate }) =>
378
- Effect.tryPromise({
379
- try: async () => activate(this.context),
380
- catch: (error) => error as Error,
381
- }),
382
- ),
383
- { concurrency: 'unbounded' },
410
+ const getCapabilities = yield* pipe(
411
+ modules,
412
+ Array.map((mod) => this._loadModule(mod)),
413
+ Effect.allWith({ concurrency: 'unbounded' }),
414
+ Effect.catchAll((error) => {
415
+ this.activation.emit({ event: key, state: 'error', error });
416
+ return Effect.fail(error);
417
+ }),
384
418
  );
385
419
 
386
- const result = yield* pipe(
420
+ // Contribute the capabilities from the activated modules.
421
+ yield* pipe(
387
422
  modules,
388
- A.zip(getCapabilities),
389
- A.map(([module, getCapabilities]) => this._activateModule(module, getCapabilities)),
423
+ Array.zip(getCapabilities),
424
+ Array.map(([module, capabilities]) => this._contributeCapabilities(module, capabilities)),
390
425
  // TODO(wittjosiah): This currently can't be run in parallel.
391
426
  // Running this with concurrency causes races with `allOf` activation events.
392
427
  Effect.all,
393
- Effect.either,
394
428
  );
395
429
 
396
- if (Either.isLeft(result)) {
397
- this.activation.emit({ event: key, state: 'error', error: result.left });
398
- yield* Effect.fail(result.left);
399
- }
430
+ // Fire activatesAfter events.
431
+ yield* pipe(
432
+ modules,
433
+ Array.flatMap((module) => module.activatesAfter ?? []),
434
+ HashSet.fromIterable,
435
+ HashSet.toValues,
436
+ Array.filter((event) => !activatingEvents.includes(eventKey(event))),
437
+ Array.map((event) => this._activate(event, { after: key })),
438
+ Effect.allWith({ concurrency: 'unbounded' }),
439
+ );
440
+
441
+ yield* Ref.update(this._activatingEvents, (activating) => Array.filter(activating, (event) => event !== key));
442
+ yield* Ref.update(this._activatingModules, (activating) =>
443
+ Array.filter(activating, (module) => !modules.map((module) => module.id).includes(module)),
444
+ );
400
445
 
401
446
  if (!this._state.eventsFired.includes(key)) {
402
447
  this._state.eventsFired.push(key);
@@ -409,43 +454,60 @@ export class PluginManager {
409
454
  });
410
455
  }
411
456
 
412
- private _activateModule(
413
- module: PluginModule,
414
- getCapabilities: AnyCapability | AnyCapability[] | (() => Promise<AnyCapability | AnyCapability[]>),
415
- ): Effect.Effect<void, Error> {
416
- return Effect.gen(this, function* () {
417
- yield* Effect.all(module.activatesBefore?.map((event) => this._activate(event)) ?? [], {
418
- concurrency: 'unbounded',
419
- });
457
+ // Memoized with _moduleMemoMap
458
+ private _loadModule = (mod: PluginModule): Effect.Effect<AnyCapability[], Error> =>
459
+ Effect.tryPromise({
460
+ try: async () => {
461
+ const entry = this._moduleMemoMap.get(mod.id);
462
+ if (entry) {
463
+ return entry;
464
+ }
420
465
 
421
- log('activating module...', { module: module.id });
422
- // TODO(wittjosiah): This is not handling errors thrown if this is synchronous.
423
- const maybeCapabilities = typeof getCapabilities === 'function' ? getCapabilities() : getCapabilities;
424
- const resolvedCapabilities = yield* Match.value(maybeCapabilities).pipe(
425
- // TODO(wittjosiah): Activate with an effect?
426
- // Match.when(Effect.isEffect, (effect) => effect),
427
- Match.when(isPromise, (promise) =>
428
- Effect.tryPromise({
429
- try: () => promise,
430
- catch: (error) => error as Error,
431
- }),
466
+ const promise = (async () => {
467
+ const start = performance.now();
468
+ let failed = false;
469
+ try {
470
+ log('loading module', { module: mod.id });
471
+ // TODO(wittjosiah): Support activation with an effect.
472
+ let activationResult = await mod.activate(this.context);
473
+ if (typeof activationResult === 'function') {
474
+ activationResult = await activationResult();
475
+ }
476
+ return Array.isArray(activationResult) ? activationResult : [activationResult];
477
+ } catch (error) {
478
+ failed = true;
479
+ throw error;
480
+ } finally {
481
+ performance.measure('activate-module', {
482
+ start,
483
+ end: performance.now(),
484
+ detail: {
485
+ module: mod.id,
486
+ },
487
+ });
488
+ log('loaded module', { module: mod.id, elapsed: performance.now() - start, failed });
489
+ }
490
+ })();
491
+ this._moduleMemoMap.set(mod.id, promise);
492
+ return promise;
493
+ },
494
+ catch: (error) => error as Error,
495
+ }).pipe(
496
+ Effect.withSpan('PluginManager._loadModule'),
497
+ together(
498
+ Effect.sleep(Duration.seconds(10)).pipe(
499
+ Effect.andThen(Effect.sync(() => log.warn(`module is taking a long time to activate`, { module: mod.id }))),
432
500
  ),
433
- Match.orElse((program) => Effect.succeed(program)),
434
- );
435
- const capabilities = Match.value(resolvedCapabilities).pipe(
436
- Match.when(Array.isArray, (array) => array),
437
- Match.orElse((value) => [value]),
438
- );
501
+ ),
502
+ );
503
+
504
+ private _contributeCapabilities(module: PluginModule, capabilities: AnyCapability[]): Effect.Effect<void, Error> {
505
+ return Effect.gen(this, function* () {
439
506
  capabilities.forEach((capability) => {
440
507
  this.context.contributeCapability({ module: module.id, ...capability });
441
508
  });
442
509
  this._state.active.push(module.id);
443
510
  this._capabilities.set(module.id, capabilities);
444
- log('activated module', { module: module.id });
445
-
446
- yield* Effect.all(module.activatesAfter?.map((event) => this._activate(event)) ?? [], {
447
- concurrency: 'unbounded',
448
- });
449
511
  });
450
512
  }
451
513
 
@@ -469,6 +531,7 @@ export class PluginManager {
469
531
  return Effect.gen(this, function* () {
470
532
  const id = module.id;
471
533
  log('deactivating', { id });
534
+ this._moduleMemoMap.delete(id);
472
535
 
473
536
  const capabilities = this._capabilities.get(id);
474
537
  if (capabilities) {
@@ -517,3 +580,18 @@ export class PluginManager {
517
580
  });
518
581
  }
519
582
  }
583
+
584
+ /**
585
+ * Runs an effect concurrently with another effect.
586
+ * If the first effect completes, the second effect is interrupted.
587
+ */
588
+ // TODO(dmaretskyi): Effect.race > Effect.asVoid
589
+ const together =
590
+ <R1>(togetherEffect: Effect.Effect<void, never, R1>) =>
591
+ <A, E, R2>(effect: Effect.Effect<A, E, R2>): Effect.Effect<A, E, R1 | R2> =>
592
+ Effect.gen(function* () {
593
+ const togetherFiber = yield* Effect.fork(togetherEffect);
594
+ const result = yield* effect;
595
+ yield* Fiber.interrupt(togetherFiber);
596
+ return result;
597
+ });
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { describe, it, expect } from 'vitest';
5
+ import { describe, expect, it } from 'vitest';
6
6
 
7
7
  import { topologicalSort } from './helpers';
8
8
 
package/src/index.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './App';
6
5
  export * from './common';
6
+ export * from './components';
7
7
  export * from './core';
8
8
  export * from './plugin-intent';
9
9
  export * from './plugin-settings';
@@ -22,7 +22,7 @@ export const Debug = () => {
22
22
  };
23
23
 
24
24
  return (
25
- <SyntaxHighlighter language='json' classNames='flex w-full text-xs opacity-75 rounded'>
25
+ <SyntaxHighlighter language='json' classNames='text-xs opacity-75 rounded'>
26
26
  {JSON.stringify(object, undefined, 2)}
27
27
  </SyntaxHighlighter>
28
28
  );
@@ -6,12 +6,13 @@ import React, { useCallback } from 'react';
6
6
 
7
7
  import { Button } from '@dxos/react-ui';
8
8
 
9
- import { createGeneratorIntent, createPluginId, Number } from './generator';
10
9
  import { Capabilities } from '../../common';
11
10
  import { contributes } from '../../core';
12
11
  import { createIntent } from '../../plugin-intent';
13
12
  import { useCapabilities, useIntentDispatcher, usePluginManager } from '../../react';
14
13
 
14
+ import { Number, createGeneratorIntent, createPluginId } from './generator';
15
+
15
16
  export const Toolbar = () => {
16
17
  const manager = usePluginManager();
17
18
  const { dispatchPromise: dispatch } = useIntentDispatcher();
@@ -5,8 +5,8 @@
5
5
  import { Schema } from 'effect';
6
6
 
7
7
  import { Capabilities, Events } from '../../common';
8
- import { contributes, defineEvent, defineCapability, defineModule, definePlugin } from '../../core';
9
- import { createResolver, type IntentSchema } from '../../plugin-intent';
8
+ import { contributes, defineCapability, defineEvent, defineModule, definePlugin } from '../../core';
9
+ import { type IntentSchema, createResolver } from '../../plugin-intent';
10
10
 
11
11
  export const Number = defineCapability<number>('dxos.org/test/generator/number');
12
12
 
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Events } from '../../common';
6
- import { definePlugin, lazy, defineModule } from '../../core';
6
+ import { defineModule, definePlugin, lazy } from '../../core';
7
7
 
8
8
  const Layout = lazy(() => import('./Layout'));
9
9
 
@@ -6,12 +6,13 @@ import React, { useCallback } from 'react';
6
6
 
7
7
  import { Button } from '@dxos/react-ui';
8
8
 
9
- import { Log } from './schema';
10
9
  import { Capabilities, createSurface } from '../../common';
11
10
  import { contributes } from '../../core';
12
11
  import { createIntent } from '../../plugin-intent';
13
12
  import { useIntentDispatcher } from '../../react';
14
13
 
14
+ import { Log } from './schema';
15
+
15
16
  export const Logger = () => {
16
17
  const { dispatchPromise } = useIntentDispatcher();
17
18
  const handleClick = useCallback(() => dispatchPromise(createIntent(Log, { message: 'Hello, world!' })), []);
@@ -4,11 +4,12 @@
4
4
 
5
5
  import { log } from '@dxos/log';
6
6
 
7
- import { Log } from './schema';
8
7
  import { Capabilities, Events } from '../../common';
9
- import { contributes, defineModule, lazy, definePlugin } from '../../core';
8
+ import { contributes, defineModule, definePlugin, lazy } from '../../core';
10
9
  import { createResolver } from '../../plugin-intent';
11
10
 
11
+ import { Log } from './schema';
12
+
12
13
  const Toolbar = lazy(() => import('./Toolbar'));
13
14
 
14
15
  export const LoggerPlugin = () =>
@@ -4,16 +4,18 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
8
  import React from 'react';
8
9
 
9
10
  import { withLayout, withTheme } from '@dxos/storybook-utils';
10
11
 
12
+ import { useApp } from '../components';
13
+ import { IntentPlugin } from '../plugin-intent';
14
+
11
15
  import { DebugPlugin } from './debug';
12
- import { createNumberPlugin, GeneratorPlugin } from './generator';
16
+ import { GeneratorPlugin, createNumberPlugin } from './generator';
13
17
  import { LayoutPlugin } from './layout';
14
18
  import { LoggerPlugin } from './logger';
15
- import { useApp } from '../App';
16
- import { IntentPlugin } from '../plugin-intent';
17
19
 
18
20
  const plugins = [IntentPlugin(), LayoutPlugin(), DebugPlugin(), LoggerPlugin(), GeneratorPlugin()];
19
21
 
@@ -21,22 +23,25 @@ const Placeholder = () => {
21
23
  return <div>Loading...</div>;
22
24
  };
23
25
 
24
- const Story = () => {
26
+ const DefaultStory = () => {
25
27
  const App = useApp({
26
28
  pluginLoader: (id) => createNumberPlugin(id),
27
29
  plugins,
28
30
  core: plugins.map((plugin) => plugin.meta.id),
29
- // Having a non-empty placeholder makes it clear if it's taking a while to load.
30
31
  placeholder: Placeholder,
31
32
  });
32
33
 
33
34
  return <App />;
34
35
  };
35
36
 
36
- export const Playground = {};
37
-
38
- export default {
37
+ const meta = {
39
38
  title: 'sdk/app-framework/playground',
40
- render: Story,
39
+ render: DefaultStory,
41
40
  decorators: [withTheme, withLayout()],
42
- };
41
+ } satisfies Meta<typeof DefaultStory>;
42
+
43
+ export default meta;
44
+
45
+ type Story = StoryObj<typeof meta>;
46
+
47
+ export const Playground: Story = {};
@@ -2,10 +2,11 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { INTENT_PLUGIN } from './actions';
6
5
  import { Events } from '../common';
7
6
  import { defineModule, definePlugin, lazy } from '../core';
8
7
 
8
+ import { INTENT_PLUGIN } from './actions';
9
+
9
10
  export const IntentPlugin = () =>
10
11
  definePlugin({ id: INTENT_PLUGIN, name: 'Intent' }, [
11
12
  defineModule({
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './actions';
6
+ export * from './errors';
6
7
  export * from './intent';
7
8
  export * from './intent-dispatcher';
8
9
  export * from './IntentPlugin';
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema, Effect, Fiber, pipe } from 'effect';
5
+ import { Effect, Fiber, Schema, pipe } from 'effect';
6
6
  import { describe, expect, test } from 'vitest';
7
7
 
8
8
  import { chain, createIntent } from './intent';