@dxos/edge-compute 0.0.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.
Files changed (39) hide show
  1. package/dist/lib/bundler.mjs +195 -0
  2. package/dist/lib/bundler.mjs.map +1 -0
  3. package/dist/lib/chunk-http-plugin-esbuild.mjs +52 -0
  4. package/dist/lib/chunk-http-plugin-esbuild.mjs.map +1 -0
  5. package/dist/lib/index.mjs +444 -0
  6. package/dist/lib/index.mjs.map +1 -0
  7. package/dist/lib/native.mjs +148 -0
  8. package/dist/lib/native.mjs.map +1 -0
  9. package/dist/lib/testing.mjs +40 -0
  10. package/dist/lib/testing.mjs.map +1 -0
  11. package/dist/types/src/EdgeProcessManager.d.ts +10 -4
  12. package/dist/types/src/EdgeProcessManager.d.ts.map +1 -1
  13. package/dist/types/src/EdgeProcessManager.test.d.ts +2 -0
  14. package/dist/types/src/EdgeProcessManager.test.d.ts.map +1 -0
  15. package/dist/types/src/EdgeTriggerManager.d.ts +11 -3
  16. package/dist/types/src/EdgeTriggerManager.d.ts.map +1 -1
  17. package/dist/types/src/native/bundler.d.ts.map +1 -1
  18. package/dist/types/src/testing/services.d.ts +7 -4
  19. package/dist/types/src/testing/services.d.ts.map +1 -1
  20. package/dist/types/tsconfig.tsbuildinfo +1 -1
  21. package/package.json +22 -22
  22. package/src/EdgeProcessManager.test.ts +59 -0
  23. package/src/EdgeProcessManager.ts +65 -16
  24. package/src/EdgeTriggerManager.ts +105 -31
  25. package/src/native/bundler.ts +8 -1
  26. package/src/testing/services.ts +22 -16
  27. package/dist/lib/neutral/bundler/index.mjs +0 -213
  28. package/dist/lib/neutral/bundler/index.mjs.map +0 -7
  29. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  30. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  31. package/dist/lib/neutral/chunk-RN6KVGN4.mjs +0 -63
  32. package/dist/lib/neutral/chunk-RN6KVGN4.mjs.map +0 -7
  33. package/dist/lib/neutral/index.mjs +0 -343
  34. package/dist/lib/neutral/index.mjs.map +0 -7
  35. package/dist/lib/neutral/meta.json +0 -1
  36. package/dist/lib/neutral/native/index.mjs +0 -186
  37. package/dist/lib/neutral/native/index.mjs.map +0 -7
  38. package/dist/lib/neutral/testing/index.mjs +0 -41
  39. package/dist/lib/neutral/testing/index.mjs.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/edge-compute",
3
- "version": "0.0.0",
3
+ "version": "0.11.0",
4
4
  "description": "EDGE compute: function deploy, scripts, and Edge implementations of Remote* runtime interfaces.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -16,22 +16,22 @@
16
16
  ".": {
17
17
  "source": "./src/index.ts",
18
18
  "types": "./dist/types/src/index.d.ts",
19
- "default": "./dist/lib/neutral/index.mjs"
19
+ "import": "./dist/lib/index.mjs"
20
20
  },
21
21
  "./testing": {
22
22
  "source": "./src/testing/index.ts",
23
23
  "types": "./dist/types/src/testing/index.d.ts",
24
- "default": "./dist/lib/neutral/testing/index.mjs"
24
+ "import": "./dist/lib/testing.mjs"
25
25
  },
26
26
  "./bundler": {
27
27
  "source": "./src/bundler/index.ts",
28
28
  "types": "./dist/types/src/bundler/index.d.ts",
29
- "default": "./dist/lib/neutral/bundler/index.mjs"
29
+ "import": "./dist/lib/bundler.mjs"
30
30
  },
