@hydranium/protocol 1.0.0-next.10
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/LICENSE +21 -0
- package/README.md +81 -0
- package/lib/abstract-logger.d.ts +66 -0
- package/lib/abstract-logger.d.ts.map +1 -0
- package/lib/abstract-logger.js +110 -0
- package/lib/abstract-logger.js.map +1 -0
- package/lib/browser-runtime.d.ts +124 -0
- package/lib/browser-runtime.d.ts.map +1 -0
- package/lib/browser-runtime.js +11 -0
- package/lib/browser-runtime.js.map +1 -0
- package/lib/client/data-events.d.ts +49 -0
- package/lib/client/data-events.d.ts.map +1 -0
- package/lib/client/data-events.js +60 -0
- package/lib/client/data-events.js.map +1 -0
- package/lib/client/data-port.d.ts +95 -0
- package/lib/client/data-port.d.ts.map +1 -0
- package/lib/client/data-port.js +11 -0
- package/lib/client/data-port.js.map +1 -0
- package/lib/client/data-session.d.ts +129 -0
- package/lib/client/data-session.d.ts.map +1 -0
- package/lib/client/data-session.js +179 -0
- package/lib/client/data-session.js.map +1 -0
- package/lib/client/index.d.ts +39 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +55 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/message-relay.d.ts +111 -0
- package/lib/client/message-relay.d.ts.map +1 -0
- package/lib/client/message-relay.js +139 -0
- package/lib/client/message-relay.js.map +1 -0
- package/lib/client/post-message-transport.d.ts +77 -0
- package/lib/client/post-message-transport.d.ts.map +1 -0
- package/lib/client/post-message-transport.js +95 -0
- package/lib/client/post-message-transport.js.map +1 -0
- package/lib/clock.d.ts +111 -0
- package/lib/clock.d.ts.map +1 -0
- package/lib/clock.js +74 -0
- package/lib/clock.js.map +1 -0
- package/lib/data/data-protocol-methods.d.ts +39 -0
- package/lib/data/data-protocol-methods.d.ts.map +1 -0
- package/lib/data/data-protocol-methods.js +61 -0
- package/lib/data/data-protocol-methods.js.map +1 -0
- package/lib/data/data-server-protocol.d.ts +264 -0
- package/lib/data/data-server-protocol.d.ts.map +1 -0
- package/lib/data/data-server-protocol.js +11 -0
- package/lib/data/data-server-protocol.js.map +1 -0
- package/lib/data/diagnostics.d.ts +97 -0
- package/lib/data/diagnostics.d.ts.map +1 -0
- package/lib/data/diagnostics.js +25 -0
- package/lib/data/diagnostics.js.map +1 -0
- package/lib/data/events.d.ts +107 -0
- package/lib/data/events.d.ts.map +1 -0
- package/lib/data/events.js +11 -0
- package/lib/data/events.js.map +1 -0
- package/lib/data/index.d.ts +15 -0
- package/lib/data/index.d.ts.map +1 -0
- package/lib/data/index.js +38 -0
- package/lib/data/index.js.map +1 -0
- package/lib/data/methods.d.ts +57 -0
- package/lib/data/methods.d.ts.map +1 -0
- package/lib/data/methods.js +60 -0
- package/lib/data/methods.js.map +1 -0
- package/lib/data/requests.d.ts +65 -0
- package/lib/data/requests.d.ts.map +1 -0
- package/lib/data/requests.js +11 -0
- package/lib/data/requests.js.map +1 -0
- package/lib/debouncer.d.ts +75 -0
- package/lib/debouncer.d.ts.map +1 -0
- package/lib/debouncer.js +91 -0
- package/lib/debouncer.js.map +1 -0
- package/lib/errors.d.ts +84 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +113 -0
- package/lib/errors.js.map +1 -0
- package/lib/host-diagnostics.d.ts +58 -0
- package/lib/host-diagnostics.d.ts.map +1 -0
- package/lib/host-diagnostics.js +18 -0
- package/lib/host-diagnostics.js.map +1 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/latency-collector.d.ts +126 -0
- package/lib/latency-collector.d.ts.map +1 -0
- package/lib/latency-collector.js +137 -0
- package/lib/latency-collector.js.map +1 -0
- package/lib/logger.d.ts +119 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +126 -0
- package/lib/logger.js.map +1 -0
- package/lib/model-server.d.ts +352 -0
- package/lib/model-server.d.ts.map +1 -0
- package/lib/model-server.js +160 -0
- package/lib/model-server.js.map +1 -0
- package/lib/model-service/args.d.ts +64 -0
- package/lib/model-service/args.d.ts.map +1 -0
- package/lib/model-service/args.js +11 -0
- package/lib/model-service/args.js.map +1 -0
- package/lib/model-service/index.d.ts +11 -0
- package/lib/model-service/index.d.ts.map +1 -0
- package/lib/model-service/index.js +31 -0
- package/lib/model-service/index.js.map +1 -0
- package/lib/model-service/reference-candidate.d.ts +56 -0
- package/lib/model-service/reference-candidate.d.ts.map +1 -0
- package/lib/model-service/reference-candidate.js +11 -0
- package/lib/model-service/reference-candidate.js.map +1 -0
- package/lib/noop-logger.d.ts +25 -0
- package/lib/noop-logger.d.ts.map +1 -0
- package/lib/noop-logger.js +36 -0
- package/lib/noop-logger.js.map +1 -0
- package/lib/observable-value.d.ts +72 -0
- package/lib/observable-value.d.ts.map +1 -0
- package/lib/observable-value.js +52 -0
- package/lib/observable-value.js.map +1 -0
- package/lib/patch-merge.d.ts +123 -0
- package/lib/patch-merge.d.ts.map +1 -0
- package/lib/patch-merge.js +104 -0
- package/lib/patch-merge.js.map +1 -0
- package/lib/profile-session.d.ts +87 -0
- package/lib/profile-session.d.ts.map +1 -0
- package/lib/profile-session.js +98 -0
- package/lib/profile-session.js.map +1 -0
- package/lib/profiling.d.ts +83 -0
- package/lib/profiling.d.ts.map +1 -0
- package/lib/profiling.js +20 -0
- package/lib/profiling.js.map +1 -0
- package/lib/project.d.ts +121 -0
- package/lib/project.d.ts.map +1 -0
- package/lib/project.js +34 -0
- package/lib/project.js.map +1 -0
- package/lib/rpc/bind-rpc-methods.d.ts +100 -0
- package/lib/rpc/bind-rpc-methods.d.ts.map +1 -0
- package/lib/rpc/bind-rpc-methods.js +110 -0
- package/lib/rpc/bind-rpc-methods.js.map +1 -0
- package/lib/rpc/create-rpc-proxy.d.ts +188 -0
- package/lib/rpc/create-rpc-proxy.d.ts.map +1 -0
- package/lib/rpc/create-rpc-proxy.js +158 -0
- package/lib/rpc/create-rpc-proxy.js.map +1 -0
- package/lib/rpc/index.d.ts +12 -0
- package/lib/rpc/index.d.ts.map +1 -0
- package/lib/rpc/index.js +31 -0
- package/lib/rpc/index.js.map +1 -0
- package/lib/rpc/wire-prefix.d.ts +37 -0
- package/lib/rpc/wire-prefix.d.ts.map +1 -0
- package/lib/rpc/wire-prefix.js +49 -0
- package/lib/rpc/wire-prefix.js.map +1 -0
- package/lib/testing/data-doubles.d.ts +121 -0
- package/lib/testing/data-doubles.d.ts.map +1 -0
- package/lib/testing/data-doubles.js +102 -0
- package/lib/testing/data-doubles.js.map +1 -0
- package/lib/testing/fake-clock.d.ts +35 -0
- package/lib/testing/fake-clock.d.ts.map +1 -0
- package/lib/testing/fake-clock.js +86 -0
- package/lib/testing/fake-clock.js.map +1 -0
- package/lib/testing/harness.d.ts +24 -0
- package/lib/testing/harness.d.ts.map +1 -0
- package/lib/testing/harness.js +11 -0
- package/lib/testing/harness.js.map +1 -0
- package/lib/testing/index.d.ts +13 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +41 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/node/duplex-connection.d.ts +25 -0
- package/lib/testing/node/duplex-connection.d.ts.map +1 -0
- package/lib/testing/node/duplex-connection.js +34 -0
- package/lib/testing/node/duplex-connection.js.map +1 -0
- package/lib/testing/node/duplex-stream.d.ts +32 -0
- package/lib/testing/node/duplex-stream.d.ts.map +1 -0
- package/lib/testing/node/duplex-stream.js +35 -0
- package/lib/testing/node/duplex-stream.js.map +1 -0
- package/lib/testing/node/index.d.ts +11 -0
- package/lib/testing/node/index.d.ts.map +1 -0
- package/lib/testing/node/index.js +37 -0
- package/lib/testing/node/index.js.map +1 -0
- package/lib/testing/wait-for.d.ts +69 -0
- package/lib/testing/wait-for.d.ts.map +1 -0
- package/lib/testing/wait-for.js +67 -0
- package/lib/testing/wait-for.js.map +1 -0
- package/lib/tracer.d.ts +115 -0
- package/lib/tracer.d.ts.map +1 -0
- package/lib/tracer.js +205 -0
- package/lib/tracer.js.map +1 -0
- package/lib/transfer-diagnostic.d.ts +73 -0
- package/lib/transfer-diagnostic.d.ts.map +1 -0
- package/lib/transfer-diagnostic.js +45 -0
- package/lib/transfer-diagnostic.js.map +1 -0
- package/lib/transfer-document.d.ts +73 -0
- package/lib/transfer-document.d.ts.map +1 -0
- package/lib/transfer-document.js +46 -0
- package/lib/transfer-document.js.map +1 -0
- package/lib/transfer-element.d.ts +39 -0
- package/lib/transfer-element.d.ts.map +1 -0
- package/lib/transfer-element.js +11 -0
- package/lib/transfer-element.js.map +1 -0
- package/lib/uri.d.ts +62 -0
- package/lib/uri.d.ts.map +1 -0
- package/lib/uri.js +28 -0
- package/lib/uri.js.map +1 -0
- package/lib/util.d.ts +147 -0
- package/lib/util.d.ts.map +1 -0
- package/lib/util.js +211 -0
- package/lib/util.js.map +1 -0
- package/package.json +100 -0
- package/src/abstract-logger.ts +120 -0
- package/src/browser-runtime.ts +135 -0
- package/src/client/data-events.ts +71 -0
- package/src/client/data-port.ts +99 -0
- package/src/client/data-session.ts +209 -0
- package/src/client/index.ts +40 -0
- package/src/client/message-relay.ts +216 -0
- package/src/client/post-message-transport.ts +167 -0
- package/src/clock.ts +173 -0
- package/src/data/data-protocol-methods.ts +79 -0
- package/src/data/data-server-protocol.ts +296 -0
- package/src/data/diagnostics.ts +116 -0
- package/src/data/events.ts +127 -0
- package/src/data/index.ts +23 -0
- package/src/data/methods.ts +59 -0
- package/src/data/requests.ts +70 -0
- package/src/debouncer.ts +118 -0
- package/src/errors.ts +127 -0
- package/src/host-diagnostics.ts +63 -0
- package/src/index.ts +41 -0
- package/src/latency-collector.ts +231 -0
- package/src/logger.ts +172 -0
- package/src/model-server.ts +479 -0
- package/src/model-service/args.ts +67 -0
- package/src/model-service/index.ts +16 -0
- package/src/model-service/reference-candidate.ts +58 -0
- package/src/noop-logger.ts +36 -0
- package/src/observable-value.ts +92 -0
- package/src/patch-merge.ts +165 -0
- package/src/profile-session.ts +164 -0
- package/src/profiling.ts +113 -0
- package/src/project.ts +125 -0
- package/src/rpc/README.md +163 -0
- package/src/rpc/bind-rpc-methods.ts +180 -0
- package/src/rpc/create-rpc-proxy.ts +292 -0
- package/src/rpc/index.ts +16 -0
- package/src/rpc/wire-prefix.ts +48 -0
- package/src/testing/data-doubles.ts +178 -0
- package/src/testing/fake-clock.ts +106 -0
- package/src/testing/harness.ts +24 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/node/duplex-connection.ts +50 -0
- package/src/testing/node/duplex-stream.ts +51 -0
- package/src/testing/node/index.ts +22 -0
- package/src/testing/wait-for.ts +94 -0
- package/src/tracer.ts +277 -0
- package/src/transfer-diagnostic.ts +92 -0
- package/src/transfer-document.ts +99 -0
- package/src/transfer-element.ts +43 -0
- package/src/uri.ts +61 -0
- package/src/util.ts +250 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# JSON-RPC primitives
|
|
2
|
+
|
|
3
|
+
Exported from the package root, `@hydranium/protocol`. There is no
|
|
4
|
+
`@hydranium/protocol/rpc` subpath: the package's `exports` map publishes `.`,
|
|
5
|
+
`./client`, `./data` and `./testing` (plus their `./lib/*` twins), so importing
|
|
6
|
+
this directory by path fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
|
|
7
|
+
|
|
8
|
+
Generic JSON-RPC primitives for typed protocol heads over a vscode-jsonrpc
|
|
9
|
+
`MessageConnection`. This page is the reference; the shape of the pattern and
|
|
10
|
+
why the two helpers mirror each other are in the
|
|
11
|
+
[package README](../../README.md#the-rpc-pattern).
|
|
12
|
+
|
|
13
|
+
## The pair
|
|
14
|
+
|
|
15
|
+
### `bindRpcMethods(connection, target, methodNames, options?)`
|
|
16
|
+
|
|
17
|
+
Server side. Registers each method named in `methodNames` on `target` as a
|
|
18
|
+
handler on `connection` under the wire name `<methodNamespace><methodName>`.
|
|
19
|
+
Notification methods (by the configured heuristic) register with
|
|
20
|
+
`onNotification`; request methods register with `onRequest`. Returns a
|
|
21
|
+
`Disposable` that tears down every registration.
|
|
22
|
+
|
|
23
|
+
### `createRpcProxy<T>(connection, options?)`
|
|
24
|
+
|
|
25
|
+
Caller side. Returns a JS `Proxy` typed as `T & RpcProxyLifecycle`. Each
|
|
26
|
+
property access lowers to `sendRequest` (request methods) or
|
|
27
|
+
`sendNotification` (notification methods, by the same heuristic) under the
|
|
28
|
+
same wire-name composition. The lifecycle events (`onDidOpenConnection` /
|
|
29
|
+
`onDidCloseConnection`) let proxy-only consumers react to the connection
|
|
30
|
+
without holding the `MessageConnection` reference directly.
|
|
31
|
+
|
|
32
|
+
## Shared semantics
|
|
33
|
+
|
|
34
|
+
The options are deliberately mirrored across both helpers. Server and client
|
|
35
|
+
MUST agree on each — a mismatch produces silent routing failures.
|
|
36
|
+
|
|
37
|
+
### Wire-name composition: `methodNamespace`
|
|
38
|
+
|
|
39
|
+
Both helpers compose the wire method name as
|
|
40
|
+
`(methodNamespace ?? '') + methodName`. Adopters that share a connection
|
|
41
|
+
across multiple protocol heads partition the wire surface with namespaced
|
|
42
|
+
prefixes — the LSP analogy is `textDocument/*` / `workspace/*`. The
|
|
43
|
+
data-server head ships with `'data-server/'` by default; adopters that
|
|
44
|
+
combine the data-server with their own protocol head under one prefix pass
|
|
45
|
+
their adopter namespace (e.g. `'myapp/'`) on both sides.
|
|
46
|
+
|
|
47
|
+
Trailing-slash discipline is the adopter's responsibility — `'foo'` is a
|
|
48
|
+
literal prefix, not interpreted as a namespace segment.
|
|
49
|
+
|
|
50
|
+
### Notification discrimination: `isNotification`
|
|
51
|
+
|
|
52
|
+
Default: property names starting with `'on'` followed by an uppercase letter
|
|
53
|
+
are notifications. That matches the observer-pattern convention used
|
|
54
|
+
throughout the framework's contracts (`onDocumentUpdated`, `onDocumentSaved`,
|
|
55
|
+
etc.), while leaving request-shaped names that merely begin with the letters
|
|
56
|
+
"on" (`onboardUser`, `onlineCheck`) routed as requests.
|
|
57
|
+
|
|
58
|
+
For contracts that don't fit the convention, both helpers accept an
|
|
59
|
+
`isNotification: (name: string) => boolean` override. Pass the same
|
|
60
|
+
predicate on both sides — they must agree on every method, otherwise one
|
|
61
|
+
end will register a request handler while the other sends a notification
|
|
62
|
+
(no reply, silent timeout).
|
|
63
|
+
|
|
64
|
+
### Single-arg-per-method convention
|
|
65
|
+
|
|
66
|
+
Method dispatch passes JSON-RPC `params` to the target as a single argument.
|
|
67
|
+
This is the framework's convention throughout — adopters define methods as
|
|
68
|
+
`doStuff(args: { x, y })`, not `doStuff(x, y)`. The proxy enforces the
|
|
69
|
+
convention at runtime: calling `proxy.doStuff(a, b)` with more than one
|
|
70
|
+
argument throws (loose `any` / `unknown` proxies would otherwise drop extra
|
|
71
|
+
args silently).
|
|
72
|
+
|
|
73
|
+
### Deferred connection
|
|
74
|
+
|
|
75
|
+
Both helpers accept `MessageConnection | Promise<MessageConnection>`. When
|
|
76
|
+
the promise form is passed:
|
|
77
|
+
|
|
78
|
+
- `createRpcProxy` — outgoing calls queue until the connection resolves,
|
|
79
|
+
then dispatch in order.
|
|
80
|
+
- `bindRpcMethods` — handler registration queues until the connection
|
|
81
|
+
resolves, then attaches in one pass. The returned disposable cancels
|
|
82
|
+
queued work if disposed before resolve; otherwise it tears down the
|
|
83
|
+
attached handlers as usual.
|
|
84
|
+
|
|
85
|
+
Wire-side safety: vscode-jsonrpc buffers nothing on the *receiving* side
|
|
86
|
+
until `connection.listen()` is called, so adopters wiring handlers in
|
|
87
|
+
`@postConstruct` (before the connection's other end opens) cannot drop a
|
|
88
|
+
message that arrives during the queue window.
|
|
89
|
+
|
|
90
|
+
## Reserved property names on `RpcProxy<T>`
|
|
91
|
+
|
|
92
|
+
The proxy's `get` trap intercepts four property names. A wire method
|
|
93
|
+
declared on `T` with one of these names would shadow the reserved
|
|
94
|
+
behaviour instead of dispatching a wire call. Adopters defining wire
|
|
95
|
+
methods should avoid:
|
|
96
|
+
|
|
97
|
+
- `onDidOpenConnection` / `onDidCloseConnection` — return the lifecycle events
|
|
98
|
+
- `then` — returns `undefined` so the proxy isn't auto-awaited by host-environment promise detection
|
|
99
|
+
- `toJSON` — returns `undefined` so serialisers don't try to flatten the proxy
|
|
100
|
+
|
|
101
|
+
Symbol property accesses also return `undefined` (the proxy is not
|
|
102
|
+
iterable, not a thenable, not serialisable).
|
|
103
|
+
|
|
104
|
+
## Example
|
|
105
|
+
|
|
106
|
+
A minimal adopter contract paired across the wire:
|
|
107
|
+
|
|
108
|
+
<!-- snippet-preamble
|
|
109
|
+
import { bindRpcMethods, createRpcProxy } from '@hydranium/protocol';
|
|
110
|
+
import type { MessageConnection } from 'vscode-jsonrpc';
|
|
111
|
+
declare const connection: MessageConnection;
|
|
112
|
+
-->
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
interface CounterApi {
|
|
116
|
+
increment(args: { by: number }): Promise<{ value: number }>;
|
|
117
|
+
onChanged(event: { value: number }): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Server side — adopter implements the contract on a class:
|
|
121
|
+
class CounterServer implements CounterApi {
|
|
122
|
+
private value = 0;
|
|
123
|
+
async increment(args: { by: number }): Promise<{ value: number }> {
|
|
124
|
+
this.value += args.by;
|
|
125
|
+
return { value: this.value };
|
|
126
|
+
}
|
|
127
|
+
onChanged(): void {
|
|
128
|
+
throw new Error('outbound notification — never called on the server');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const server = new CounterServer();
|
|
133
|
+
const disposable = bindRpcMethods<CounterApi>(
|
|
134
|
+
connection,
|
|
135
|
+
server,
|
|
136
|
+
['increment', 'onChanged'],
|
|
137
|
+
{ methodNamespace: 'counter/' }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Caller side — typed proxy over the same wire prefix:
|
|
141
|
+
const proxy = createRpcProxy<CounterApi>(connection, { methodNamespace: 'counter/' });
|
|
142
|
+
const { value } = await proxy.increment({ by: 3 });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The data-server head is built from these same two primitives rather than from
|
|
146
|
+
a data-specific wrapper: `DataServer` binds its own contract and its client
|
|
147
|
+
proxy in a single `createRpcProxy` call, over the method-name lists
|
|
148
|
+
`@hydranium/protocol/data` publishes (`DATA_SERVER_PROTOCOL_METHODS` and its
|
|
149
|
+
client dual). An adopter composing a head of their own follows the same shape.
|
|
150
|
+
|
|
151
|
+
## Relationship to other RPC libraries
|
|
152
|
+
|
|
153
|
+
- **vscode-jsonrpc directly.** These helpers are a thin typed shell over
|
|
154
|
+
`connection.sendRequest` / `onRequest` / `sendNotification` /
|
|
155
|
+
`onNotification`. Adopters wanting to opt out for one specific method
|
|
156
|
+
can always reach the connection underneath.
|
|
157
|
+
- **LSP `RequestType` / `NotificationType` keys.** LSP-style codebases
|
|
158
|
+
declare each wire method as a typed symbol; this framework declares the
|
|
159
|
+
whole protocol as an interface and walks its keys. Interface-based is
|
|
160
|
+
the right call for a *generic* framework (one declaration, N adopters);
|
|
161
|
+
adopters coming from LSP looking for `RequestType` helpers won't find
|
|
162
|
+
them — the equivalent here is the contract interface plus its
|
|
163
|
+
method-name list (e.g. `DATA_SERVER_PROTOCOL_METHODS`).
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import type { MessageConnection } from 'vscode-jsonrpc';
|
|
11
|
+
import type { LatencyCollector } from '../latency-collector';
|
|
12
|
+
import { type Disposable, DisposableCollection } from '../util';
|
|
13
|
+
import { defaultIsNotification } from './create-rpc-proxy';
|
|
14
|
+
import { assertValidMethodNamespace } from './wire-prefix';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options controlling how method names are translated into wire-method
|
|
18
|
+
* registrations on the supplied connection. The defaults match
|
|
19
|
+
* `createRpcProxy` so the two helpers compose cleanly: an adopter
|
|
20
|
+
* defining a contract `T` registers handlers with `bindRpcMethods(conn, target, names, opts)`
|
|
21
|
+
* on one side and a typed proxy with `createRpcProxy<T>(conn, opts)` on
|
|
22
|
+
* the other — same wire prefix, same notification heuristic, no drift.
|
|
23
|
+
*/
|
|
24
|
+
export interface BindRpcMethodsOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Prefix prepended to each method name to form the JSON-RPC wire string.
|
|
27
|
+
* Defaults to the empty string.
|
|
28
|
+
*/
|
|
29
|
+
readonly methodNamespace?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Predicate to discriminate notification methods from request methods.
|
|
33
|
+
* Notification methods register with `connection.onNotification`;
|
|
34
|
+
* request methods register with `connection.onRequest`. Default: `on`
|
|
35
|
+
* followed by an uppercase letter marks a notification, matching
|
|
36
|
+
* `createRpcProxy`.
|
|
37
|
+
*/
|
|
38
|
+
readonly isNotification?: (methodName: string) => boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* When `true` (the default), every name in `methodNames` MUST exist
|
|
42
|
+
* as a function on `target` — missing names throw at attach time. This
|
|
43
|
+
* catches typos in adopter-supplied `additionalMethods` arrays where
|
|
44
|
+
* the names are string literals.
|
|
45
|
+
*
|
|
46
|
+
* Set to `false` to fall back to the silent-skip behaviour (useful
|
|
47
|
+
* for transitional method-name lists where some methods are not yet
|
|
48
|
+
* implemented on every target).
|
|
49
|
+
*/
|
|
50
|
+
readonly requireAll?: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Invoked when a notification handler throws. Notifications have no reply
|
|
54
|
+
* channel, so the error cannot propagate back to the caller; this hook
|
|
55
|
+
* lets a caller that holds a logger route the failure somewhere
|
|
56
|
+
* structured. Receives the wire-method name and the thrown value.
|
|
57
|
+
*
|
|
58
|
+
* Defaults to `console.error` — the protocol layer carries no logger of
|
|
59
|
+
* its own, and a caller in a process whose stdout is not the JSON-RPC
|
|
60
|
+
* transport (the framework's IPC / socket heads) can leave the default in
|
|
61
|
+
* place. Callers binding handlers over a `--stdio` LSP connection should
|
|
62
|
+
* pass a hook that routes through `connection.console` instead.
|
|
63
|
+
*/
|
|
64
|
+
readonly onNotificationError?: (wireName: string, error: unknown) => void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* When supplied, every dispatched handler is timed into this collector under
|
|
68
|
+
* its wire name (`<prefix><methodName>`) — the single RPC chokepoint the
|
|
69
|
+
* latency/throughput surface hooks, so no per-handler change is needed.
|
|
70
|
+
* Requests and notifications are both timed. Absent by default (no overhead).
|
|
71
|
+
*/
|
|
72
|
+
readonly latency?: LatencyCollector;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register each named method on `target` as a handler on `connection`
|
|
77
|
+
* under the wire-name `<prefix><methodName>`. Notification methods (by
|
|
78
|
+
* default `on` + an uppercase letter) register as fire-and-forget listeners; request
|
|
79
|
+
* methods register as request handlers and propagate the method's return
|
|
80
|
+
* value back to the caller.
|
|
81
|
+
*
|
|
82
|
+
* The returned disposable tears down every registered handler in one call —
|
|
83
|
+
* sufficient for the framework's lifecycle: bind handlers at construction,
|
|
84
|
+
* dispose at shutdown.
|
|
85
|
+
*
|
|
86
|
+
* Method dispatch always passes the JSON-RPC `params` to the target
|
|
87
|
+
* method as a single argument. This matches the single-arg-per-method
|
|
88
|
+
* convention used throughout the framework's typed contracts — adopters
|
|
89
|
+
* defining methods with multiple positional parameters would need a
|
|
90
|
+
* different binder.
|
|
91
|
+
*
|
|
92
|
+
* Errors thrown synchronously from a request handler — or surfaced as a
|
|
93
|
+
* rejected promise — propagate back to the caller through vscode-jsonrpc's
|
|
94
|
+
* standard error envelope. Errors from a notification handler cannot, and are
|
|
95
|
+
* routed to {@link BindRpcMethodsOptions.onNotificationError} instead.
|
|
96
|
+
*
|
|
97
|
+
* Accepts either a ready connection or a `Promise<MessageConnection>` —
|
|
98
|
+
* registrations queue until the connection resolves, then attach. The
|
|
99
|
+
* returned disposable can be invoked at any time: if it fires before the
|
|
100
|
+
* connection resolves the queued work is cancelled and never attaches;
|
|
101
|
+
* if it fires afterwards the registrations dispose normally. Mirrors
|
|
102
|
+
* `createRpcProxy`'s deferred-connection pattern so adopters can wire
|
|
103
|
+
* inbound handlers in `@postConstruct` before the underlying transport
|
|
104
|
+
* exists. Wire-side safety: no notifications can arrive before
|
|
105
|
+
* `connection.listen()` runs, so a not-yet-attached handler cannot drop
|
|
106
|
+
* a real message.
|
|
107
|
+
*/
|
|
108
|
+
export function bindRpcMethods<T extends object>(
|
|
109
|
+
connection: MessageConnection | Promise<MessageConnection>,
|
|
110
|
+
target: T,
|
|
111
|
+
methodNames: readonly (keyof T & string)[],
|
|
112
|
+
options: BindRpcMethodsOptions = {}
|
|
113
|
+
): Disposable {
|
|
114
|
+
const methodNamespace = options.methodNamespace ?? '';
|
|
115
|
+
assertValidMethodNamespace(methodNamespace, 'bindRpcMethods');
|
|
116
|
+
const isNotification = options.isNotification ?? defaultIsNotification;
|
|
117
|
+
const requireAll = options.requireAll ?? true;
|
|
118
|
+
const onNotificationError =
|
|
119
|
+
options.onNotificationError ??
|
|
120
|
+
((wireName: string, error: unknown) => console.error(`[bindRpcMethods] notification handler '${wireName}' threw:`, error));
|
|
121
|
+
const disposables = new DisposableCollection();
|
|
122
|
+
let cancelled = false;
|
|
123
|
+
|
|
124
|
+
const attach = (resolved: MessageConnection): void => {
|
|
125
|
+
if (cancelled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
for (const methodName of methodNames) {
|
|
129
|
+
const wireName = methodNamespace + methodName;
|
|
130
|
+
const method = target[methodName];
|
|
131
|
+
if (typeof method !== 'function') {
|
|
132
|
+
if (requireAll) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`bindRpcMethods: method '${methodName}' is not a function on the target. ` +
|
|
135
|
+
`Either fix the typo, implement the method, or pass { requireAll: false } to opt into silent-skip ` +
|
|
136
|
+
'for transitional method-name lists.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const bound = (method as (params: unknown) => unknown).bind(target);
|
|
142
|
+
// Time the dispatch at this one chokepoint when a collector is present.
|
|
143
|
+
const latency = options.latency;
|
|
144
|
+
const dispatch = latency ? (params: unknown): unknown => latency.time(wireName, () => bound(params)) : bound;
|
|
145
|
+
|
|
146
|
+
if (isNotification(methodName)) {
|
|
147
|
+
disposables.push(
|
|
148
|
+
resolved.onNotification(wireName, (params: unknown) => {
|
|
149
|
+
try {
|
|
150
|
+
dispatch(params);
|
|
151
|
+
} catch (err: unknown) {
|
|
152
|
+
onNotificationError(wireName, err);
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
disposables.push(resolved.onRequest(wireName, async (params: unknown) => dispatch(params)));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (connection instanceof Promise) {
|
|
163
|
+
void connection
|
|
164
|
+
.then(resolved => attach(resolved))
|
|
165
|
+
.catch(() => {
|
|
166
|
+
// Connection promise rejected — treat as never-resolved; nothing
|
|
167
|
+
// to register. Adopters observe transport failures through their
|
|
168
|
+
// own connection-construction error handling.
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
attach(connection);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
dispose(): void {
|
|
176
|
+
cancelled = true;
|
|
177
|
+
disposables.dispose();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { Emitter, type Event, type MessageConnection } from 'vscode-jsonrpc';
|
|
11
|
+
import { type BindRpcMethodsOptions, bindRpcMethods } from './bind-rpc-methods';
|
|
12
|
+
import { assertValidMethodNamespace } from './wire-prefix';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lifecycle events surfaced on every {@link createRpcProxy} return — `onDidOpenConnection`
|
|
16
|
+
* fires once after the underlying `MessageConnection` resolves; `onDidCloseConnection`
|
|
17
|
+
* fires when the connection's `onClose` triggers. Useful for proxy-only consumers
|
|
18
|
+
* (widgets, downstream services) that don't hold the `MessageConnection` directly
|
|
19
|
+
* and would otherwise need it threaded through just to react to disconnects.
|
|
20
|
+
*
|
|
21
|
+
* Reconnection is NOT modelled — the framework assumes a single connection
|
|
22
|
+
* lifetime per proxy. If the transport drops, the adopter constructs a new
|
|
23
|
+
* connection + proxy.
|
|
24
|
+
*
|
|
25
|
+
* **Reserved property names on `RpcProxy<T>`.** The proxy's get-trap
|
|
26
|
+
* intercepts four property names — any wire method declared on `T` with
|
|
27
|
+
* one of these names would shadow the reserved behaviour instead of
|
|
28
|
+
* dispatching a wire call. Adopters defining wire-method names should
|
|
29
|
+
* avoid:
|
|
30
|
+
*
|
|
31
|
+
* - `onDidOpenConnection` / `onDidCloseConnection` — return the
|
|
32
|
+
* lifecycle events declared on this interface.
|
|
33
|
+
* - `then` — returns `undefined` so the proxy is not auto-awaited
|
|
34
|
+
* when caught by Promise-detection in the host environment.
|
|
35
|
+
* - `toJSON` — returns `undefined` so JSON serializers do not try to
|
|
36
|
+
* flatten the proxy.
|
|
37
|
+
*
|
|
38
|
+
* Symbol property accesses also return `undefined` (the proxy is not
|
|
39
|
+
* iterable, not a thenable, not serialisable).
|
|
40
|
+
*/
|
|
41
|
+
export interface RpcProxyLifecycle {
|
|
42
|
+
/**
|
|
43
|
+
* Fires exactly once, when the underlying connection promise resolves. It
|
|
44
|
+
* does NOT replay: a proxy built over an already-resolved connection fires
|
|
45
|
+
* on the next microtask, so a subscriber attached after that never hears
|
|
46
|
+
* anything. Subscribe in the same synchronous block that builds the proxy,
|
|
47
|
+
* or track readiness yourself.
|
|
48
|
+
*
|
|
49
|
+
* Never fires if the connection promise rejects — a failed transport
|
|
50
|
+
* construction is indistinguishable here from one still pending, and is
|
|
51
|
+
* meant to be observed where the connection is built.
|
|
52
|
+
*/
|
|
53
|
+
readonly onDidOpenConnection: Event<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Fires when the transport closes. Reconnection is not modelled, so it fires
|
|
56
|
+
* at most once and is never followed by another open — a consumer that has
|
|
57
|
+
* to survive a drop constructs a new proxy rather than waiting here.
|
|
58
|
+
*/
|
|
59
|
+
readonly onDidCloseConnection: Event<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Proxy of `T` plus the framework's connection-lifecycle events. */
|
|
63
|
+
export type RpcProxy<T extends object> = T & RpcProxyLifecycle;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Options controlling how method-name properties on the returned proxy
|
|
67
|
+
* are translated into wire calls.
|
|
68
|
+
*
|
|
69
|
+
* Generic over `TLocal` — the type of an OPTIONAL inbound handler target
|
|
70
|
+
* (see {@link localTarget} / {@link localMethods}). `TLocal` defaults to
|
|
71
|
+
* `never`, so callers that only want an outbound proxy omit the local
|
|
72
|
+
* fields entirely and the proxy behaves exactly as a pure remote proxy.
|
|
73
|
+
*/
|
|
74
|
+
export interface CreateRpcProxyOptions<TLocal extends object = never> {
|
|
75
|
+
/**
|
|
76
|
+
* Prefix prepended to the property name to form the JSON-RPC method
|
|
77
|
+
* string. For example, prefix `'data-server/'` turns a property access
|
|
78
|
+
* `proxy.getModelDocument` into the wire method
|
|
79
|
+
* `'data-server/getModelDocument'`. Defaults to the empty string —
|
|
80
|
+
* the property name is the wire name.
|
|
81
|
+
*
|
|
82
|
+
* When {@link localTarget} is supplied, the SAME prefix applies to the
|
|
83
|
+
* inbound handler registrations — both ends of a combined connection
|
|
84
|
+
* share one namespace.
|
|
85
|
+
*/
|
|
86
|
+
readonly methodNamespace?: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Predicate to discriminate notification methods from request methods
|
|
90
|
+
* by their TS property name. Notification methods lower to
|
|
91
|
+
* `connection.sendNotification` and return `void`; request methods
|
|
92
|
+
* lower to `connection.sendRequest` and return `Promise<TResult>`.
|
|
93
|
+
*
|
|
94
|
+
* Default: property names starting with `'on'` followed by an uppercase
|
|
95
|
+
* letter are notifications. Adopters defining typed contracts in the
|
|
96
|
+
* `DataClientProtocol` style get the right routing without per-method
|
|
97
|
+
* configuration.
|
|
98
|
+
*
|
|
99
|
+
* Applies symmetrically to the {@link localTarget} binding, so the
|
|
100
|
+
* inbound and outbound sides agree on which methods are notifications.
|
|
101
|
+
*/
|
|
102
|
+
readonly isNotification?: (methodName: string) => boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* OPTIONAL inbound-handler target. When supplied together with
|
|
106
|
+
* {@link localMethods}, `createRpcProxy` ALSO binds those methods of
|
|
107
|
+
* `localTarget` as inbound request/notification handlers on the same
|
|
108
|
+
* connection (delegating to `bindRpcMethods`) — collapsing the common
|
|
109
|
+
* "proxy the remote + handle the local" two-step into one call. The
|
|
110
|
+
* binding is torn down on `connection.onClose`; the return value stays
|
|
111
|
+
* just the remote proxy (no `Disposable` surfaced — there is no use
|
|
112
|
+
* case for unbinding a connection-scoped target before its connection
|
|
113
|
+
* closes). Callers that need the explicit `Disposable` call
|
|
114
|
+
* `bindRpcMethods` directly instead.
|
|
115
|
+
*
|
|
116
|
+
* Omit (the `TLocal = never` default) for a pure outbound proxy.
|
|
117
|
+
*/
|
|
118
|
+
readonly localTarget?: TLocal;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Method names of {@link localTarget} to bind as inbound handlers.
|
|
122
|
+
* REQUIRED for the binding to happen (no auto-enumeration — TypeScript
|
|
123
|
+
* access modifiers are erased at runtime, so a class instance's
|
|
124
|
+
* prototype carries internal helpers that must not become wire
|
|
125
|
+
* endpoints; the explicit list is the allowlist). Declare it
|
|
126
|
+
* `as const satisfies keyof TLocal` at the call site for a compile-time
|
|
127
|
+
* drift check; `bindRpcMethods` additionally verifies each name exists
|
|
128
|
+
* on the target at attach time.
|
|
129
|
+
*/
|
|
130
|
+
readonly localMethods?: readonly (keyof TLocal & string)[];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Forwarded to the inbound {@link localTarget} binding: when supplied, every
|
|
134
|
+
* inbound handler is timed into this collector under its wire name. Lets a
|
|
135
|
+
* head that owns its server through `createRpcProxy` (rather than a bare
|
|
136
|
+
* `bindRpcMethods` call) still capture per-method latency. Absent by default.
|
|
137
|
+
*/
|
|
138
|
+
readonly latency?: BindRpcMethodsOptions['latency'];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Default notification discriminator — `on`-followed-by-an-uppercase-letter
|
|
143
|
+
* methods are notifications (`onProgress`, `onDocumentUpdated`).
|
|
144
|
+
*
|
|
145
|
+
* The uppercase requirement is what keeps the heuristic honest: a
|
|
146
|
+
* request-shaped method that merely *starts* with the letters "on"
|
|
147
|
+
* (`onboardUser`, `onlineCheck`) stays a request, instead of being misrouted
|
|
148
|
+
* as a fire-and-forget notification with its `Promise` result silently
|
|
149
|
+
* dropped. It follows the observer-callback convention the framework's
|
|
150
|
+
* contracts already use, where the capital always marks the event name.
|
|
151
|
+
*
|
|
152
|
+
* Still lexical, so it cannot catch everything — `onDemandRebuild` reads as a
|
|
153
|
+
* request but matches. Supply a custom
|
|
154
|
+
* {@link CreateRpcProxyOptions.isNotification} for contracts that don't fit
|
|
155
|
+
* the convention, and pass the same predicate to both ends.
|
|
156
|
+
*/
|
|
157
|
+
export function defaultIsNotification(methodName: string): boolean {
|
|
158
|
+
return /^on[A-Z]/.test(methodName);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Runtime enforcement of the single-arg convention. TypeScript catches
|
|
163
|
+
* misuse at compile time for typed contract callers, but loosely-typed
|
|
164
|
+
* callsites (`any` / `unknown` proxy) would silently drop extra args.
|
|
165
|
+
* Throw loudly so the violation is visible.
|
|
166
|
+
*/
|
|
167
|
+
function assertSingleArg(wireName: string, args: unknown[]): void {
|
|
168
|
+
if (args.length > 1) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`RPC method '${wireName}' called with ${args.length} arguments — typed contracts use a single params object. ` +
|
|
171
|
+
'Pass a single object instead: proxy.foo({ ...args }).'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build a typed RPC proxy `T` over a vscode-jsonrpc {@link MessageConnection}.
|
|
178
|
+
* Every method access on the returned object lowers transparently to
|
|
179
|
+
* `connection.sendRequest` (request methods) or `connection.sendNotification`
|
|
180
|
+
* (notification methods, by default `on*`-prefixed). The single-arg
|
|
181
|
+
* payload shape is preserved: `proxy.foo(args)` sends `(method, args)`
|
|
182
|
+
* over the wire and resolves with the response.
|
|
183
|
+
*
|
|
184
|
+
* `on` + an uppercase letter is the only notification marker — see
|
|
185
|
+
* {@link defaultIsNotification}, and pass a custom `isNotification` to both
|
|
186
|
+
* ends for a contract that doesn't fit.
|
|
187
|
+
*
|
|
188
|
+
* **Wire tracing.** The proxy adds no tracing layer of its own, deliberately:
|
|
189
|
+
* vscode-jsonrpc's own `connection.trace` already covers wire method names,
|
|
190
|
+
* params, results and errors, and a second layer here would double every
|
|
191
|
+
* traced line.
|
|
192
|
+
*
|
|
193
|
+
* Accepts either a ready connection or a `Promise<MessageConnection>` —
|
|
194
|
+
* proxy methods called before the promise resolves queue until it does,
|
|
195
|
+
* then dispatch, so adopters can wire the proxy before its underlying
|
|
196
|
+
* transport is available (e.g. before the Langium services finish
|
|
197
|
+
* constructing).
|
|
198
|
+
*
|
|
199
|
+
* A few property names are intercepted rather than dispatched — see
|
|
200
|
+
* {@link RpcProxyLifecycle} for the reserved list and why each is guarded.
|
|
201
|
+
*
|
|
202
|
+
* Each method of `T` dispatches under `<methodNamespace><methodName>`: a
|
|
203
|
+
* request method resolves through `connection.sendRequest`, while one the
|
|
204
|
+
* notification heuristic matches goes out through `sendNotification` and
|
|
205
|
+
* returns nothing, so a caller that awaits it waits on `undefined` rather
|
|
206
|
+
* than on delivery.
|
|
207
|
+
*
|
|
208
|
+
* **Combined proxy + inbound binding.** Supply `localTarget` + `localMethods`
|
|
209
|
+
* to ALSO register inbound handlers on the same connection in one call —
|
|
210
|
+
* the typical both-ends-of-a-bidirectional-connection setup. The remote
|
|
211
|
+
* proxy is still the return value; the inbound binding tears down on
|
|
212
|
+
* `connection.onClose`, so binding twice over one connection leaks the
|
|
213
|
+
* first set of handlers until it closes.
|
|
214
|
+
*/
|
|
215
|
+
export function createRpcProxy<T extends object, TLocal extends object = never>(
|
|
216
|
+
connection: MessageConnection | Promise<MessageConnection>,
|
|
217
|
+
options: CreateRpcProxyOptions<TLocal> = {}
|
|
218
|
+
): RpcProxy<T> {
|
|
219
|
+
const methodNamespace = options.methodNamespace ?? '';
|
|
220
|
+
assertValidMethodNamespace(methodNamespace, 'createRpcProxy');
|
|
221
|
+
const isNotification = options.isNotification ?? defaultIsNotification;
|
|
222
|
+
const resolvedConnection = Promise.resolve(connection);
|
|
223
|
+
|
|
224
|
+
// Tied to `connection.onClose` so the inbound handlers release with the
|
|
225
|
+
// connection; see `localTarget` for why no `Disposable` is surfaced.
|
|
226
|
+
const { localTarget, localMethods } = options;
|
|
227
|
+
if (localTarget && localMethods && localMethods.length > 0) {
|
|
228
|
+
const binding = bindRpcMethods(connection, localTarget, localMethods, { methodNamespace, isNotification, latency: options.latency });
|
|
229
|
+
resolvedConnection.then(conn => conn.onClose(() => binding.dispose())).catch(() => undefined);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const onDidOpenConnectionEmitter = new Emitter<void>();
|
|
233
|
+
const onDidCloseConnectionEmitter = new Emitter<void>();
|
|
234
|
+
resolvedConnection
|
|
235
|
+
.then(conn => {
|
|
236
|
+
onDidOpenConnectionEmitter.fire(undefined);
|
|
237
|
+
conn.onClose(() => onDidCloseConnectionEmitter.fire(undefined));
|
|
238
|
+
})
|
|
239
|
+
.catch(() => {
|
|
240
|
+
// Connection promise rejected — treat as never-opened; lifecycle
|
|
241
|
+
// events simply never fire. Adopters observe transport-construction
|
|
242
|
+
// failures through their own connection-construction error handling.
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const target = Object.create(null) as T;
|
|
246
|
+
return new Proxy(target, {
|
|
247
|
+
get(_t, prop) {
|
|
248
|
+
if (typeof prop !== 'string') {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
// Keep the proxy out of thenable / serializer code paths so it
|
|
252
|
+
// doesn't trigger spurious requests.
|
|
253
|
+
if (prop === 'then' || prop === 'toJSON') {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
if (prop === 'onDidOpenConnection') {
|
|
257
|
+
return onDidOpenConnectionEmitter.event;
|
|
258
|
+
}
|
|
259
|
+
if (prop === 'onDidCloseConnection') {
|
|
260
|
+
return onDidCloseConnectionEmitter.event;
|
|
261
|
+
}
|
|
262
|
+
const wireName = methodNamespace + prop;
|
|
263
|
+
if (isNotification(prop)) {
|
|
264
|
+
return (...args: unknown[]): void => {
|
|
265
|
+
assertSingleArg(wireName, args);
|
|
266
|
+
// Fire-and-forget; rejection (e.g. connection closed) is swallowed
|
|
267
|
+
// to match the notification contract — adopters observe transport
|
|
268
|
+
// failures via the connection's own close / error events, never
|
|
269
|
+
// via a notification's return value.
|
|
270
|
+
void resolvedConnection.then(connection => connection.sendNotification(wireName, args[0])).catch(() => undefined);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return (...args: unknown[]): Promise<unknown> => {
|
|
274
|
+
assertSingleArg(wireName, args);
|
|
275
|
+
// Capture the calling stack frame BEFORE the await so debugging gets
|
|
276
|
+
// both client- and server-side stacks on rejection. vscode-jsonrpc's
|
|
277
|
+
// default error envelope surfaces only the server-side stack; without
|
|
278
|
+
// this, a rejected RPC promise looks like it came from "somewhere
|
|
279
|
+
// inside vscode-jsonrpc" rather than from the calling code.
|
|
280
|
+
const capturedError = new Error(`RPC request '${wireName}' failed`);
|
|
281
|
+
return resolvedConnection
|
|
282
|
+
.then(connection => connection.sendRequest(wireName, args[0]))
|
|
283
|
+
.catch((err: unknown) => {
|
|
284
|
+
if (err instanceof Error && capturedError.stack) {
|
|
285
|
+
err.stack = `${err.stack ?? err.message}\nCaused by request from:\n${capturedError.stack}`;
|
|
286
|
+
}
|
|
287
|
+
throw err;
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}) as RpcProxy<T>;
|
|
292
|
+
}
|
package/src/rpc/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
// Generic JSON-RPC primitives shared across the framework's typed
|
|
11
|
+
// protocol heads — see `data/data-protocol-methods.ts` for the
|
|
12
|
+
// specialised composition.
|
|
13
|
+
|
|
14
|
+
export * from './bind-rpc-methods';
|
|
15
|
+
export * from './create-rpc-proxy';
|
|
16
|
+
export * from './wire-prefix';
|