@dxos/app-framework 0.13.0 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/app-framework",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "A framework for building applications from composible plugins.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -171,27 +171,27 @@
171
171
  "esbuild": "0.28.2",
172
172
  "idb-keyval": "^6.2.1",
173
173
  "solid-js": "^1.9.11",
174
- "@dxos/async": "0.13.0",
175
- "@dxos/compute": "0.13.0",
176
- "@dxos/context": "0.13.0",
177
- "@dxos/compute-runtime": "0.13.0",
178
- "@dxos/debug": "0.13.0",
179
- "@dxos/edge-client": "0.13.0",
180
- "@dxos/echo": "0.13.0",
181
- "@dxos/effect": "0.13.0",
182
- "@dxos/errors": "0.13.0",
183
- "@dxos/graph": "0.13.0",
184
- "@dxos/invariant": "0.13.0",
185
- "@dxos/keys": "0.13.0",
186
- "@dxos/log": "0.13.0",
187
- "@dxos/operation": "0.13.0",
188
- "@dxos/protocols": "0.13.0",
189
- "@dxos/react-error-boundary": "0.13.0",
190
- "@dxos/react-hooks": "0.13.0",
191
- "@dxos/ui-theme": "0.13.0",
192
- "@dxos/util": "0.13.0",
193
- "@dxos/web-context": "0.13.0",
194
- "@dxos/web-context-react": "0.13.0"
174
+ "@dxos/async": "0.14.0",
175
+ "@dxos/compute": "0.14.0",
176
+ "@dxos/compute-runtime": "0.14.0",
177
+ "@dxos/context": "0.14.0",
178
+ "@dxos/debug": "0.14.0",
179
+ "@dxos/echo": "0.14.0",
180
+ "@dxos/edge-client": "0.14.0",
181
+ "@dxos/effect": "0.14.0",
182
+ "@dxos/errors": "0.14.0",
183
+ "@dxos/graph": "0.14.0",
184
+ "@dxos/invariant": "0.14.0",
185
+ "@dxos/keys": "0.14.0",
186
+ "@dxos/log": "0.14.0",
187
+ "@dxos/operation": "0.14.0",
188
+ "@dxos/protocols": "0.14.0",
189
+ "@dxos/react-error-boundary": "0.14.0",
190
+ "@dxos/react-hooks": "0.14.0",
191
+ "@dxos/ui-theme": "0.14.0",
192
+ "@dxos/util": "0.14.0",
193
+ "@dxos/web-context": "0.14.0",
194
+ "@dxos/web-context-react": "0.14.0"
195
195
  },
196
196
  "devDependencies": {
197
197
  "@effect/atom-react": "4.0.0",
@@ -206,10 +206,10 @@
206
206
  "storybook-solidjs-vite": "^10.7.1",
207
207
  "typedoc": "0.28.1",
208
208
  "vite": "^8.2.2",
209
- "@dxos/brand": "0.13.0",
210
- "@dxos/random": "0.13.0",
211
- "@dxos/react-ui": "0.13.0",
212
- "@dxos/react-ui-list": "0.13.0"
209
+ "@dxos/brand": "0.14.0",
210
+ "@dxos/random": "0.14.0",
211
+ "@dxos/react-ui": "0.14.0",
212
+ "@dxos/react-ui-list": "0.14.0"
213
213
  },