31
31
  "./native": {
32
32
  "source": "./src/native/index.ts",
33
33
  "types": "./dist/types/src/native/index.d.ts",
34
- "default": "./dist/lib/neutral/native/index.mjs"
34
+ "import": "./dist/lib/native.mjs"
35
35
  }
36
36
  },
37
37
  "types": "dist/types/src/index.d.ts",
@@ -60,23 +60,23 @@
60
60
  "esbuild": "0.28.1",
61
61
  "esbuild-wasm": "^0.28.0",
62
62
  "iso-did": "^1.6.0",
63
- "@dxos/client": "0.10.0",
64
- "@dxos/compute-runtime": "0.10.0",
65
- "@dxos/echo": "0.10.0",
66
- "@dxos/context": "0.10.0",
67
- "@dxos/compute": "0.10.0",
68
- "@dxos/crypto": "0.10.0",
69
- "@dxos/echo-client": "0.10.0",
70
- "@dxos/edge-client": "0.10.0",
71
- "@dxos/effect": "0.10.0",
72
- "@dxos/errors": "0.10.0",
73
- "@dxos/functions-runtime-cloudflare": "0.10.0",
74
- "@dxos/invariant": "0.10.0",
75
- "@dxos/keys": "0.10.0",
76
- "@dxos/node-std": "0.10.0",
77
- "@dxos/util": "0.10.0",
78
- "@dxos/protocols": "0.10.0",
79
- "@dxos/log": "0.10.0"
63
+ "@dxos/client": "0.11.0",
64
+ "@dxos/compute": "0.11.0",
65
+ "@dxos/compute-runtime": "0.11.0",
66
+ "@dxos/context": "0.11.0",
67
+ "@dxos/crypto": "0.11.0",
68
+ "@dxos/echo": "0.11.0",
69
+ "@dxos/echo-client": "0.11.0",
70
+ "@dxos/edge-client": "0.11.0",
71
+ "@dxos/effect": "0.11.0",
72
+ "@dxos/errors": "0.11.0",
73
+ "@dxos/functions-runtime-cloudflare": "0.11.0",
74
+ "@dxos/invariant": "0.11.0",
75
+ "@dxos/keys": "0.11.0",
76
+ "@dxos/log": "0.11.0",
77
+ "@dxos/node-std": "0.11.0",
78
+ "@dxos/protocols": "0.11.0",
79
+ "@dxos/util": "0.11.0"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@effect/vitest": "0.29.0",
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Registry } from '@effect-atom/atom';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Layer from 'effect/Layer';
8
+ import { describe, expect, test, vi } from 'vitest';
9
+
10
+ import { RemoteProcessManager } from '@dxos/compute-runtime';
11
+ import { EdgeHttpClient } from '@dxos/edge-client';
12
+ import { EffectEx } from '@dxos/effect';
13
+ import { invariant } from '@dxos/invariant';
14
+ import { SpaceId } from '@dxos/keys';
15
+
16
+ import * as EdgeProcessManager from './EdgeProcessManager';
17
+
18
+ describe('EdgeProcessManager', () => {
19
+ test('cancel force-cancels the trigger run on the edge client', async () => {
20
+ const edgeClient = new EdgeHttpClient('https://edge.example.com');
21
+ const cancelSpy = vi.spyOn(edgeClient, 'cancelTriggerRun').mockResolvedValue(undefined);
22
+ const space = SpaceId.random();
23
+ const trigger = 'TRIGGER1';
24
+
25
+ const program = Effect.gen(function* () {
26
+ const manager = yield* RemoteProcessManager.Service;
27
+ invariant(manager.cancel, 'edge process manager exposes cancel');
28
+ yield* manager.cancel({ space, trigger });
29
+ });
30
+
31
+ await EffectEx.runPromise(
32
+ program.pipe(
33
+ Effect.provide(EdgeProcessManager.fromEdgeClient(edgeClient)),
34
+ Effect.provide(Layer.succeed(Registry.AtomRegistry, Registry.make())),
35
+ ),
36
+ );
37
+
38
+ expect(cancelSpy).toHaveBeenCalledTimes(1);
39
+ expect(cancelSpy.mock.calls[0]?.[1]).toBe(space);
40
+ expect(cancelSpy.mock.calls[0]?.[2]).toBe(trigger);
41
+ });
42
+
43
+ test('layer without a client omits cancel and yields an empty tree', async () => {
44
+ const program = Effect.gen(function* () {
45
+ const manager = yield* RemoteProcessManager.Service;
46
+ return { hasCancel: manager.cancel !== undefined, tree: yield* manager.processTree };
47
+ });
48
+
49
+ const result = await EffectEx.runPromise(
50
+ program.pipe(
51
+ Effect.provide(EdgeProcessManager.layer),
52
+ Effect.provide(Layer.succeed(Registry.AtomRegistry, Registry.make())),
53
+ ),
54
+ );
55
+
56
+ expect(result.hasCancel).toBe(false);
57
+ expect(result.tree).toEqual([]);
58
+ });
59
+ });
@@ -11,37 +11,86 @@ import * as Layer from 'effect/Layer';
11
11
  import { type Client } from '@dxos/client';
