@dxos/client-protocol 0.9.1-staging.ee54ba693a → 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 (44) hide show
  1. package/dist/lib/index.mjs +590 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/lib/types.mjs +26 -0
  4. package/dist/lib/types.mjs.map +1 -0
  5. package/dist/types/src/Rpc.d.ts +41 -0
  6. package/dist/types/src/Rpc.d.ts.map +1 -0
  7. package/dist/types/src/bridge-rpc.d.ts +27 -0
  8. package/dist/types/src/bridge-rpc.d.ts.map +1 -0
  9. package/dist/types/src/config.d.ts +0 -1
  10. package/dist/types/src/config.d.ts.map +1 -1
  11. package/dist/types/src/index.d.ts +3 -1
  12. package/dist/types/src/index.d.ts.map +1 -1
  13. package/dist/types/src/service-rpc.d.ts +551 -0
  14. package/dist/types/src/service-rpc.d.ts.map +1 -0
  15. package/dist/types/src/service.d.ts +14 -18
  16. package/dist/types/src/service.d.ts.map +1 -1
  17. package/dist/types/src/types/SpaceProperties.d.ts +5 -11
  18. package/dist/types/src/types/SpaceProperties.d.ts.map +1 -1
  19. package/dist/types/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +19 -14
  21. package/src/Rpc.ts +200 -0
  22. package/src/bridge-rpc.ts +114 -0
  23. package/src/config.ts +0 -1
  24. package/src/index.ts +3 -1
  25. package/src/service-rpc.ts +439 -0
  26. package/src/service.ts +17 -45
  27. package/src/types/SpaceProperties.ts +4 -11
  28. package/dist/lib/browser/chunk-3SERD2CK.mjs +0 -40
  29. package/dist/lib/browser/chunk-3SERD2CK.mjs.map +0 -7
  30. package/dist/lib/browser/index.mjs +0 -233
  31. package/dist/lib/browser/index.mjs.map +0 -7
  32. package/dist/lib/browser/meta.json +0 -1
  33. package/dist/lib/browser/types/index.mjs +0 -11
  34. package/dist/lib/browser/types/index.mjs.map +0 -7
  35. package/dist/lib/node-esm/chunk-YDC7XBNG.mjs +0 -42
  36. package/dist/lib/node-esm/chunk-YDC7XBNG.mjs.map +0 -7
  37. package/dist/lib/node-esm/index.mjs +0 -234
  38. package/dist/lib/node-esm/index.mjs.map +0 -7
  39. package/dist/lib/node-esm/meta.json +0 -1
  40. package/dist/lib/node-esm/types/index.mjs +0 -12
  41. package/dist/lib/node-esm/types/index.mjs.map +0 -7
  42. package/dist/types/src/client.d.ts +0 -2
  43. package/dist/types/src/client.d.ts.map +0 -1
  44. package/src/client.ts +0 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/client-protocol",
3
- "version": "0.9.1-staging.ee54ba693a",
3
+ "version": "0.11.0",
4
4
  "description": "Core DXOS Client API.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -16,14 +16,12 @@
16
16
  ".": {
17
17
  "source": "./src/index.ts",
18
18
  "types": "./dist/types/src/index.d.ts",
19
- "browser": "./dist/lib/browser/index.mjs",
20
- "node": "./dist/lib/node-esm/index.mjs"
19
+ "import": "./dist/lib/index.mjs"
21
20
  },
22
21
  "./types": {
23
22
  "source": "./src/types/index.ts",
24
23
  "types": "./dist/types/src/types/index.d.ts",
25
- "browser": "./dist/lib/browser/types/index.mjs",
26
- "node": "./dist/lib/node-esm/types/index.mjs"
24
+ "import": "./dist/lib/types.mjs"
27
25
  }
28
26
  },
29
27
  "types": "dist/types/src/index.d.ts",
@@ -32,17 +30,24 @@
32
30
  "src"
33
31
  ],