214
214
  "peerDependencies": {
215
215
  "@effect/atom-react": "4.0.0",
@@ -195,7 +195,6 @@ export type ProcessManagerRuntimeServices =
195
195
  | Plugin$.Service
196
196
  | ProcessManager$.ProcessManagerService
197
197
  | Operation.Service
198
- | ProcessManager$.ProcessOperationInvoker.Service
199
198
  | ComputeServiceResolver.ServiceResolver;
200
199
 
201
200
  /**
@@ -264,7 +263,7 @@ export const UndoMapping = Capability$.make<UndoMapping[]>()('org.dxos.app-frame
264
263
 
265
264
  /**
266
265
  * Operation invoker backed by the process manager. Spawns a process per
267
- * operation invocation; see {@link ProcessManager$.ProcessOperationInvoker}.
266
+ * operation invocation; see `ProcessOperationInvoker` in `@dxos/compute-runtime`.
268
267
  */
269
268
  export type OperationInvoker = OperationInvoker$.OperationInvoker;
270
269
 
@@ -3,15 +3,20 @@
3
3
  //
4
4
 
5
5
  import { describe, expect, it, test } from '@effect/vitest';
6
+ import * as Cause from 'effect/Cause';
6
7
  import * as Context from 'effect/Context';
7
8
  import * as Effect from 'effect/Effect';
9
+ import * as Exit from 'effect/Exit';
8
10
  import * as Layer from 'effect/Layer';
11
+ import * as Registry from 'effect/reactivity/AtomRegistry';
9
12
 
13
+ import { RemoteProcessManager } from '@dxos/compute-runtime';
10
14
  import * as LayerSpec from '@dxos/compute/LayerSpec';
15
+ import * as Process from '@dxos/compute/Process';
11
16
  import * as ServiceResolver from '@dxos/compute/ServiceResolver';
12
17
  import * as Trace from '@dxos/compute/Trace';
13
18
  import { Obj } from '@dxos/echo';
14
- import { DXN } from '@dxos/keys';
19
+ import { DXN, SpaceId } from '@dxos/keys';
15
20
  import { type LogConfig, type LogEntry, LogLevel, log } from '@dxos/log';
16
21
 
17
22
  import { ActivationEvents, Capabilities } from '../common/index.ts';
@@ -281,3 +286,75 @@ describe('dynamic trace sink', () => {
281
286
  expect(reached).toEqual(['second']);
282
287
  });
283
288
  });
289
+
290
+ describe('one process manager', () => {
291
+ const remoteMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.test.remoteRuntime'), name: 'Remote runtime' });
292
+ const edge = { location: { kind: 'edge', space: SpaceId.random() } } as const;
293
+
294
+ /** A plugin contributing a remote manager whose `list` records the spaces it was asked about, as EDGE's does. */
295
+ const RemoteRuntime = (listed: string[]) =>
296
+ Plugin.make(
297
+ Plugin.define(remoteMeta).pipe(
298
+ Plugin.addModule({
299
+ id: 'remote-process-manager',
300
+ activatesOn: ActivationEvents.Startup,
301
+ provides: [Capabilities.LayerSpec],
302
+ activate: () =>
303
+ Effect.succeed([
304
+ Capability.contribute(
305
+ Capabilities.LayerSpec,
306
+ LayerSpec.make(
307
+ {
308
+ affinity: 'application',
309
+ requires: [Registry.AtomRegistry],
310
+ provides: [RemoteProcessManager.Service],
311
+ },
312
+ () =>
313
+ Layer.effect(
314
+ RemoteProcessManager.Service,
315
+ Effect.gen(function* () {
316
+ const noop = yield* RemoteProcessManager.Service;
317
+ return {
318
+ ...noop,
319
+ list: ({ spaceId }: RemoteProcessManager.ListOptions) =>
320
+ Effect.sync(() => {
321
+ listed.push(spaceId);
322
+ return [];
323
+ }),
324
+ };
325
+ }),
326
+ ).pipe(Layer.provide(RemoteProcessManager.layerNoop)),
327
+ ),
328
+ ),
329
+ ]),
330
+ }),
331
+ ),
332
+ );
333
+
334
+ it.effect("sends the app's edge control to the remote manager a plugin contributes", () =>
335
+ Effect.gen(function* () {
336
+ const listed: string[] = [];
337
+ const manager = makeManager({ plugins: [RemoteRuntime(listed)()], enabled: [remoteMeta.profile.key] });
338
+ yield* manager.activate(ActivationEvents.Startup);
339
+
340
+ // The app's manager and the stack's (the one `AgentService` resolves) are the same one.
341
+ const appManager = yield* manager.capabilities.waitFor(Capabilities.ProcessManager);
342
+ const stackManager = yield* resolveWith(manager, Process.ManagerService);
343
+ yield* appManager.handles(edge);
344
+ yield* stackManager.handles(edge);
345
+ expect(listed).toEqual([edge.location.space, edge.location.space]);
346
+ }),
347
+ );
348
+
349
+ it.effect('falls back to no remote control without such a plugin, and keeps local control', () =>
350
+ Effect.gen(function* () {
351
+ const manager = makeManager({ plugins: [], enabled: [] });
352
+ yield* manager.activate(ActivationEvents.Startup);
353
+
354
+ const appManager = yield* manager.capabilities.waitFor(Capabilities.ProcessManager);
355
+ expect(yield* appManager.handles()).toEqual([]);
356
+ const remote = yield* Effect.exit(appManager.handles(edge));
357
+ expect(Exit.isFailure(remote) && String(Cause.squash(remote.cause))).toContain('offers no process control');
358
+ }),
359
+ );
360
+ });
@@ -2,15 +2,21 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
+ import * as Context from 'effect/Context';
5
6
  import * as Effect from 'effect/Effect';