12
12
  import { type Process } from '@dxos/compute';
13
13
  import { RemoteProcessManager } from '@dxos/compute-runtime';
14
+ import { Context as DxosContext } from '@dxos/context';
15
+ import { type EdgeHttpClient } from '@dxos/edge-client';
16
+ import { SpaceId } from '@dxos/keys';
17
+ import { log } from '@dxos/log';
18
+
19
+ import { createEdgeClient } from './edge-client';
14
20
 
15
21
  /**
16
22
  * EDGE implementation of {@link RemoteProcessManager.Service}.
17
23
  *
18
- * The EDGE worker exposes no process-tree endpoint yet (see Decision D3), so
19
- * this currently returns an empty tree. Once an endpoint lands, poll it into
20
- * `processTreeAtom` here.
24
+ * The read view (`processTree`) is still empty — the EDGE worker exposes no
25
+ * process-tree endpoint yet (see Decision D3). What this adds is the `cancel`
26
+ * control: it force-cancels the current run of an edge trigger (its in-flight
27
+ * execution and `runAgain` continuation chain) via
28
+ * {@link EdgeHttpClient.cancelTriggerRun}, addressed by `trigger` in `space`.
29
+ * A manager built without a client (the {@link layer} stub) omits `cancel`.
21
30
  */
22
- const make = (): Layer.Layer<RemoteProcessManager.Service, never, Registry.AtomRegistry> =>
31
+ const makeManager = (
32
+ registry: Registry.Registry,
33
+ getEdgeClient?: () => EdgeHttpClient,
34
+ ): RemoteProcessManager.Manager => {
35
+ // TODO(edge): Populate from an EDGE process-tree endpoint once available.
36
+ const processTreeAtom = Atom.make<readonly Process.Info[]>([]);
37
+ registry.mount(processTreeAtom);
38
+ return {
39
+ processTree: Effect.sync(() => registry.get(processTreeAtom)),
40
+ processTreeAtom,
41
+ ...(getEdgeClient
42
+ ? {
43
+ cancel: ({ space, trigger }: RemoteProcessManager.CancelTarget) =>
44
+ Effect.gen(function* () {
45
+ // `space` arrives as an untyped string from trace meta; skip rather than throw when it is
46
+ // not a valid space id (best-effort — the meter has already cleared locally).
47
+ if (!SpaceId.isValid(space)) {
48
+ log.warn('remote trigger cancel skipped: invalid space id', { space });
49
+ return;
50
+ }
51
+ yield* Effect.tryPromise(() =>
52
+ getEdgeClient().cancelTriggerRun(DxosContext.default(), space, trigger),
53
+ ).pipe(
54
+ Effect.asVoid,
55
+ // A missing/unreachable endpoint (e.g. an older edge deploy) must not surface as a defect
56
+ // from this fire-and-forget cancel — log and move on.
57
+ Effect.catchAll((error) => Effect.sync(() => log.warn('remote trigger cancel failed', { error }))),
58
+ );
59
+ }),
60
+ }
61
+ : {}),
62
+ } satisfies RemoteProcessManager.Manager;
63
+ };
64
+
65
+ const make = (
66
+ getEdgeClient?: () => EdgeHttpClient,
67
+ ): Layer.Layer<RemoteProcessManager.Service, never, Registry.AtomRegistry> =>
23
68
  Layer.effect(
24
69
  RemoteProcessManager.Service,
25
70
  Effect.gen(function* () {
26
71
  const registry = yield* Registry.AtomRegistry;
27
- // TODO(edge): Populate from an EDGE process-tree endpoint once available.
28
- const processTreeAtom = Atom.make<readonly Process.Info[]>([]);
29
- registry.mount(processTreeAtom);
30
- return {
31
- processTree: Effect.sync(() => registry.get(processTreeAtom)),
32
- processTreeAtom,
33
- } satisfies RemoteProcessManager.Manager;
72
+ return makeManager(registry, getEdgeClient);
34
73
  }),
35
74
  );
