@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.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/lib/abstract-logger.d.ts +66 -0
  4. package/lib/abstract-logger.d.ts.map +1 -0
  5. package/lib/abstract-logger.js +110 -0
  6. package/lib/abstract-logger.js.map +1 -0
  7. package/lib/browser-runtime.d.ts +124 -0
  8. package/lib/browser-runtime.d.ts.map +1 -0
  9. package/lib/browser-runtime.js +11 -0
  10. package/lib/browser-runtime.js.map +1 -0
  11. package/lib/client/data-events.d.ts +49 -0
  12. package/lib/client/data-events.d.ts.map +1 -0
  13. package/lib/client/data-events.js +60 -0
  14. package/lib/client/data-events.js.map +1 -0
  15. package/lib/client/data-port.d.ts +95 -0
  16. package/lib/client/data-port.d.ts.map +1 -0
  17. package/lib/client/data-port.js +11 -0
  18. package/lib/client/data-port.js.map +1 -0
  19. package/lib/client/data-session.d.ts +129 -0
  20. package/lib/client/data-session.d.ts.map +1 -0
  21. package/lib/client/data-session.js +179 -0
  22. package/lib/client/data-session.js.map +1 -0
  23. package/lib/client/index.d.ts +39 -0
  24. package/lib/client/index.d.ts.map +1 -0
  25. package/lib/client/index.js +55 -0
  26. package/lib/client/index.js.map +1 -0
  27. package/lib/client/message-relay.d.ts +111 -0
  28. package/lib/client/message-relay.d.ts.map +1 -0
  29. package/lib/client/message-relay.js +139 -0
  30. package/lib/client/message-relay.js.map +1 -0
  31. package/lib/client/post-message-transport.d.ts +77 -0
  32. package/lib/client/post-message-transport.d.ts.map +1 -0
  33. package/lib/client/post-message-transport.js +95 -0
  34. package/lib/client/post-message-transport.js.map +1 -0
  35. package/lib/clock.d.ts +111 -0
  36. package/lib/clock.d.ts.map +1 -0
  37. package/lib/clock.js +74 -0
  38. package/lib/clock.js.map +1 -0
  39. package/lib/data/data-protocol-methods.d.ts +39 -0
  40. package/lib/data/data-protocol-methods.d.ts.map +1 -0
  41. package/lib/data/data-protocol-methods.js +61 -0
  42. package/lib/data/data-protocol-methods.js.map +1 -0
  43. package/lib/data/data-server-protocol.d.ts +264 -0
  44. package/lib/data/data-server-protocol.d.ts.map +1 -0
  45. package/lib/data/data-server-protocol.js +11 -0
  46. package/lib/data/data-server-protocol.js.map +1 -0
  47. package/lib/data/diagnostics.d.ts +97 -0
  48. package/lib/data/diagnostics.d.ts.map +1 -0
  49. package/lib/data/diagnostics.js +25 -0
  50. package/lib/data/diagnostics.js.map +1 -0
  51. package/lib/data/events.d.ts +107 -0
  52. package/lib/data/events.d.ts.map +1 -0
  53. package/lib/data/events.js +11 -0
  54. package/lib/data/events.js.map +1 -0
  55. package/lib/data/index.d.ts +15 -0
  56. package/lib/data/index.d.ts.map +1 -0
  57. package/lib/data/index.js +38 -0
  58. package/lib/data/index.js.map +1 -0
  59. package/lib/data/methods.d.ts +57 -0
  60. package/lib/data/methods.d.ts.map +1 -0
  61. package/lib/data/methods.js +60 -0
  62. package/lib/data/methods.js.map +1 -0
  63. package/lib/data/requests.d.ts +65 -0
  64. package/lib/data/requests.d.ts.map +1 -0
  65. package/lib/data/requests.js +11 -0
  66. package/lib/data/requests.js.map +1 -0
  67. package/lib/debouncer.d.ts +75 -0
  68. package/lib/debouncer.d.ts.map +1 -0
  69. package/lib/debouncer.js +91 -0
  70. package/lib/debouncer.js.map +1 -0
  71. package/lib/errors.d.ts +84 -0
  72. package/lib/errors.d.ts.map +1 -0
  73. package/lib/errors.js +113 -0
  74. package/lib/errors.js.map +1 -0
  75. package/lib/host-diagnostics.d.ts +58 -0
  76. package/lib/host-diagnostics.d.ts.map +1 -0
  77. package/lib/host-diagnostics.js +18 -0
  78. package/lib/host-diagnostics.js.map +1 -0
  79. package/lib/index.d.ts +34 -0
  80. package/lib/index.d.ts.map +1 -0
  81. package/lib/index.js +56 -0
  82. package/lib/index.js.map +1 -0
  83. package/lib/latency-collector.d.ts +126 -0
  84. package/lib/latency-collector.d.ts.map +1 -0
  85. package/lib/latency-collector.js +137 -0
  86. package/lib/latency-collector.js.map +1 -0
  87. package/lib/logger.d.ts +119 -0
  88. package/lib/logger.d.ts.map +1 -0
  89. package/lib/logger.js +126 -0
  90. package/lib/logger.js.map +1 -0
  91. package/lib/model-server.d.ts +352 -0
  92. package/lib/model-server.d.ts.map +1 -0
  93. package/lib/model-server.js +160 -0
  94. package/lib/model-server.js.map +1 -0
  95. package/lib/model-service/args.d.ts +64 -0
  96. package/lib/model-service/args.d.ts.map +1 -0
  97. package/lib/model-service/args.js +11 -0
  98. package/lib/model-service/args.js.map +1 -0
  99. package/lib/model-service/index.d.ts +11 -0
  100. package/lib/model-service/index.d.ts.map +1 -0
  101. package/lib/model-service/index.js +31 -0
  102. package/lib/model-service/index.js.map +1 -0
  103. package/lib/model-service/reference-candidate.d.ts +56 -0
  104. package/lib/model-service/reference-candidate.d.ts.map +1 -0
  105. package/lib/model-service/reference-candidate.js +11 -0
  106. package/lib/model-service/reference-candidate.js.map +1 -0
  107. package/lib/noop-logger.d.ts +25 -0
  108. package/lib/noop-logger.d.ts.map +1 -0
  109. package/lib/noop-logger.js +36 -0
  110. package/lib/noop-logger.js.map +1 -0
  111. package/lib/observable-value.d.ts +72 -0
  112. package/lib/observable-value.d.ts.map +1 -0
  113. package/lib/observable-value.js +52 -0
  114. package/lib/observable-value.js.map +1 -0
  115. package/lib/patch-merge.d.ts +123 -0
  116. package/lib/patch-merge.d.ts.map +1 -0
  117. package/lib/patch-merge.js +104 -0
  118. package/lib/patch-merge.js.map +1 -0
  119. package/lib/profile-session.d.ts +87 -0
  120. package/lib/profile-session.d.ts.map +1 -0
  121. package/lib/profile-session.js +98 -0
  122. package/lib/profile-session.js.map +1 -0
  123. package/lib/profiling.d.ts +83 -0
  124. package/lib/profiling.d.ts.map +1 -0
  125. package/lib/profiling.js +20 -0
  126. package/lib/profiling.js.map +1 -0
  127. package/lib/project.d.ts +121 -0
  128. package/lib/project.d.ts.map +1 -0
  129. package/lib/project.js +34 -0
  130. package/lib/project.js.map +1 -0
  131. package/lib/rpc/bind-rpc-methods.d.ts +100 -0
  132. package/lib/rpc/bind-rpc-methods.d.ts.map +1 -0
  133. package/lib/rpc/bind-rpc-methods.js +110 -0
  134. package/lib/rpc/bind-rpc-methods.js.map +1 -0
  135. package/lib/rpc/create-rpc-proxy.d.ts +188 -0
  136. package/lib/rpc/create-rpc-proxy.d.ts.map +1 -0
  137. package/lib/rpc/create-rpc-proxy.js +158 -0
  138. package/lib/rpc/create-rpc-proxy.js.map +1 -0
  139. package/lib/rpc/index.d.ts +12 -0
  140. package/lib/rpc/index.d.ts.map +1 -0
  141. package/lib/rpc/index.js +31 -0
  142. package/lib/rpc/index.js.map +1 -0
  143. package/lib/rpc/wire-prefix.d.ts +37 -0
  144. package/lib/rpc/wire-prefix.d.ts.map +1 -0
  145. package/lib/rpc/wire-prefix.js +49 -0
  146. package/lib/rpc/wire-prefix.js.map +1 -0
  147. package/lib/testing/data-doubles.d.ts +121 -0
  148. package/lib/testing/data-doubles.d.ts.map +1 -0
  149. package/lib/testing/data-doubles.js +102 -0
  150. package/lib/testing/data-doubles.js.map +1 -0
  151. package/lib/testing/fake-clock.d.ts +35 -0
  152. package/lib/testing/fake-clock.d.ts.map +1 -0
  153. package/lib/testing/fake-clock.js +86 -0
  154. package/lib/testing/fake-clock.js.map +1 -0
  155. package/lib/testing/harness.d.ts +24 -0
  156. package/lib/testing/harness.d.ts.map +1 -0
  157. package/lib/testing/harness.js +11 -0
  158. package/lib/testing/harness.js.map +1 -0
  159. package/lib/testing/index.d.ts +13 -0
  160. package/lib/testing/index.d.ts.map +1 -0
  161. package/lib/testing/index.js +41 -0
  162. package/lib/testing/index.js.map +1 -0
  163. package/lib/testing/node/duplex-connection.d.ts +25 -0
  164. package/lib/testing/node/duplex-connection.d.ts.map +1 -0
  165. package/lib/testing/node/duplex-connection.js +34 -0
  166. package/lib/testing/node/duplex-connection.js.map +1 -0
  167. package/lib/testing/node/duplex-stream.d.ts +32 -0
  168. package/lib/testing/node/duplex-stream.d.ts.map +1 -0
  169. package/lib/testing/node/duplex-stream.js +35 -0
  170. package/lib/testing/node/duplex-stream.js.map +1 -0
  171. package/lib/testing/node/index.d.ts +11 -0
  172. package/lib/testing/node/index.d.ts.map +1 -0
  173. package/lib/testing/node/index.js +37 -0
  174. package/lib/testing/node/index.js.map +1 -0
  175. package/lib/testing/wait-for.d.ts +69 -0
  176. package/lib/testing/wait-for.d.ts.map +1 -0
  177. package/lib/testing/wait-for.js +67 -0
  178. package/lib/testing/wait-for.js.map +1 -0
  179. package/lib/tracer.d.ts +115 -0
  180. package/lib/tracer.d.ts.map +1 -0
  181. package/lib/tracer.js +205 -0
  182. package/lib/tracer.js.map +1 -0
  183. package/lib/transfer-diagnostic.d.ts +73 -0
  184. package/lib/transfer-diagnostic.d.ts.map +1 -0
  185. package/lib/transfer-diagnostic.js +45 -0
  186. package/lib/transfer-diagnostic.js.map +1 -0
  187. package/lib/transfer-document.d.ts +73 -0
  188. package/lib/transfer-document.d.ts.map +1 -0
  189. package/lib/transfer-document.js +46 -0
  190. package/lib/transfer-document.js.map +1 -0
  191. package/lib/transfer-element.d.ts +39 -0
  192. package/lib/transfer-element.d.ts.map +1 -0
  193. package/lib/transfer-element.js +11 -0
  194. package/lib/transfer-element.js.map +1 -0
  195. package/lib/uri.d.ts +62 -0
  196. package/lib/uri.d.ts.map +1 -0
  197. package/lib/uri.js +28 -0
  198. package/lib/uri.js.map +1 -0
  199. package/lib/util.d.ts +147 -0
  200. package/lib/util.d.ts.map +1 -0
  201. package/lib/util.js +211 -0
  202. package/lib/util.js.map +1 -0
  203. package/package.json +100 -0
  204. package/src/abstract-logger.ts +120 -0
  205. package/src/browser-runtime.ts +135 -0
  206. package/src/client/data-events.ts +71 -0
  207. package/src/client/data-port.ts +99 -0
  208. package/src/client/data-session.ts +209 -0
  209. package/src/client/index.ts +40 -0
  210. package/src/client/message-relay.ts +216 -0
  211. package/src/client/post-message-transport.ts +167 -0
  212. package/src/clock.ts +173 -0
  213. package/src/data/data-protocol-methods.ts +79 -0
  214. package/src/data/data-server-protocol.ts +296 -0
  215. package/src/data/diagnostics.ts +116 -0
  216. package/src/data/events.ts +127 -0
  217. package/src/data/index.ts +23 -0
  218. package/src/data/methods.ts +59 -0
  219. package/src/data/requests.ts +70 -0
  220. package/src/debouncer.ts +118 -0
  221. package/src/errors.ts +127 -0
  222. package/src/host-diagnostics.ts +63 -0
  223. package/src/index.ts +41 -0
  224. package/src/latency-collector.ts +231 -0
  225. package/src/logger.ts +172 -0
  226. package/src/model-server.ts +479 -0
  227. package/src/model-service/args.ts +67 -0
  228. package/src/model-service/index.ts +16 -0
  229. package/src/model-service/reference-candidate.ts +58 -0
  230. package/src/noop-logger.ts +36 -0
  231. package/src/observable-value.ts +92 -0
  232. package/src/patch-merge.ts +165 -0
  233. package/src/profile-session.ts +164 -0
  234. package/src/profiling.ts +113 -0
  235. package/src/project.ts +125 -0
  236. package/src/rpc/README.md +163 -0
  237. package/src/rpc/bind-rpc-methods.ts +180 -0
  238. package/src/rpc/create-rpc-proxy.ts +292 -0
  239. package/src/rpc/index.ts +16 -0
  240. package/src/rpc/wire-prefix.ts +48 -0
  241. package/src/testing/data-doubles.ts +178 -0
  242. package/src/testing/fake-clock.ts +106 -0
  243. package/src/testing/harness.ts +24 -0
  244. package/src/testing/index.ts +26 -0
  245. package/src/testing/node/duplex-connection.ts +50 -0
  246. package/src/testing/node/duplex-stream.ts +51 -0
  247. package/src/testing/node/index.ts +22 -0
  248. package/src/testing/wait-for.ts +94 -0
  249. package/src/tracer.ts +277 -0
  250. package/src/transfer-diagnostic.ts +92 -0
  251. package/src/transfer-document.ts +99 -0
  252. package/src/transfer-element.ts +43 -0
  253. package/src/uri.ts +61 -0
  254. 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
+ }
@@ -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';