7
+ import * as Exit from 'effect/Exit';
6
8
  import * as Layer from 'effect/Layer';
7
9
  import * as ManagedRuntime from 'effect/ManagedRuntime';
10
+ import * as Atom from 'effect/reactivity/Atom';
8
11
  import * as Registry from 'effect/reactivity/AtomRegistry';
12
+ import * as Scope from 'effect/Scope';
13
+ import * as Stream from 'effect/Stream';
9
14
  import * as Tracer from 'effect/Tracer';
10
15
 
11
16
  import {
12
17
  LayerStack,
13
18
  ProcessManager,
19
+ ProcessOperationInvoker,
14
20
  RemoteProcessManager,
15
21
  RemoteTraceMonitor,
16
22
  UnifiedProcessManager,
@@ -87,6 +93,62 @@ export const makeDynamicTraceSink = (
87
93
  return { write: (message) => Trace.mergeSinks(resolve()).write(message) };
88
94
  };
89
95
 
96
+ /**
97
+ * The application's one {@link Process.ManagerService}, as built by the stack. Every built-in and plugin
98
+ * consumer sees the same manager, so a host's remote manager (e.g. EDGE) reaches the app's invoker too.
99
+ */
100
+ const ProcessManagerSpec = LayerSpec.make(
101
+ {
102
+ affinity: 'application',
103
+ requires: [
104
+ ProcessManager.ProcessManagerService,
105
+ RemoteProcessManager.Service,
106
+ RemoteTraceMonitor.Service,
107
+ Registry.AtomRegistry,
108
+ ],
109
+ provides: [Process.ManagerService],
110
+ },
111
+ () => UnifiedProcessManager.layer,
112
+ );
113
+
114
+ /**
115
+ * A {@link Process.Manager} that is the stack's {@link Process.ManagerService}, resolved on first use: building it
116
+ * resolves the host's remote manager, which may need services (the client) that are not ready at activation.
117
+ * The process tree reads empty until then.
118
+ */
119
+ export const fromStack = (
120
+ resolver: ServiceResolver.ServiceResolver,
121
+ scope: Scope.Scope,
122
+ registry: Registry.AtomRegistry,
123
+ ): Process.Manager => {
124
+ const resolvedAtom = Atom.make<Process.Manager | undefined>(undefined).pipe(Atom.keepAlive);
125
+ const manager: Effect.Effect<Process.Manager> = Effect.suspend(() => {
126
+ const resolved = registry.get(resolvedAtom);
127
+ return resolved !== undefined
128
+ ? Effect.succeed(resolved)
129
+ : resolver.resolve(Process.ManagerService, {}).pipe(
130
+ Scope.provide(scope),
131
+ Effect.tap((stackManager) => Effect.sync(() => registry.set(resolvedAtom, stackManager))),
132
+ // The framework's own spec provides it, so a failure here is a broken stack, not a missing plugin.
133
+ Effect.orDie,
134
+ );
135
+ });
136
+
137
+ return {
138
+ processTree: Effect.flatMap(manager, (stackManager) => stackManager.processTree),
139
+ processTreeAtom: Atom.make((get) => {
140
+ const stackManager = get(resolvedAtom);
141
+ return stackManager === undefined ? [] : get(stackManager.processTreeAtom);
142
+ }),
143
+ list: (filter) => Effect.flatMap(manager, (stackManager) => stackManager.list(filter)),
144
+ subscribeToTraceMessages: (filter) =>
145
+ Stream.unwrap(Effect.map(manager, (stackManager) => stackManager.subscribeToTraceMessages(filter))),
146
+ spawn: (definition, options) => Effect.flatMap(manager, (stackManager) => stackManager.spawn(definition, options)),
147
+ handles: (options) => Effect.flatMap(manager, (stackManager) => stackManager.handles(options)),
148
+ attach: (pid, options) => Effect.flatMap(manager, (stackManager) => stackManager.attach(pid, options)),
149
+ };
150
+ };
151
+
90
152
  export default Capability.makeModule(
91
153
  Effect.fnUntraced(function* () {
92
154
  const capabilityManager = yield* Capability.Service;
@@ -143,7 +205,32 @@ export default Capability.makeModule(
143
205
  ),
144
206
  );
145
207
 
146
- const layerStack = new LayerStack.LayerStack({ layers: [ambientLayerSpec, ...layerSpecs] });
208
+ // Fallbacks for a host with no remote runtime; a plugin spec providing either tag (e.g. EDGE's) wins.
209
+ const fallbacks = Effect.runSync(
210
+ Effect.gen(function* () {
211
+ const remoteProcessManager = yield* RemoteProcessManager.Service;
212
+ const remoteTraceMonitor = yield* RemoteTraceMonitor.Service;
213
+ return Context.make(RemoteProcessManager.Service, remoteProcessManager).pipe(
214
+ Context.add(RemoteTraceMonitor.Service, remoteTraceMonitor),
215
+ );
216
+ }).pipe(
217
+ Effect.provide(
218
+ Layer.mergeAll(
219
+ RemoteProcessManager.layerNoop,
220
+ // Remote ephemeral trace (DX-1125): the first contributed swarm-backed monitor, else no-op.
221
+ remoteTraceMonitors.length > 0
222
+ ? Layer.succeed(RemoteTraceMonitor.Service, remoteTraceMonitors[0])
223
+ : RemoteTraceMonitor.layerNoop,
224
+ ),
225
+ ),
226
+ Effect.provideService(Registry.AtomRegistry, atomRegistry),
227
+ ),
228
+ );
229
+
230
+ const layerStack = new LayerStack.LayerStack({
231
+ layers: [ambientLayerSpec, ProcessManagerSpec, ...layerSpecs],
232
+ services: fallbacks,
233
+ });
147
234
  const serviceResolver = layerStack.getServiceResolver();
148
235
 
149
236
  // The stack extends built slices in place, so admitting a late spec costs no live service.
@@ -195,21 +282,14 @@ export default Capability.makeModule(
195
282
  const processManagerLayer = ProcessManager.layer({ runtimeName: Trace.CommonRuntimeName.local }).pipe(
196
283
  Layer.provide(baseLayer),
197
284
  );
198
- const operationInvokerLayer = ProcessManager.ProcessOperationInvoker.layer.pipe(
285
+ // Holds the stack's application slice that the manager resolves from, for the module's lifetime.
286
+ const stackScope = yield* Scope.make();
287
+ yield* Effect.addFinalizer(() => Scope.close(stackScope, Exit.void));
288
+ const unifiedProcessManager = fromStack(serviceResolver, stackScope, atomRegistry);
289
+ const unifiedProcessManagerLayer = Layer.succeed(Process.ManagerService, unifiedProcessManager);
290
+ const operationInvokerLayer = ProcessOperationInvoker.layer.pipe(
199
291
  // Operations invoked through the app's own invoker are the person's actions, from a menu, dialog or shortcut.
200
- Layer.provide(Layer.mergeAll(processManagerLayer, baseLayer, Layer.succeed(Database.Origin, 'user'))),
201
- );
202
-
203
- // App-framework has no EDGE runtime, so the remote process view is empty;
204
- // the aggregate manager therefore equals the local process tree.
205
- const remoteProcessManagerLayer = RemoteProcessManager.layerNoop.pipe(Layer.provide(baseLayer));
206
- // Remote ephemeral trace (DX-1125): use the first contributed swarm-backed monitor, else no-op.
207
- const remoteTraceMonitorLayer =
208
- remoteTraceMonitors.length > 0
209
- ? Layer.succeed(RemoteTraceMonitor.Service, remoteTraceMonitors[0])
210
- : RemoteTraceMonitor.layerNoop;
211
- const unifiedProcessManagerLayer = UnifiedProcessManager.layer.pipe(
212
- Layer.provide(Layer.mergeAll(processManagerLayer, remoteProcessManagerLayer, remoteTraceMonitorLayer, baseLayer)),
292
+ Layer.provide(Layer.mergeAll(unifiedProcessManagerLayer, baseLayer, Layer.succeed(Database.Origin, 'user'))),
213
293
  );
214
294
 
215
295
  const runtimeLayer = Layer.mergeAll(
@@ -235,12 +315,6 @@ export default Capability.makeModule(
235
315
  runSync: (effect) => managedRuntime.runSync(effect as Effect.Effect<any, any, any>),
236
316
  };
237
317
 
238
- // Eagerly extract the process manager. Safe because it does not require a
239
- // fresh scope and is a stable reference for the lifetime of the runtime.
240
- const unifiedProcessManager = managedRuntime.runSync(
241
- Effect.flatMap(Process.ManagerService, Effect.succeed) as Effect.Effect<Process.Manager, never, never>,
242
- );
243
-
244
318
  // Publish the manager into the ambient-layer holder so that
245
319
  // `ProcessManager.ProcessManagerService` becomes resolvable through the
246
320
  // LayerStack alongside the other framework-supplied services.
@@ -252,13 +326,16 @@ export default Capability.makeModule(
252
326
  >,
253
327
  );
254
328
 
255
- // Eagerly extract the operation invoker built by ProcessOperationInvoker.layer.
256
- // Pulled via the ProcessOperationInvoker tag so the contributed value carries
257
- // the full OperationInvoker interface (`invocations`, `pendingFollowups`,
329
+ // Resolved in the background rather than on first spawn, so a view that only reads the tree still fills in;
330
+ // after the holder above, which the stack's manager is built over.
331
+ managedRuntime.runFork(unifiedProcessManager.processTree);
332
+
333
+ // Eagerly extract the operation invoker built by ProcessOperationInvoker.layer, via its own tag so the
334
+ // contributed value carries the full OperationInvoker interface (`invocations`, `pendingFollowups`,
258
335
  // `awaitFollowups`, `_invokeCore`) that HistoryTracker requires.
259
336
  const operationInvoker: OperationInvoker.OperationInvoker = managedRuntime.runSync(
260
- Effect.flatMap(ProcessManager.ProcessOperationInvoker.Service, Effect.succeed) as unknown as Effect.Effect<
261
- OperationInvoker.OperationInvoker,
337
+ Effect.flatMap(ProcessOperationInvoker.Service, Effect.succeed) as Effect.Effect<
338
+ ProcessOperationInvoker.ProcessOperationInvoker,
262
339
  never,
263
340
  never
264
341
  >,
@@ -133,7 +133,6 @@ export const DEFAULT_PACKAGES = [
133
133
  '@dxos/react-ui-calendar',
134
134
  '@dxos/react-ui-canvas',
135
135
  '@dxos/react-ui-canvas-compute',
136
- '@dxos/react-ui-canvas-editor',
137
136
  '@dxos/react-ui-chat',
138
137
  '@dxos/react-ui-components',
139
138
  '@dxos/react-ui-dnd',