36
75
 
37
76
  /**
38
- * Build from a `Client`. Currently ignores the client until an EDGE
39
- * process-tree endpoint exists.
77
+ * For tests: provide a pre-built edge client.
78
+ */
79
+ export const fromEdgeClient = (
80
+ edgeClient: EdgeHttpClient,
81
+ ): Layer.Layer<RemoteProcessManager.Service, never, Registry.AtomRegistry> => make(() => edgeClient);
82
+
83
+ /**
84
+ * Build from a `Client`, deferring edge-client creation until the first cancel
85
+ * (identity / edge config may be absent at boot).
40
86
  */
41
- export const fromClient = (_client: Client): Layer.Layer<RemoteProcessManager.Service, never, Registry.AtomRegistry> =>
42
- make();
87
+ export const fromClient = (client: Client): Layer.Layer<RemoteProcessManager.Service, never, Registry.AtomRegistry> => {
88
+ let cached: EdgeHttpClient | undefined;
89
+ return make(() => (cached ??= createEdgeClient(client)));
90
+ };
43
91
 
44
92
  /**
45
- * Empty EDGE process manager (no endpoint yet).
93
+ * EDGE process manager with no client — empty process tree and no cancel control.
94
+ * Used where edge is not configured.
46
95
  */
47
96
  export const layer: Layer.Layer<RemoteProcessManager.Service, never, Registry.AtomRegistry> = make();
@@ -5,66 +5,140 @@
5
5
  // @import-as-namespace
6
6
 
7
7
  import { Atom, Registry } from '@effect-atom/atom';
8
+ import * as Duration from 'effect/Duration';
8
9
  import * as Effect from 'effect/Effect';
10
+ import * as Exit from 'effect/Exit';
9
11
  import * as Layer from 'effect/Layer';
12
+ import * as Schedule from 'effect/Schedule';
13
+ import type * as Scope from 'effect/Scope';
10
14
 
11
15
  import { type Client, ClientService } from '@dxos/client';
12
16
  import { type Trigger } from '@dxos/compute';
13
17
  import { RemoteTriggerManager } from '@dxos/compute-runtime';
14
- import { type SpaceId } from '@dxos/keys';
18
+ import { Context as DxosContext } from '@dxos/context';
19
+ import { Ref } from '@dxos/echo';
20
+ import { type EdgeTriggerStatus } from '@dxos/edge-client';
21
+ import { EID, type SpaceId } from '@dxos/keys';
22
+
23
+ import { createEdgeClient } from './edge-client';
24
+
25
+ type EdgeClient = ReturnType<typeof createEdgeClient>;
26
+
27
+ /** How often the remote trigger view is refreshed from the EDGE dispatcher. */
28
+ const POLL_INTERVAL = Duration.seconds(15);
15
29
 