34
32
  "dependencies": {
33
+ "@effect/platform": "0.96.2",
34
+ "@effect/platform-browser": "0.76.0",
35
+ "@effect/rpc": "0.75.1",
35
36
  "base-x": "~3.0.11",
36
37
  "effect": "3.21.4",
37
- "@dxos/credentials": "0.9.1-staging.ee54ba693a",
38
- "@dxos/echo-client": "0.9.1-staging.ee54ba693a",
39
- "@dxos/echo": "0.9.1-staging.ee54ba693a",
40
- "@dxos/async": "0.9.1-staging.ee54ba693a",
41
- "@dxos/node-std": "0.9.1-staging.ee54ba693a",
42
- "@dxos/invariant": "0.9.1-staging.ee54ba693a",
43
- "@dxos/keys": "0.9.1-staging.ee54ba693a",
44
- "@dxos/protocols": "0.9.1-staging.ee54ba693a",
45
- "@dxos/rpc": "0.9.1-staging.ee54ba693a"
38
+ "@dxos/async": "0.11.0",
39
+ "@dxos/codec-protobuf": "0.11.0",
40
+ "@dxos/credentials": "0.11.0",
41
+ "@dxos/echo": "0.11.0",
42
+ "@dxos/echo-client": "0.11.0",
43
+ "@dxos/effect": "0.11.0",
44
+ "@dxos/invariant": "0.11.0",
45
+ "@dxos/keys": "0.11.0",
46
+ "@dxos/node-std": "0.11.0",
47
+ "@dxos/protocols": "0.11.0",
48
+ "@dxos/rpc": "0.11.0",
49
+ "@dxos/rpc-tunnel": "0.11.0",
50
+ "@dxos/worker-framework": "0.11.0"
46
51
  },
