@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.
- package/dist/lib/index.mjs +590 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/types.mjs +26 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/types/src/Rpc.d.ts +41 -0
- package/dist/types/src/Rpc.d.ts.map +1 -0
- package/dist/types/src/bridge-rpc.d.ts +27 -0
- package/dist/types/src/bridge-rpc.d.ts.map +1 -0
- package/dist/types/src/config.d.ts +0 -1
- package/dist/types/src/config.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/service-rpc.d.ts +551 -0
- package/dist/types/src/service-rpc.d.ts.map +1 -0
- package/dist/types/src/service.d.ts +14 -18
- package/dist/types/src/service.d.ts.map +1 -1
- package/dist/types/src/types/SpaceProperties.d.ts +5 -11
- package/dist/types/src/types/SpaceProperties.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -14
- package/src/Rpc.ts +200 -0
- package/src/bridge-rpc.ts +114 -0
- package/src/config.ts +0 -1
- package/src/index.ts +3 -1
- package/src/service-rpc.ts +439 -0
- package/src/service.ts +17 -45
- package/src/types/SpaceProperties.ts +4 -11
- package/dist/lib/browser/chunk-3SERD2CK.mjs +0 -40
- package/dist/lib/browser/chunk-3SERD2CK.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -233
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/types/index.mjs +0 -11
- package/dist/lib/browser/types/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-YDC7XBNG.mjs +0 -42
- package/dist/lib/node-esm/chunk-YDC7XBNG.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -234
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/types/index.mjs +0 -12
- package/dist/lib/node-esm/types/index.mjs.map +0 -7
- package/dist/types/src/client.d.ts +0 -2
- package/dist/types/src/client.d.ts.map +0 -1
- package/src/client.ts +0 -5
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as EffectRpc from '@effect/rpc/Rpc';
|
|
6
|
+
import * as RpcGroup from '@effect/rpc/RpcGroup';
|
|
7
|
+
import * as RpcSchema from '@effect/rpc/RpcSchema';
|
|
8
|
+
import * as RpcServer from '@effect/rpc/RpcServer';
|
|
9
|
+
import * as RpcTest from '@effect/rpc/RpcTest';
|
|
10
|
+
import * as Cause from 'effect/Cause';
|
|
11
|
+
import * as Effect from 'effect/Effect';
|
|
12
|
+
import * as Layer from 'effect/Layer';
|
|
13
|
+
import * as Runtime from 'effect/Runtime';
|
|
14
|
+
import type * as Scope from 'effect/Scope';
|
|
15
|
+
import * as Stream from 'effect/Stream';
|
|
16
|
+
|
|
17
|
+
import { type RequestOptions } from '@dxos/codec-protobuf';
|
|
18
|
+
import { Stream as PbStream } from '@dxos/codec-protobuf/stream';
|
|
19
|
+
import { invariant } from '@dxos/invariant';
|
|
20
|
+
import { runServiceCall } from '@dxos/protocols';
|
|
21
|
+
import {
|
|
22
|
+
ContactsService,
|
|
23
|
+
DataService,
|
|
24
|
+
DevicesService,
|
|
25
|
+
DevtoolsHost,
|
|
26
|
+
EdgeAgentService,
|
|
27
|
+
FeedService,
|
|
28
|
+
IdentityService,
|
|
29
|
+
InvitationsService,
|
|
30
|
+
LoggingService,
|
|
31
|
+
NetworkService,
|
|
32
|
+
QueryService,
|
|
33
|
+
SpacesService,
|
|
34
|
+
SystemService,
|
|
35
|
+
WorkerService,
|
|
36
|
+
} from '@dxos/protocols/rpc';
|
|
37
|
+
import { type RpcPort, layerProtocolRpcPortClient, layerProtocolRpcPortServer } from '@dxos/rpc';
|
|
38
|
+
import { createIFramePort } from '@dxos/rpc-tunnel';
|
|
39
|
+
|
|
40
|
+
import { DEFAULT_CLIENT_CHANNEL } from './config';
|
|
41
|
+
import * as Rpc from './Rpc';
|
|
42
|
+
import { type ClientServices } from './service';
|
|
43
|
+
|
|
44
|
+
export type MessagePortLike = MessagePort;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Transport a {@link ClientServicesRpc} can run over: the native Worker-platform {@link MessagePort}
|
|
48
|
+
* (shared-worker app port) or a byte {@link RpcPort} for legacy protobuf bridges (iframe shell,
|
|
49
|
+
* devtools extension).
|
|
50
|
+
*/
|
|
51
|
+
export type ClientServicesTransport = MessagePortLike | RpcPort;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* All client service RPCs served over a single connection.
|
|
55
|
+
* Rpc tags are prefixed with the {@link ClientServices} key (e.g. `DataService.subscribe`).
|
|
56
|
+
*
|
|
57
|
+
* {@link WorkerService} (the tab→worker control channel: `start`/`stop`) is merged in here rather
|
|
58
|
+
* than served over a second port: it runs in the same tab→worker direction as the service RPCs, so
|
|
59
|
+
* it multiplexes over the same app {@link MessagePort}. Only the reverse-direction `BridgeService`
|
|
60
|
+
* (worker→tab) needs its own port.
|
|
61
|
+
*/
|
|
62
|
+
export class ClientServicesRpcs extends RpcGroup.make().merge(
|
|
63
|
+
SystemService.Rpcs,
|
|
64
|
+
NetworkService.Rpcs,
|
|
65
|
+
LoggingService.Rpcs,
|
|
66
|
+
IdentityService.Rpcs,
|
|
67
|
+
InvitationsService.Rpcs,
|
|
68
|
+
DevicesService.Rpcs,
|
|
69
|
+
SpacesService.Rpcs,
|
|
70
|
+
DataService.Rpcs,
|
|
71
|
+
QueryService.Rpcs,
|
|
72
|
+
FeedService.Rpcs,
|
|
73
|
+
ContactsService.Rpcs,
|
|
74
|
+
EdgeAgentService.Rpcs,
|
|
75
|
+
DevtoolsHost.Rpcs,
|
|
76
|
+
WorkerService.Rpcs,
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
type ClientServicesRpcUnion = RpcGroup.Rpcs<typeof ClientServicesRpcs>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Host-side service implementations, one per client service, each in the effect-rpc `Handlers`
|
|
83
|
+
* shape (Effect/Stream-returning, tag-keyed). This is the shape service hosts provide and
|
|
84
|
+
* {@link ClientRpcServer} serves directly, with no protobuf encode/decode at the boundary.
|
|
85
|
+
*/
|
|
86
|
+
export type ClientServicesHandlers = {
|
|
87
|
+
SystemService: SystemService.Handlers;
|
|
88
|
+
NetworkService: NetworkService.Handlers;
|
|
89
|
+
LoggingService: LoggingService.Handlers;
|
|
90
|
+
IdentityService: IdentityService.Handlers;
|
|
91
|
+
InvitationsService: InvitationsService.Handlers;
|
|
92
|
+
DevicesService: DevicesService.Handlers;
|
|
93
|
+
SpacesService: SpacesService.Handlers;
|
|
94
|
+
DataService: DataService.Handlers;
|
|
95
|
+
QueryService: QueryService.Handlers;
|
|
96
|
+
FeedService: FeedService.Handlers;
|
|
97
|
+
ContactsService: ContactsService.Handlers;
|
|
98
|
+
EdgeAgentService: EdgeAgentService.Handlers;
|
|
99
|
+
DevtoolsHost: DevtoolsHost.Handlers;
|
|
100
|
+
// Provided per-session by the worker session (drives readiness/origin/lock), not by the host.
|
|
101
|
+
WorkerService: WorkerService.Handlers;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const toError = (cause: unknown): Error => (cause instanceof Error ? cause : new Error(String(cause)));
|
|
105
|
+
|
|
106
|
+
const isVoidSchema = (schema: { ast: { _tag: string } }): boolean => schema.ast._tag === 'VoidKeyword';
|
|
107
|
+
|
|
108
|
+
/** Splits an rpc tag into the {@link ClientServices} key and the service method name. */
|
|
109
|
+
const parseTag = (tag: string): [serviceKey: keyof ClientServices, methodName: string] => {
|
|
110
|
+
const index = tag.indexOf('.');
|
|
111
|
+
return [tag.slice(0, index) as keyof ClientServices, tag.slice(index + 1)];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Resolves a bound service method from a services provider, throwing if the service/method is absent. */
|
|
115
|
+
const resolveServiceMethod = (
|
|
116
|
+
services: Partial<ClientServices>,
|
|
117
|
+
serviceKey: keyof ClientServices,
|
|
118
|
+
methodName: string,
|
|
119
|
+
tag: string,
|
|
120
|
+
): ((request: unknown) => unknown) => {
|
|
121
|
+
const service = services[serviceKey] as Record<string, unknown> | undefined;
|
|
122
|
+
if (!service) {
|
|
123
|
+
throw new Error(`Service not available: ${serviceKey}`);
|
|
124
|
+
}
|
|
125
|
+
const method = service[methodName];
|
|
126
|
+
if (typeof method !== 'function') {
|
|
127
|
+
throw new Error(`Method not available: ${tag}`);
|
|
128
|
+
}
|
|
129
|
+
return (method as (request: unknown) => unknown).bind(service);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
//
|
|
133
|
+
// Server.
|
|
134
|
+
//
|
|
135
|
+
|
|
136
|
+
export type ClientRpcServerParams = {
|
|
137
|
+
/**
|
|
138
|
+
* Native Worker-platform transport. Provide this or {@link protocol}.
|
|
139
|
+
*/
|
|
140
|
+
port?: MessagePortLike;
|
|
141
|
+
/**
|
|
142
|
+
* Pre-built server protocol (the value the {@link RpcServer.Protocol} tag resolves to — e.g. handed
|
|
143
|
+
* to a worker-framework session via effect context). Provide this or {@link port}.
|
|
144
|
+
*/
|
|
145
|
+
protocol?: RpcServer.Protocol['Type'];
|
|
146
|
+
/**
|
|
147
|
+
* Resolved per call so the served set follows the host lifecycle (services host open/close).
|
|
148
|
+
*/
|
|
149
|
+
services: () => Partial<ClientServicesHandlers>;
|
|
150
|
+
/**
|
|
151
|
+
* Awaited before dispatching each request (e.g. worker readiness); a rejection fails the call.
|
|
152
|
+
*/
|
|
153
|
+
onRequest?: () => Promise<void>;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Serves {@link ClientServices} implementations over a {@link MessagePort} via effect-rpc.
|
|
158
|
+
*/
|
|
159
|
+
export class ClientRpcServer {
|
|
160
|
+
readonly #params: ClientRpcServerParams;
|
|
161
|
+
#server?: ReturnType<typeof Rpc.serve>;
|
|
162
|
+
|
|
163
|
+
constructor(params: ClientRpcServerParams) {
|
|
164
|
+
this.#params = params;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async open(): Promise<void> {
|
|
168
|
+
if (this.#server) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const handlers = makeClientServicesHandlers(this.#params);
|
|
173
|
+
const options = { disableTracing: true, concurrency: 'unbounded' } as const;
|
|
174
|
+
if (this.#params.protocol) {
|
|
175
|
+
this.#server = Rpc.serveOverProtocol(
|
|
176
|
+
Layer.succeed(RpcServer.Protocol, this.#params.protocol),
|
|
177
|
+
ClientServicesRpcs,
|
|
178
|
+
handlers,
|
|
179
|
+
options,
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
invariant(this.#params.port, 'ClientRpcServer requires a port or a protocol.');
|
|
183
|
+
this.#server = Rpc.serve(this.#params.port, ClientServicesRpcs, handlers, options);
|
|
184
|
+
}
|
|
185
|
+
await this.#server.open();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async close(): Promise<void> {
|
|
189
|
+
const server = this.#server;
|
|
190
|
+
this.#server = undefined;
|
|
191
|
+
await server?.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type ServeClientServicesOverIFrameOptions = {
|
|
196
|
+
iframe: HTMLIFrameElement;
|
|
197
|
+
origin: string;
|
|
198
|
+
channel?: string;
|
|
199
|
+
services: () => Partial<ClientServicesHandlers>;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Serves {@link ClientServices} to a child iframe (e.g. the shell) over the effect-rpc byte protocol
|
|
204
|
+
* and opens the connection. The iframe uses a byte {@link RpcPort} transport (not a `MessagePort`),
|
|
205
|
+
* matching the shell's {@link ClientServicesProxy} consumer, so this serves over
|
|
206
|
+
* {@link layerProtocolRpcPortServer} rather than the Worker-runner protocol used by
|
|
207
|
+
* {@link ClientRpcServer}. Kept here — alongside the rpc definitions — so the `RpcPort` binding is
|
|
208
|
+
* resolved in this package's type universe rather than in `@dxos/client`, whose merged-with-`main`
|
|
209
|
+
* build resolves `ClientRpcServer.port` as a `MessagePort`.
|
|
210
|
+
*/
|
|
211
|
+
export const serveClientServicesOverIFrame = async ({
|
|
212
|
+
iframe,
|
|
213
|
+
origin,
|
|
214
|
+
channel = DEFAULT_CLIENT_CHANNEL,
|
|
215
|
+
services,
|
|
216
|
+
}: ServeClientServicesOverIFrameOptions): Promise<Rpc.GroupServer> => {
|
|
217
|
+
const server = Rpc.serveOverProtocol(
|
|
218
|
+
layerProtocolRpcPortServer(createIFramePort({ channel, iframe, origin })),
|
|
219
|
+
ClientServicesRpcs,
|
|
220
|
+
makeClientServicesHandlers({ services }),
|
|
221
|
+
{ disableTracing: true, concurrency: 'unbounded' },
|
|
222
|
+
);
|
|
223
|
+
await server.open();
|
|
224
|
+
return server;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Builds handler layers for every client service RPC, dispatching to the service implementations
|
|
229
|
+
* resolved from `services` on each call.
|
|
230
|
+
*/
|
|
231
|
+
export const makeClientServicesHandlers = ({
|
|
232
|
+
services,
|
|
233
|
+
onRequest,
|
|
234
|
+
}: Pick<ClientRpcServerParams, 'services' | 'onRequest'>): Layer.Layer<EffectRpc.ToHandler<ClientServicesRpcUnion>> => {
|
|
235
|
+
const gate = onRequest ? Effect.tryPromise({ try: onRequest, catch: toError }) : Effect.void;
|
|
236
|
+
|
|
237
|
+
const handlers: Record<string, (payload: unknown) => unknown> = {};
|
|
238
|
+
for (const [tag, rpc] of ClientServicesRpcs.requests) {
|
|
239
|
+
const [serviceKey] = parseTag(tag);
|
|
240
|
+
// The host service is itself in the Handlers shape, keyed by the full prefixed tag; invoking it
|
|
241
|
+
// returns the Effect/Stream directly, so no protobuf encode/decode adapter is needed.
|
|
242
|
+
const invoke = (payload: unknown) => {
|
|
243
|
+
const service = services()[serviceKey] as Record<string, (payload: unknown) => unknown> | undefined;
|
|
244
|
+
const handler = service?.[tag];
|
|
245
|
+
if (typeof handler !== 'function') {
|
|
246
|
+
throw new Error(`Service handler not available: ${tag}`);
|
|
247
|
+
}
|
|
248
|
+
return handler.call(service, payload);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (RpcSchema.isStreamSchema(rpc.successSchema)) {
|
|
252
|
+
handlers[tag] = (payload: unknown) =>
|
|
253
|
+
gate.pipe(
|
|
254
|
+
Effect.map(() => invoke(payload) as Stream.Stream<unknown, unknown>),
|
|
255
|
+
Stream.unwrap,
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
handlers[tag] = (payload: unknown) =>
|
|
259
|
+
gate.pipe(Effect.flatMap(() => invoke(payload) as Effect.Effect<unknown, unknown>));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Handlers are dispatched dynamically across all merged service groups, so their per-method types
|
|
264
|
+
// cannot be expressed statically.
|
|
265
|
+
return ClientServicesRpcs.toLayer(handlers as never);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
//
|
|
269
|
+
// Client.
|
|
270
|
+
//
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Effect-native client for all client services, inferred from the effect-rpc definitions.
|
|
274
|
+
* Nested by service key (e.g. `rpc.DataService.subscribe(req)` returns a `Stream`, unary methods
|
|
275
|
+
* return an `Effect`). Each service's `Client` type already nests its methods under the service key
|
|
276
|
+
* (the rpc tags are prefixed), so this is their intersection — cheaper for the type-checker than
|
|
277
|
+
* re-expanding the full merged {@link RpcClient.RpcClient} mapped type over all services.
|
|
278
|
+
*/
|
|
279
|
+
export interface ClientServicesRpc
|
|
280
|
+
extends
|
|
281
|
+
SystemService.Client,
|
|
282
|
+
NetworkService.Client,
|
|
283
|
+
LoggingService.Client,
|
|
284
|
+
IdentityService.Client,
|
|
285
|
+
InvitationsService.Client,
|
|
286
|
+
DevicesService.Client,
|
|
287
|
+
SpacesService.Client,
|
|
288
|
+
DataService.Client,
|
|
289
|
+
QueryService.Client,
|
|
290
|
+
FeedService.Client,
|
|
291
|
+
ContactsService.Client,
|
|
292
|
+
EdgeAgentService.Client,
|
|
293
|
+
DevtoolsHost.Client,
|
|
294
|
+
WorkerService.Client {}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Builds the effect-native {@link ClientServicesRpc} over a {@link MessagePort}.
|
|
298
|
+
* The returned scope owns the connection; closing it releases the transport.
|
|
299
|
+
*/
|
|
300
|
+
export const makeClientServicesRpc = (
|
|
301
|
+
port: ClientServicesTransport,
|
|
302
|
+
): Effect.Effect<ClientServicesRpc, never, Scope.Scope> =>
|
|
303
|
+
// An RpcPort (byte transport) carries the legacy iframe/devtools bridges; a MessagePort uses the
|
|
304
|
+
// native Worker-platform protocol.
|
|
305
|
+
('send' in port
|
|
306
|
+
? Rpc.makeClientOverProtocol(layerProtocolRpcPortClient(port), ClientServicesRpcs, { disableTracing: true })
|
|
307
|
+
: Rpc.makeClient(port, ClientServicesRpcs, { disableTracing: true })
|
|
308
|
+
).pipe(Effect.map((client) => client as ClientServicesRpc));
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Builds an in-process {@link ClientServicesRpc} backed directly by host {@link ClientServicesHandlers}
|
|
312
|
+
* (no wire hop or serialization). Used by host-internal consumers (diagnostics, devtools) that need
|
|
313
|
+
* the client surface without a transport.
|
|
314
|
+
*/
|
|
315
|
+
export const makeInProcessClientServicesRpc = (
|
|
316
|
+
services: () => Partial<ClientServicesHandlers>,
|
|
317
|
+
): Effect.Effect<ClientServicesRpc, never, Scope.Scope> =>
|
|
318
|
+
RpcTest.makeClient(ClientServicesRpcs).pipe(
|
|
319
|
+
Effect.provide(makeClientServicesHandlers({ services })),
|
|
320
|
+
) as unknown as Effect.Effect<ClientServicesRpc, never, Scope.Scope>;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Derives host {@link ClientServicesHandlers} from an effect-native {@link ClientServicesRpc}, so a
|
|
324
|
+
* client-side rpc surface can be re-served (e.g. the devtools bridge). Each handler delegates to the
|
|
325
|
+
* corresponding client method.
|
|
326
|
+
*/
|
|
327
|
+
export const makeHandlersFromRpc = (rpc: ClientServicesRpc): Partial<ClientServicesHandlers> => {
|
|
328
|
+
const rpcRecord = rpc as unknown as Record<string, Record<string, (...args: any[]) => unknown>>;
|
|
329
|
+
const handlers: Partial<Record<keyof ClientServices, Record<string, unknown>>> = {};
|
|
330
|
+
for (const [tag] of ClientServicesRpcs.requests) {
|
|
331
|
+
const [serviceKey, methodName] = parseTag(tag);
|
|
332
|
+
const service = (handlers[serviceKey] ??= {});
|
|
333
|
+
service[tag] = (payload: unknown) => rpcRecord[serviceKey][methodName](payload);
|
|
334
|
+
}
|
|
335
|
+
return handlers as Partial<ClientServicesHandlers>;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Derives the Promise/{@link PbStream} shaped {@link ClientServices} from an effect-native
|
|
340
|
+
* {@link ClientServicesRpc}. Retained for consumers not yet migrated to the effect surface.
|
|
341
|
+
*/
|
|
342
|
+
export const makeServicesFromRpc = (
|
|
343
|
+
rpc: ClientServicesRpc,
|
|
344
|
+
runtime: Runtime.Runtime<never>,
|
|
345
|
+
): Partial<ClientServices> => {
|
|
346
|
+
// The rpc client is nested by service; methods are addressed dynamically from the rpc groups,
|
|
347
|
+
// so the per-method types cannot be expressed statically.
|
|
348
|
+
const rpcRecord = rpc as unknown as Record<string, Record<string, (...args: any[]) => unknown>>;
|
|
349
|
+
const services: Partial<Record<keyof ClientServices, Record<string, unknown>>> = {};
|
|
350
|
+
for (const [tag, rpcDef] of ClientServicesRpcs.requests) {
|
|
351
|
+
const [serviceKey, methodName] = parseTag(tag);
|
|
352
|
+
const service = (services[serviceKey] ??= {});
|
|
353
|
+
const hasPayload = !isVoidSchema(rpcDef.payloadSchema);
|
|
354
|
+
const invoke = (request?: unknown) => rpcRecord[serviceKey][methodName](hasPayload ? (request ?? {}) : undefined);
|
|
355
|
+
|
|
356
|
+
if (RpcSchema.isStreamSchema(rpcDef.successSchema)) {
|
|
357
|
+
service[methodName] = (request?: unknown) =>
|
|
358
|
+
streamToPbStream(runtime, invoke(request) as Stream.Stream<unknown, unknown>);
|
|
359
|
+
} else {
|
|
360
|
+
service[methodName] = (request?: unknown, options?: RequestOptions) =>
|
|
361
|
+
runServiceCall(runtime, invoke(request) as Effect.Effect<unknown, unknown, never>, {
|
|
362
|
+
timeout: options?.timeout,
|
|
363
|
+
label: tag,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return services as Partial<ClientServices>;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Builds an effect-native {@link ClientServicesRpc} from Promise/{@link PbStream} shaped
|
|
372
|
+
* {@link ClientServices} implementations, without a wire hop. Used by in-process providers
|
|
373
|
+
* (e.g. `LocalClientServices`) so their consumers use the same effect surface as remote proxies.
|
|
374
|
+
*/
|
|
375
|
+
export const makeRpcFromServices = (services: () => Partial<ClientServices>): ClientServicesRpc => {
|
|
376
|
+
const rpc: Record<string, Record<string, (...args: any[]) => unknown>> = {};
|
|
377
|
+
for (const [tag, rpcDef] of ClientServicesRpcs.requests) {
|
|
378
|
+
const [serviceKey, methodName] = parseTag(tag);
|
|
379
|
+
const service = (rpc[serviceKey] ??= {});
|
|
380
|
+
const resolveMethod = () => resolveServiceMethod(services(), serviceKey, methodName, tag);
|
|
381
|
+
|
|
382
|
+
if (RpcSchema.isStreamSchema(rpcDef.successSchema)) {
|
|
383
|
+
service[methodName] = (request?: unknown) =>
|
|
384
|
+
pbStreamToStream(() => resolveMethod()(request) as PbStream<unknown>);
|
|
385
|
+
} else {
|
|
386
|
+
service[methodName] = (request?: unknown) =>
|
|
387
|
+
Effect.tryPromise({ try: async () => resolveMethod()(request), catch: toError });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return rpc as unknown as ClientServicesRpc;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
//
|
|
394
|
+
// Stream adapters.
|
|
395
|
+
//
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Adapts a protobuf service stream to an effect stream.
|
|
399
|
+
* Unbounded buffering matches the push semantics of the source stream.
|
|
400
|
+
*/
|
|
401
|
+
export const pbStreamToStream = <T>(open: () => PbStream<T>): Stream.Stream<T, Error> =>
|
|
402
|
+
Stream.asyncScoped<T, Error>(
|
|
403
|
+
(emit) =>
|
|
404
|
+
Effect.acquireRelease(Effect.try({ try: open, catch: toError }), (stream) =>
|
|
405
|
+
Effect.promise(async () => stream.close()),
|
|
406
|
+
).pipe(
|
|
407
|
+
Effect.tap((stream) =>
|
|
408
|
+
Effect.sync(() => {
|
|
409
|
+
stream.subscribe(
|
|
410
|
+
(data) => void emit.single(data),
|
|
411
|
+
(err) => void (err ? emit.fail(toError(err)) : emit.end()),
|
|
412
|
+
);
|
|
413
|
+
}),
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
'unbounded',
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Adapts an effect stream to a protobuf service stream.
|
|
421
|
+
* Consumer close interrupts the underlying rpc subscription.
|
|
422
|
+
*/
|
|
423
|
+
export const streamToPbStream = <T>(runtime: Runtime.Runtime<never>, stream: Stream.Stream<T, unknown>): PbStream<T> =>
|
|
424
|
+
new PbStream<T>(({ ready, next, close }) => {
|
|
425
|
+
const fiber = stream.pipe(
|
|
426
|
+
Stream.onStart(Effect.sync(ready)),
|
|
427
|
+
Stream.runForEach((item) => Effect.sync(() => next(item))),
|
|
428
|
+
Effect.matchCauseEffect({
|
|
429
|
+
onFailure: (cause) =>
|
|
430
|
+
Effect.sync(() => close(Cause.isInterruptedOnly(cause) ? undefined : toError(Cause.squash(cause)))),
|
|
431
|
+
onSuccess: () => Effect.sync(() => close()),
|
|
432
|
+
}),
|
|
433
|
+
Runtime.runFork(runtime),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
return () => {
|
|
437
|
+
fiber.unsafeInterruptAsFork(fiber.id());
|
|
438
|
+
};
|
|
439
|
+
});
|
package/src/service.ts
CHANGED
|
@@ -8,22 +8,23 @@ import type {
|
|
|
8
8
|
ContactsService,
|
|
9
9
|
DevicesService,
|
|
10
10
|
EdgeAgentService,
|
|
11
|
+
FeedService,
|
|
11
12
|
IdentityService,
|
|
12
13
|
InvitationsService,
|
|
13
14
|
LoggingService,
|
|
14
15
|
NetworkService,
|
|
15
|
-
QueueService,
|
|
16
16
|
SpacesService,
|
|
17
17
|
SystemService,
|
|
18
18
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
19
19
|
import type { DevtoolsHost } from '@dxos/protocols/proto/dxos/devtools/host';
|
|
20
20
|
import type { QueryService } from '@dxos/protocols/proto/dxos/echo/query';
|
|
21
21
|
import type { DataService } from '@dxos/protocols/proto/dxos/echo/service';
|
|
22
|
-
import type { AppService, ShellService
|
|
23
|
-
import type
|
|
24
|
-
import { type ServiceBundle, createServiceBundle } from '@dxos/rpc';
|
|
22
|
+
import type { AppService, ShellService } from '@dxos/protocols/proto/dxos/iframe';
|
|
23
|
+
import { type ServiceBundle } from '@dxos/rpc';
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
import { type ClientServicesRpc } from './service-rpc';
|
|
26
|
+
|
|
27
|
+
export type { FeedService } from '@dxos/protocols/proto/dxos/client/services';
|
|
27
28
|
|
|
28
29
|
//
|
|
29
30
|
// NOTE: Should contain client/proxy dependencies only.
|
|
@@ -41,7 +42,7 @@ export type ClientServices = {
|
|
|
41
42
|
|
|
42
43
|
DataService: DataService;
|
|
43
44
|
QueryService: QueryService;
|
|
44
|
-
|
|
45
|
+
FeedService: FeedService;
|
|
45
46
|
|
|
46
47
|
ContactsService: ContactsService;
|
|
47
48
|
EdgeAgentService: EdgeAgentService;
|
|
@@ -73,7 +74,16 @@ export interface ClientServicesProvider {
|
|
|
73
74
|
*/
|
|
74
75
|
onReconnect?: (callback: () => Promise<void>) => void;
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Effect-native client for all client services, inferred from the effect-rpc definitions.
|
|
79
|
+
* Preferred surface for new consumers; must be re-read after reconnect rather than cached.
|
|
80
|
+
* Effects it produces require only the default runtime and can be run with any `Runtime<never>`.
|
|
81
|
+
*/
|
|
82
|
+
rpc: ClientServicesRpc;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated Prefer {@link rpc}. Promise/`Stream` shaped services derived from {@link rpc}.
|
|
86
|
+
*/
|
|
77
87
|
services: Partial<ClientServices>;
|
|
78
88
|
|
|
79
89
|
// TODO(burdon): Should take context from parent?
|
|
@@ -81,44 +91,6 @@ export interface ClientServicesProvider {
|
|
|
81
91
|
close(): Promise<unknown>;
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
/**
|
|
85
|
-
* Services supported by host.
|
|
86
|
-
*/
|
|
87
|
-
export const clientServiceBundle = createServiceBundle<ClientServices>({
|
|
88
|
-
SystemService: schema.getService('dxos.client.services.SystemService'),
|
|
89
|
-
NetworkService: schema.getService('dxos.client.services.NetworkService'),
|
|
90
|
-
LoggingService: schema.getService('dxos.client.services.LoggingService'),
|
|
91
|
-
|
|
92
|
-
IdentityService: schema.getService('dxos.client.services.IdentityService'),
|
|
93
|
-
QueryService: schema.getService('dxos.echo.query.QueryService'),
|
|
94
|
-
InvitationsService: schema.getService('dxos.client.services.InvitationsService'),
|
|
95
|
-
DevicesService: schema.getService('dxos.client.services.DevicesService'),
|
|
96
|
-
SpacesService: schema.getService('dxos.client.services.SpacesService'),
|
|
97
|
-
DataService: schema.getService('dxos.echo.service.DataService'),
|
|
98
|
-
ContactsService: schema.getService('dxos.client.services.ContactsService'),
|
|
99
|
-
EdgeAgentService: schema.getService('dxos.client.services.EdgeAgentService'),
|
|
100
|
-
QueueService: schema.getService('dxos.client.services.QueueService'),
|
|
101
|
-
|
|
102
|
-
// TODO(burdon): Deprecated.
|
|
103
|
-
DevtoolsHost: schema.getService('dxos.devtools.host.DevtoolsHost'),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
export type IframeServiceBundle = {
|
|
107
|
-
BridgeService: BridgeService;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export const iframeServiceBundle: ServiceBundle<IframeServiceBundle> = {
|
|
111
|
-
BridgeService: schema.getService('dxos.mesh.bridge.BridgeService'),
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export type WorkerServiceBundle = {
|
|
115
|
-
WorkerService: WorkerService;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export const workerServiceBundle: ServiceBundle<WorkerServiceBundle> = {
|
|
119
|
-
WorkerService: schema.getService('dxos.iframe.WorkerService'),
|
|
120
|
-
};
|
|
121
|
-
|
|
122
94
|
export type AppServiceBundle = {
|
|
123
95
|
AppService: AppService;
|
|
124
96
|
};
|
|
@@ -6,12 +6,6 @@ import * as Schema from 'effect/Schema';
|
|
|
6
6
|
|
|
7
7
|
import { Annotation, DXN, Feed, Ref, Type } from '@dxos/echo';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Where do triggers get executed.
|
|
11
|
-
*/
|
|
12
|
-
export const ComputeEnvironment = Schema.Literal('disabled', 'local', 'edge');
|
|
13
|
-
export type ComputeEnvironment = Schema.Schema.Type<typeof ComputeEnvironment>;
|
|
14
|
-
|
|
15
9
|
export const SpacePropertiesSchema = Schema.Struct({
|
|
16
10
|
//
|
|
17
11
|
// System properties.
|
|
@@ -27,12 +21,11 @@ export const SpacePropertiesSchema = Schema.Struct({
|
|
|
27
21
|
invocationTraceFeed: Schema.optional(Ref.Ref(Feed.Feed)),
|
|
28
22
|
|
|
29
23
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* *edge* - triggers are executed on the edge, triggers are not run locally.
|
|
24
|
+
* Space-wide kill-switch for trigger execution.
|
|
25
|
+
* When true, the local trigger dispatcher is stopped for the space (no triggers run locally).
|
|
26
|
+
* Per-trigger local/edge routing is controlled by the trigger's `remote` flag.
|
|
34
27
|
*/
|
|
35
|
-
|
|
28
|
+
triggersDisabled: Schema.optional(Schema.Boolean),
|
|
36
29
|
|
|
37
30
|
//
|
|
38
31
|
// User properties.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// src/types/SpaceProperties.ts
|
|
2
|
-
import * as Schema from "effect/Schema";
|
|
3
|
-
import { Annotation, DXN, Feed, Ref, Type } from "@dxos/echo";
|
|
4
|
-
var ComputeEnvironment = Schema.Literal("disabled", "local", "edge");
|
|
5
|
-
var SpacePropertiesSchema = Schema.Struct({
|
|
6
|
-
//
|
|
7
|
-
// System properties.
|
|
8
|
-
//
|
|
9
|
-
// TODO(burdon): Change to mode (no booleans?)
|
|
10
|
-
// TODO(wittjosiah): Make optional with default value.
|
|
11
|
-
edgeReplication: Schema.optional(Schema.Boolean),
|
|
12
|
-
/**
|
|
13
|
-
* @deprecated
|
|
14
|
-
*/
|
|
15
|
-
invocationTraceFeed: Schema.optional(Ref.Ref(Feed.Feed)),
|
|
16
|
-
/**
|
|
17
|
-
* Preference for trigger execution.
|
|
18
|
-
* *disabled* - triggers do not run locally or on EDGE.
|
|
19
|
-
* *local* - triggers are executed locally on the client, edge is not running triggers.
|
|
20
|
-
* *edge* - triggers are executed on the edge, triggers are not run locally.
|
|
21
|
-
*/
|
|
22
|
-
computeEnvironment: Schema.optional(ComputeEnvironment),
|
|
23
|
-
//
|
|
24
|
-
// User properties.
|
|
25
|
-
//
|
|
26
|
-
name: Schema.optional(Schema.String),
|
|
27
|
-
description: Schema.optional(Schema.String),
|
|
28
|
-
icon: Schema.optional(Schema.String),
|
|
29
|
-
iconHue: Schema.optional(Schema.String),
|
|
30
|
-
hue: Schema.optional(Schema.String)
|
|
31
|
-
});
|
|
32
|
-
var SpaceProperties = class extends Type.makeObject(DXN.make("org.dxos.type.spaceProperties", "0.1.0"))(SpacePropertiesSchema.pipe(Annotation.HiddenAnnotation.set(true))) {
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export {
|
|
36
|
-
ComputeEnvironment,
|
|
37
|
-
SpacePropertiesSchema,
|
|
38
|
-
SpaceProperties
|
|
39
|
-
};
|
|
40
|
-
//# sourceMappingURL=chunk-3SERD2CK.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/types/SpaceProperties.ts"],
|
|
4
|
-
"sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport * as Schema from 'effect/Schema';\n\nimport { Annotation, DXN, Feed, Ref, Type } from '@dxos/echo';\n\n/**\n * Where do triggers get executed.\n */\nexport const ComputeEnvironment = Schema.Literal('disabled', 'local', 'edge');\nexport type ComputeEnvironment = Schema.Schema.Type<typeof ComputeEnvironment>;\n\nexport const SpacePropertiesSchema = Schema.Struct({\n //\n // System properties.\n //\n\n // TODO(burdon): Change to mode (no booleans?)\n // TODO(wittjosiah): Make optional with default value.\n edgeReplication: Schema.optional(Schema.Boolean),\n\n /**\n * @deprecated\n */\n invocationTraceFeed: Schema.optional(Ref.Ref(Feed.Feed)),\n\n /**\n * Preference for trigger execution.\n * *disabled* - triggers do not run locally or on EDGE.\n * *local* - triggers are executed locally on the client, edge is not running triggers.\n * *edge* - triggers are executed on the edge, triggers are not run locally.\n */\n computeEnvironment: Schema.optional(ComputeEnvironment),\n\n //\n // User properties.\n //\n\n name: Schema.optional(Schema.String),\n description: Schema.optional(Schema.String),\n icon: Schema.optional(Schema.String),\n iconHue: Schema.optional(Schema.String),\n hue: Schema.optional(Schema.String),\n});\n\nexport type SpacePropertiesSchema = Schema.Schema.Type<typeof SpacePropertiesSchema>;\n\n// TODO(burdon): Pipe Schem.optional, or partial to entire struct to make everything optional?\n// TODO(burdon): Is separate schema def required for forms? Can it be extracted from SpaceProperties?\nexport class SpaceProperties extends Type.makeObject<SpaceProperties>(\n DXN.make('org.dxos.type.spaceProperties', '0.1.0'),\n)(SpacePropertiesSchema.pipe(Annotation.HiddenAnnotation.set(true))) {}\n"],
|
|
5
|
-
"mappings": ";AAIA,YAAYA,YAAY;AAExB,SAASC,YAAYC,KAAKC,MAAMC,KAAKC,YAAY;AAK1C,IAAMC,qBAA4BC,eAAQ,YAAY,SAAS,MAAA;AAG/D,IAAMC,wBAA+BC,cAAO;;;;;;EAOjDC,iBAAwBC,gBAAgBC,cAAO;;;;EAK/CC,qBAA4BF,gBAASP,IAAIA,IAAID,KAAKA,IAAI,CAAA;;;;;;;EAQtDW,oBAA2BH,gBAASL,kBAAAA;;;;EAMpCS,MAAaJ,gBAAgBK,aAAM;EACnCC,aAAoBN,gBAAgBK,aAAM;EAC1CE,MAAaP,gBAAgBK,aAAM;EACnCG,SAAgBR,gBAAgBK,aAAM;EACtCI,KAAYT,gBAAgBK,aAAM;AACpC,CAAA;AAMO,IAAMK,kBAAN,cAA8BhB,KAAKiB,WACxCpB,IAAIqB,KAAK,iCAAiC,OAAA,CAAA,EAC1Cf,sBAAsBgB,KAAKvB,WAAWwB,iBAAiBC,IAAI,IAAA,CAAA,CAAA,EAAA;AAAS;",
|
|
6
|
-
"names": ["Schema", "Annotation", "DXN", "Feed", "Ref", "Type", "ComputeEnvironment", "Literal", "SpacePropertiesSchema", "Struct", "edgeReplication", "optional", "Boolean", "invocationTraceFeed", "computeEnvironment", "name", "String", "description", "icon", "iconHue", "hue", "SpaceProperties", "makeObject", "make", "pipe", "HiddenAnnotation", "set"]
|
|
7
|
-
}
|