16
30
  /**
17
31
  * EDGE implementation of {@link RemoteTriggerManager.Service}.
18
32
  *
19
- * Edge triggers are ECHO objects replicated into the local database, so the
20
- * aggregate {@link TriggerMonitor} already surfaces them (marked
21
- * `environment: 'edge'`) from that database view; the `triggers` atom here is
22
- * therefore left empty to avoid double-counting. It exists as the seam through
23
- * which EDGE dispatcher runtime status can be layered in once a mapping to
24
- * {@link Trigger.State} is available.
33
+ * The `triggers` atom is populated by polling
34
+ * {@link EdgeHttpClient.getSpaceTriggers} and mapping each edge trigger's
35
+ * runtime status into a {@link Trigger.State} (marked `environment: 'edge'`).
36
+ * The referenced `Trigger` objects are replicated into the local database, so
37
+ * the trigger ref is a space-relative echo ref synthesized from the id. The
38
+ * aggregate {@link TriggerMonitor} dedupes these against the database-derived
39
+ * view (edge entries here supersede the bare database ones).
25
40
  *
26
- * `invokeTrigger` is not yet supported over the wire (edge triggers run
27
- * autonomously on the EDGE dispatcher).
41
+ * `invokeTrigger` force-runs the trigger's cron on the EDGE dispatcher via
42
+ * {@link EdgeHttpClient.forceRunCronTrigger}.
43
+ */
44
+ const toState = (status: EdgeTriggerStatus): Trigger.State => ({
45
+ // Space-relative echo ref (same shape as `Ref.make(trigger)` for an in-space object), resolvable
46
+ // against the space database where the edge trigger is replicated.
47
+ trigger: Ref.fromURI(EID.make({ entityId: status.triggerId })),
48
+ environment: 'edge',
49
+ nextExecution: status.nextExecutionTimestamp !== undefined ? new Date(status.nextExecutionTimestamp) : undefined,
50
+ cooldownUntil: status.cooldownUntilTimestamp !== undefined ? new Date(status.cooldownUntilTimestamp) : undefined,
51
+ lastResult: toExit(status.lastResult),
52
+ });
53
+
54
+ const toExit = (result: EdgeTriggerStatus['lastResult']): Exit.Exit<unknown> | null => {
55
+ if (!result) {
56
+ return null;
57
+ }
58
+ // The original error channel is not reconstructed over the wire, so failures are surfaced as
59
+ // defects carrying the serialized message.
60
+ return result.status === 'success'
61
+ ? Exit.void
62
+ : Exit.die(new Error(result.error?.message ?? 'Edge trigger invocation failed'));
63
+ };
64
+
65
+ const make = (
66
+ getEdgeClient: () => EdgeClient,
67
+ spaceId: SpaceId,
68
+ registry: Registry.Registry,
69
+ ): Effect.Effect<RemoteTriggerManager.Manager, never, Scope.Scope> =>
70
+ Effect.gen(function* () {
71
+ const triggers = Atom.make<readonly Trigger.State[]>([]);
72
+ registry.mount(triggers);
73
+
74
+ const refresh = Effect.tryPromise(() => getEdgeClient().getSpaceTriggers(DxosContext.default(), spaceId)).pipe(
75
+ Effect.tap((response) => Effect.sync(() => registry.update(triggers, () => response.triggers.map(toState)))),
76
+ // The endpoint may be unimplemented or unreachable; a failed poll must not tear down the layer.
77
+ Effect.catchAll(() => Effect.void),
78
+ );
79
+
80
+ // Poll the EDGE dispatcher on an interval; the fiber is scoped to the layer's lifetime.
81
+ yield* refresh.pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL)), Effect.forkScoped);
82
+
83
+ return {
84
+ triggers,
85
+ invokeTrigger: (options: Trigger.InvokeOptions) =>
86
+ // Manual invocation of a remote trigger maps onto force-running its cron on the EDGE
87
+ // dispatcher; refresh the view promptly afterwards.
88
+ Effect.promise(() =>
89
+ getEdgeClient().forceRunCronTrigger(DxosContext.default(), spaceId, options.trigger.id),
90
+ ).pipe(
91
+ Effect.asVoid,
92
+ Effect.orDie,
93
+ Effect.tap(() => refresh),
94
+ ),
95
+ } satisfies RemoteTriggerManager.Manager;
96
+ });
97
+
98
+ /**
99
+ * For tests: provide a pre-built edge client.
28
100
  */