47
52
  "peerDependencies": {
48
53
  "effect": "3.21.4"
package/src/Rpc.ts ADDED
@@ -0,0 +1,200 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import * as BrowserWorker from '@effect/platform-browser/BrowserWorker';
8
+ import * as BrowserWorkerRunner from '@effect/platform-browser/BrowserWorkerRunner';
9
+ import * as RpcClient from '@effect/rpc/RpcClient';
10
+ import * as RpcServer from '@effect/rpc/RpcServer';
11
+ import * as Effect from 'effect/Effect';
12
+ import * as Layer from 'effect/Layer';
13
+ import * as ManagedRuntime from 'effect/ManagedRuntime';
14
+ import type * as Scope from 'effect/Scope';
15
+
16
+ import { RpcTiming } from '@dxos/worker-framework';
17
+
18
+ export type ServeOptions = {
19
+ disableTracing?: boolean;
20
+ concurrency?: number | 'unbounded';
21
+ /**
22
+ * When enabled, stamps each outbound RPC with a `Date.now()` send time and logs queue-wait /
23
+ * service durations on the worker above {@link RpcTiming.Options.minLogMs} (default 100 ms).
24
+ */
25
+ timing?: boolean | RpcTiming.Options;
26
+ };
27
+
28
+ // A worker RPC client runs over a single MessagePort that multiplexes every request by id, so there
29
+ // is no real per-connection concurrency limit. The `@effect/rpc` worker protocol backs the client
30
+ // with a `Pool` that holds one worker for a request's whole lifetime — including long-lived streams.
31
+ // The pool's default concurrency of 1 therefore lets a single open stream (e.g. a subscription) block
32
+ // every other call. Allow effectively-unbounded concurrent requests over the one worker instead.
33
+ const WORKER_CLIENT_CONCURRENCY = Number.MAX_SAFE_INTEGER;
34
+
35
+ // Merged rpc groups (e.g. ClientServicesRpcs) do not structurally satisfy RpcGroup<Rpc.Any>
36
+ // in @effect/rpc's type parameter; runtime dispatch accepts any RpcGroup instance.
37
+ const asRpcGroup = <G>(group: G): Parameters<typeof RpcClient.make>[0] => group as Parameters<typeof RpcClient.make>[0];
38
+
39
+ /**
40
+ * Builds an effect-native RPC client over a caller-supplied {@link RpcClient.Protocol} layer.
41
+ * Transport-agnostic: consumers provide the Worker-platform protocol (see {@link makeClient}) or a
42
+ * byte protocol over a legacy transport (e.g. `@dxos/rpc`'s RpcPort layer for iframe/devtools bridges).
43
+ */
44
+ export const makeClientOverProtocol = <G, ProtocolError, ProtocolRequirements>(
45
+ protocol: Layer.Layer<RpcClient.Protocol, ProtocolError, ProtocolRequirements>,
46
+ group: G,
47
+ options?: Pick<ServeOptions, 'disableTracing' | 'timing'>,
48
+ ): Effect.Effect<unknown, never, Scope.Scope | ProtocolRequirements> =>
49
+ Effect.gen(function* () {
50
+ const timingEnabled = RpcTiming.isEnabled(options?.timing);
51
+ const rpcGroup = timingEnabled ? RpcTiming.applyMiddleware(asRpcGroup(group)) : asRpcGroup(group);
52
+ const protocolLayer = timingEnabled ? protocol.pipe(Layer.provideMerge(RpcTiming.clientLayer())) : protocol;
53
+
54
+ // Build the transport into the caller's scope (extended via `Scope.extend`) rather than
55
+ // `Effect.provide`-ing the layer directly: that would bind the transport's lifetime to this
56
+ // construction effect, tearing the worker connection down the instant the client is returned
57
+ // (the client is used later by the caller). `Layer.build` keeps it alive for the caller's scope.
58
+ const context = yield* Layer.build(protocolLayer);
59
+ return yield* RpcClient.make(asRpcGroup(rpcGroup), { disableTracing: options?.disableTracing ?? true }).pipe(
60
+ Effect.provide(context),
61
+ );
62
+ }).pipe(Effect.orDie);
63
+
64
+ /**
65
+ * Builds an effect-native RPC client over a {@link MessagePort} using the native Worker platform
66
+ * protocol (structured-clone frames, transferables supported).
67
+ */
68
+ export const makeClient = <G>(
69
+ port: MessagePort,
70
+ group: G,
71
+ options?: Pick<ServeOptions, 'disableTracing' | 'timing'>,
72
+ ): Effect.Effect<unknown, never, Scope.Scope> =>
73
+ makeClientOverProtocol(
74
+ RpcClient.layerProtocolWorker({ size: 1, concurrency: WORKER_CLIENT_CONCURRENCY }).pipe(
75
+ Layer.provide(BrowserWorker.layerPlatform(() => port)),
76
+ ),
77
+ group,
78
+ options,
79
+ );
80
+
81
+ export type GroupServer = {
82
+ open(): Promise<void>;
83
+ close(): Promise<void>;
84
+ };
85
+
86
+ /**
87
+ * Serves an {@link RpcGroup} on a {@link MessagePort} via the native Worker runner protocol.
88
+ */
89
+ export const serve = <G, H extends Layer.Layer<never, never, never>>(
90
+ port: MessagePort,
91
+ group: G,
92
+ handlers: H,
93
+ options?: ServeOptions,
94
+ ): GroupServer => {
95
+ let runtime: ManagedRuntime.ManagedRuntime<never, never> | undefined;
96
+
97
+ return {
98
+ async open(): Promise<void> {
99
+ if (runtime) {
100
+ return;
101
+ }
102
+
103
+ const timingEnabled = RpcTiming.isEnabled(options?.timing);
104
+ const rpcGroup = timingEnabled ? RpcTiming.applyMiddleware(asRpcGroup(group)) : asRpcGroup(group);
105
+ // Merge the timing middleware layer into the handler layer (rather than a conditional
106
+ // `.pipe(...)` element) so the pipeline stays a fixed tuple.
107
+ const handlersLayer = timingEnabled
108
+ ? Layer.merge(handlers, RpcTiming.serverLayer(RpcTiming.resolveOptions(options?.timing)))
109
+ : handlers;
110
+ const serverLayer = RpcServer.layer(asRpcGroup(rpcGroup), {
111
+ disableTracing: options?.disableTracing ?? true,
112
+ concurrency: options?.concurrency ?? 'unbounded',
113
+ }).pipe(
114
+ Layer.provide(handlersLayer),
115
+ Layer.provide(
116
+ RpcServer.layerProtocolWorkerRunner.pipe(Layer.provide(BrowserWorkerRunner.layerMessagePort(port))),
117
+ ),
118
+ Layer.orDie,
119
+ );
120
+
121
+ const current = ManagedRuntime.make(serverLayer);
122
+ try {
123
+ await current.runPromise(Effect.void);
124
+ } catch (error) {
125
+ // Leave the server un-opened on startup failure so a later open() can retry rather than
126
+ // returning early against a runtime that never started.
127
+ await current.dispose();
128
+ throw error;
129
+ }
130
+ runtime = current;
131
+ },
132
+
133
+ async close(): Promise<void> {
134
+ const current = runtime;
135
+ runtime = undefined;
136
+ await current?.dispose();
137
+ },
138
+ };
139
+ };
140
+
141
+ /**
142
+ * Serves an {@link RpcGroup} over a caller-supplied {@link RpcServer.Protocol} layer.
143
+ * Transport-agnostic counterpart to {@link makeClientOverProtocol}: consumers provide a byte
144
+ * protocol over a legacy transport (e.g. `@dxos/rpc`'s RpcPort layer for iframe/devtools bridges).
145
+ */
146
+ export const serveOverProtocol = <G, H extends Layer.Layer<never, never, never>>(
147
+ protocol: Layer.Layer<RpcServer.Protocol>,
148
+ group: G,
149
+ handlers: H,
150
+ options?: ServeOptions,
151
+ ): GroupServer => {
152
+ let runtime: ManagedRuntime.ManagedRuntime<never, never> | undefined;
153
+ // Cache the in-flight open so concurrent opens share one initialization and a close during open
154
+ // can await it before disposing — otherwise `runtime` is unset mid-open and a fast open/close
155
+ // (e.g. client restart) leaks the runtime.
156
+ let openPromise: Promise<void> | undefined;
157
+
158
+ return {
159
+ async open(): Promise<void> {
160
+ if (openPromise) {
161
+ return openPromise;
162
+ }
163
+
164
+ openPromise = (async () => {
165
+ const timingEnabled = RpcTiming.isEnabled(options?.timing);
166
+ const rpcGroup = timingEnabled ? RpcTiming.applyMiddleware(asRpcGroup(group)) : asRpcGroup(group);
167
+ const handlersLayer = timingEnabled
168
+ ? Layer.merge(handlers, RpcTiming.serverLayer(RpcTiming.resolveOptions(options?.timing)))
169
+ : handlers;
170
+ const serverLayer = RpcServer.layer(asRpcGroup(rpcGroup), {
171
+ disableTracing: options?.disableTracing ?? true,
172
+ concurrency: options?.concurrency ?? 'unbounded',
173
+ }).pipe(Layer.provide(handlersLayer), Layer.provide(protocol), Layer.orDie);
174
+
175
+ const current = ManagedRuntime.make(serverLayer);
176
+ try {
177
+ await current.runPromise(Effect.void);
178
+ } catch (error) {
179
+ // Leave the server un-opened on startup failure so a later open() can retry.
180
+ openPromise = undefined;
181
+ await current.dispose();
182
+ throw error;
183
+ }
184
+ runtime = current;
185
+ })();
186
+ return openPromise;
187
+ },
188
+
189
+ async close(): Promise<void> {
190
+ if (openPromise) {
191
+ // Wait for an in-flight open so we dispose the runtime it created rather than leaking it.
192
+ await openPromise.catch(() => {});
193
+ openPromise = undefined;
194
+ }
195
+ const current = runtime;
196
+ runtime = undefined;
197
+ await current?.dispose();
198
+ },
199
+ };
200
+ };
@@ -0,0 +1,114 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as RpcClient from '@effect/rpc/RpcClient';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Exit from 'effect/Exit';
8
+ import * as Layer from 'effect/Layer';
9
+ import * as Runtime from 'effect/Runtime';
10
+ import * as Scope from 'effect/Scope';
11
+
12
+ import { type Stream as PbStream } from '@dxos/codec-protobuf/stream';
13
+ import { EffectEx } from '@dxos/effect';
14
+ import { type BridgeService as BridgeServiceRpc } from '@dxos/protocols/proto/dxos/mesh/bridge';
15
+ import { BridgeService } from '@dxos/protocols/rpc';
16
+
17
+ import * as Rpc from './Rpc';
18
+ import { pbStreamToStream, streamToPbStream } from './service-rpc';
19
+
20
+ /**
21
+ * The system channel runs the WebRTC {@link BridgeServiceRpc} in the worker→tab direction: the worker
22
+ * calls the tab's network stack to proxy RTC connections. It is served over its own {@link MessagePort}
23
+ * (the reverse of the app port) via effect-rpc, replacing the legacy protobuf duplex peer.
24
+ *
25
+ * `BridgeServiceRpc` is the proto-shaped interface (`Promise`/{@link PbStream}) that
26
+ * `RtcTransportService` implements and `RtcTransportProxyFactory` consumes; the helpers below adapt
27
+ * it to and from the effect-rpc surface.
28
+ */
29
+
30
+ const toError = (cause: unknown): Error => (cause instanceof Error ? cause : new Error(String(cause)));
31
+
32
+ /**
33
+ * Serves a proto-shaped {@link BridgeServiceRpc} (the tab's `RtcTransportService`) over a
34
+ * {@link MessagePort} via effect-rpc. The worker consumes it with {@link makeBridgeServiceClient}.
35
+ */
36
+ export const serveBridgeService = (port: MessagePort, service: BridgeServiceRpc): Rpc.GroupServer => {
37
+ const unary =
38
+ <Req, Res>(method: (request: Req) => Promise<Res>) =>
39
+ (payload: Req) =>
40
+ Effect.tryPromise({ try: () => method(payload), catch: toError });
41
+
42
+ const handlers = {
43
+ 'BridgeService.open': (payload: Parameters<BridgeServiceRpc['open']>[0]) =>
44
+ pbStreamToStream(() => service.open(payload)),
45
+ 'BridgeService.sendSignal': unary((request: Parameters<BridgeServiceRpc['sendSignal']>[0]) =>
46
+ service.sendSignal(request),
47
+ ),
48
+ 'BridgeService.sendData': unary((request: Parameters<BridgeServiceRpc['sendData']>[0]) =>
49
+ service.sendData(request),
50
+ ),
51
+ 'BridgeService.close': unary((request: Parameters<BridgeServiceRpc['close']>[0]) => service.close(request)),
52
+ 'BridgeService.getDetails': unary((request: Parameters<BridgeServiceRpc['getDetails']>[0]) =>
53
+ service.getDetails(request),
54
+ ),
55
+ 'BridgeService.getStats': unary((request: Parameters<BridgeServiceRpc['getStats']>[0]) =>
56
+ service.getStats(request),
57
+ ),
58
+ };
59
+
60
+ // Dispatched dynamically across the group; per-method handler types cannot be expressed statically.
61
+ return Rpc.serve(port, BridgeService.Rpcs, BridgeService.Rpcs.toLayer(handlers as never), {
62
+ disableTracing: true,
63
+ concurrency: 'unbounded',
64
+ });
65
+ };
66
+
67
+ /**
68
+ * Builds a proto-shaped {@link BridgeServiceRpc} backed by an effect-rpc client over a
69
+ * {@link MessagePort}. Used in the worker to hand `RtcTransportProxyFactory` a bridge that proxies to
70
+ * the tab. The returned `close` releases the transport scope.
71
+ */
72
+ export const makeBridgeServiceClient = async (
73
+ port: MessagePort,
74
+ ): Promise<{ bridgeService: BridgeServiceRpc; close: () => Promise<void> }> =>
75
+ bridgeServiceClientFromEffect((scope) => Rpc.makeClient(port, BridgeService.Rpcs).pipe(Scope.extend(scope)));
76
+
77
+ /**
78
+ * Builds a proto-shaped {@link BridgeServiceRpc} over a pre-built {@link RpcClient.Protocol} (the
79
+ * value the tag resolves to — e.g. the worker→client protocol handed to a worker-framework session
80
+ * via effect context) rather than a raw {@link MessagePort}.
81
+ */
82
+ export const makeBridgeServiceClientOverProtocol = async (
83
+ protocol: RpcClient.Protocol['Type'],
84
+ ): Promise<{ bridgeService: BridgeServiceRpc; close: () => Promise<void> }> =>
85
+ bridgeServiceClientFromEffect((scope) =>
86
+ Rpc.makeClientOverProtocol(Layer.succeed(RpcClient.Protocol, protocol), BridgeService.Rpcs).pipe(
87
+ Scope.extend(scope),
88
+ ),
89
+ );
90
+
91
+ /** Adapts an effect-rpc {@link BridgeService.Client} (built by the caller) to the proto-shaped surface. */
92
+ const bridgeServiceClientFromEffect = async (
93
+ makeClient: (scope: Scope.Scope) => Effect.Effect<unknown, never, never>,
94
+ ): Promise<{ bridgeService: BridgeServiceRpc; close: () => Promise<void> }> => {
95
+ const scope = Effect.runSync(Scope.make());
96
+ const client = (await EffectEx.runPromise(makeClient(scope))) as BridgeService.Client;
97
+
98
+ const bridge = client.BridgeService;
99
+ const bridgeService: BridgeServiceRpc = {
100
+ open: (request) => streamToPbStream(Runtime.defaultRuntime, bridge.open(request)),
101
+ sendSignal: (request) => EffectEx.runPromise(bridge.sendSignal(request)),
102
+ sendData: (request) => EffectEx.runPromise(bridge.sendData(request)),
103
+ close: (request) => EffectEx.runPromise(bridge.close(request)),
104
+ getDetails: (request) => EffectEx.runPromise(bridge.getDetails(request)),
105
+ getStats: (request) => EffectEx.runPromise(bridge.getStats(request)),
106
+ };
107
+
108
+ return {
109
+ bridgeService,
110
+ close: async () => {
111
+ await EffectEx.runPromise(Scope.close(scope, Exit.void));
112
+ },
113
+ };
114
+ };
package/src/config.ts CHANGED
@@ -7,7 +7,6 @@
7
7
  export const DEFAULT_CLIENT_CHANNEL = 'dxos:app';
8
8
  export const DEFAULT_INTERNAL_CHANNEL = 'dxos:vault';
9
9
  export const DEFAULT_SHELL_CHANNEL = 'dxos:shell';
10
- export const DEFAULT_WORKER_BROADCAST_CHANNEL = 'dxos:shared-worker';
11
10
 
12
11
  /**
13
12
  * @deprecated
package/src/index.ts CHANGED
@@ -2,12 +2,14 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- export * from './client';
5
+ export * from './bridge-rpc';
6
6
  export * from './config';
7
+ export * as Rpc from './Rpc';
7
8
  export type * from './echo';
8
9
  export type * from './halo';
9
10
  export * from './invitations';
10
11
  export * from './service';
12
+ export * from './service-rpc';
11
13
  export type * from './shell';
12
14
  export * from './space';
13
15
  export * from './timeouts';