29
- const make = (_spaceId?: SpaceId): Layer.Layer<RemoteTriggerManager.Service, never, Registry.AtomRegistry> =>
30
- Layer.effect(
101
+ export const fromEdgeClient = (
102
+ edgeClient: EdgeClient,
103
+ spaceId: SpaceId,
104
+ ): Layer.Layer<RemoteTriggerManager.Service, never, Registry.AtomRegistry> =>
105
+ Layer.scoped(
31
106
  RemoteTriggerManager.Service,
32
107
  Effect.gen(function* () {
33
108
  const registry = yield* Registry.AtomRegistry;
34
- // TODO(edge): Poll `EdgeHttpClient.getTriggersDispatcherStatus` and enrich edge trigger state here.
35
- const triggers = Atom.make<readonly Trigger.State[]>([]);
36
- registry.mount(triggers);
37
- return {
38
- triggers,
39
- invokeTrigger: () => Effect.die(new Error('Remote (EDGE) trigger invocation is not supported')),
40
- } satisfies RemoteTriggerManager.Manager;
109
+ return yield* make(() => edgeClient, spaceId, registry);
41
110
  }),
42
111
  );
43
112
 
44
113
  /**
45
- * Build from a `Client`.
114
+ * Build from a `Client`, deferring edge-client creation until first use
115
+ * (identity may be absent at boot).
46
116
  */
47
117
  export const fromClient = (
48
- _client: Client,
49
- spaceId?: SpaceId,
50
- ): Layer.Layer<RemoteTriggerManager.Service, never, Registry.AtomRegistry> => make(spaceId);
118
+ client: Client,
119
+ spaceId: SpaceId,
120
+ ): Layer.Layer<RemoteTriggerManager.Service, never, Registry.AtomRegistry> =>
121
+ Layer.scoped(
122
+ RemoteTriggerManager.Service,
123
+ Effect.gen(function* () {
124
+ const registry = yield* Registry.AtomRegistry;
125
+ let cached: EdgeClient | undefined;
126
+ return yield* make(() => (cached ??= createEdgeClient(client)), spaceId, registry);
127
+ }),
128
+ );
51
129
 
52
130
  /**
53
131
  * Build from the ambient `ClientService`.
54
132
  */
55
133
  export const layer = (
56
- spaceId?: SpaceId,
134
+ spaceId: SpaceId,
57
135
  ): Layer.Layer<RemoteTriggerManager.Service, never, Registry.AtomRegistry | ClientService> =>
58
- Layer.effect(
136
+ Layer.scoped(
59
137
  RemoteTriggerManager.Service,
60
138
  Effect.gen(function* () {
61
- yield* ClientService;
139
+ const client = yield* ClientService;
62
140
  const registry = yield* Registry.AtomRegistry;
63
- const triggers = Atom.make<readonly Trigger.State[]>([]);
64
- registry.mount(triggers);
65
- return {
66
- triggers,
67
- invokeTrigger: () => Effect.die(new Error('Remote (EDGE) trigger invocation is not supported')),
68
- } satisfies RemoteTriggerManager.Manager;
141
+ let cached: EdgeClient | undefined;
142
+ return yield* make(() => (cached ??= createEdgeClient(client)), spaceId, registry);
69
143
  }),
70
144
  );
@@ -13,6 +13,7 @@ import { statSync } from 'node:fs';
13
13
  import { writeFile } from 'node:fs/promises';
14
14
  import * as fs from 'node:fs/promises';
15
15
  import { basename, dirname, join, relative, resolve } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
16
17
 
17
18
  import { BaseError } from '@dxos/errors';
18
19
  import { PublicKey } from '@dxos/keys';
@@ -85,7 +86,13 @@ export const bundleFunction = async (options: BundleOptions): Promise<BundleResu
85
86
  },
86
87
  };
87
88
  `,
88
- resolveDir: new URL('.', import.meta.url).pathname,
89
+ // NOTE: Compute the module directory via `fileURLToPath`, not
90
+ // `new URL('.', import.meta.url)`. Vite's library-mode build treats the latter as an
91
+ // asset reference and base64-inlines the resolved `.ts` module as a
92
+ // `data:video/mp2t;base64,…` data URL, so `resolveDir` becomes garbage in `dist` and
93
+ // the bundler can no longer resolve `@dxos/compute-runtime` /
94
+ // `@dxos/functions-runtime-cloudflare`.
95
+ resolveDir: dirname(fileURLToPath(import.meta.url)),
89
96
  }));
90
97
  },
91
98
  },
@@ -3,11 +3,11 @@
3
3
  //
4
4
 
5
5
  import type * as Context from 'effect/Context';
6
+ import * as Layer from 'effect/Layer';
6
7
 
7
8
  import type { Space } from '@dxos/client/echo';
8
- import { type Credential, type Trace } from '@dxos/compute';
9
+ import { Credential, Trace } from '@dxos/compute';
9
10
  import { ConfiguredCredentialsService } from '@dxos/compute-runtime';
10
- import { ServiceContainer } from '@dxos/compute-runtime';
11
11
  import { Database } from '@dxos/echo';
12
12
  import { type EchoDatabase } from '@dxos/echo-client';
13
13
  import { assertArgument } from '@dxos/invariant';
@@ -63,23 +63,29 @@ export type TestServiceOptions = {
63
63
  };
64
64
 
65
65
  /**
66
- * @deprecated
66
+ * Composes the foundational Effect layers available to compute nodes and operations in tests:
67
+ * credentials, database (from a space or standalone database), and trace. Callers layer AI,
68
+ * `Operation.Service`, and registry on top as needed for the code under test.
67
69
  */
68
- export const createTestServices = ({
69
- ai,
70
- credentials,
71
- db,
72
- logging,
73
- space,
74
- }: TestServiceOptions = {}): ServiceContainer => {
70
+ export const createTestServices = ({ credentials, db, logging, space }: TestServiceOptions = {}): Layer.Layer<
71
+ Credential.CredentialsService | Database.Service | Trace.TraceService
72
+ > => {
75
73
  assertArgument(!(!!space && !!db), 'space', 'space can be provided only if db is not');
76
74
 
77
- return new ServiceContainer().setServices({
78
- // ai: createAiService(ai),
79
- credentials: createCredentialsService(credentials),
80
- database: space || db ? Database.makeService(space?.db || db!) : undefined,
81
- trace: logging?.trace ?? (logging?.enabled ? consoleTraceWriter : noopTraceWriter),
82
- });
75
+ const credentialsLayer = Layer.succeed(
76
+ Credential.CredentialsService,
77
+ createCredentialsService(credentials) ?? new ConfiguredCredentialsService(),
78
+ );
79
+
80
+ const database = space?.db ?? db;
81
+ const databaseLayer = database ? Database.layer(database) : Database.notAvailable;
82
+
83
+ const traceLayer = Layer.succeed(
84
+ Trace.TraceService,
85
+ logging?.trace ?? (logging?.enabled ? consoleTraceWriter : noopTraceWriter),
86
+ );
87
+
88
+ return Layer.mergeAll(credentialsLayer, databaseLayer, traceLayer);
83
89
  };
84
90
 
85
91
  const createCredentialsService = (