@hydranium/protocol 1.0.0-next.23 → 1.0.0-next.232

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 (246) hide show
  1. package/README.md +40 -3
  2. package/lib/abstract-logger.d.ts +5 -0
  3. package/lib/abstract-logger.d.ts.map +1 -1
  4. package/lib/abstract-logger.js +7 -0
  5. package/lib/abstract-logger.js.map +1 -1
  6. package/lib/client/data-connection.d.ts +236 -0
  7. package/lib/client/data-connection.d.ts.map +1 -0
  8. package/lib/client/data-connection.js +404 -0
  9. package/lib/client/data-connection.js.map +1 -0
  10. package/lib/client/data-events.d.ts +13 -1
  11. package/lib/client/data-events.d.ts.map +1 -1
  12. package/lib/client/data-events.js +21 -0
  13. package/lib/client/data-events.js.map +1 -1
  14. package/lib/client/data-port.d.ts +27 -22
  15. package/lib/client/data-port.d.ts.map +1 -1
  16. package/lib/client/data-session.d.ts +473 -81
  17. package/lib/client/data-session.d.ts.map +1 -1
  18. package/lib/client/data-session.js +743 -108
  19. package/lib/client/data-session.js.map +1 -1
  20. package/lib/client/index.d.ts +14 -9
  21. package/lib/client/index.d.ts.map +1 -1
  22. package/lib/client/index.js +14 -9
  23. package/lib/client/index.js.map +1 -1
  24. package/lib/client/message-relay.d.ts +9 -3
  25. package/lib/client/message-relay.d.ts.map +1 -1
  26. package/lib/client/message-relay.js +11 -5
  27. package/lib/client/message-relay.js.map +1 -1
  28. package/lib/client/post-message-transport.d.ts +64 -3
  29. package/lib/client/post-message-transport.d.ts.map +1 -1
  30. package/lib/client/post-message-transport.js +175 -1
  31. package/lib/client/post-message-transport.js.map +1 -1
  32. package/lib/client/rpc-connection.d.ts +157 -0
  33. package/lib/client/rpc-connection.d.ts.map +1 -0
  34. package/lib/client/rpc-connection.js +214 -0
  35. package/lib/client/rpc-connection.js.map +1 -0
  36. package/lib/client-ids.d.ts +45 -0
  37. package/lib/client-ids.d.ts.map +1 -0
  38. package/lib/client-ids.js +48 -0
  39. package/lib/client-ids.js.map +1 -0
  40. package/lib/clock.d.ts +38 -0
  41. package/lib/clock.d.ts.map +1 -1
  42. package/lib/clock.js +36 -1
  43. package/lib/clock.js.map +1 -1
  44. package/lib/console-logger.d.ts +23 -0
  45. package/lib/console-logger.d.ts.map +1 -0
  46. package/lib/console-logger.js +39 -0
  47. package/lib/console-logger.js.map +1 -0
  48. package/lib/data/data-protocol-methods.d.ts +4 -4
  49. package/lib/data/data-protocol-methods.d.ts.map +1 -1
  50. package/lib/data/data-protocol-methods.js +12 -1
  51. package/lib/data/data-protocol-methods.js.map +1 -1
  52. package/lib/data/data-server-protocol.d.ts +132 -41
  53. package/lib/data/data-server-protocol.d.ts.map +1 -1
  54. package/lib/data/events.d.ts +117 -21
  55. package/lib/data/events.d.ts.map +1 -1
  56. package/lib/data/requests.d.ts +69 -11
  57. package/lib/data/requests.d.ts.map +1 -1
  58. package/lib/debouncer.d.ts.map +1 -1
  59. package/lib/debouncer.js.map +1 -1
  60. package/lib/errors.d.ts +187 -29
  61. package/lib/errors.d.ts.map +1 -1
  62. package/lib/errors.js +270 -29
  63. package/lib/errors.js.map +1 -1
  64. package/lib/glsp-request-model-args.d.ts +16 -0
  65. package/lib/glsp-request-model-args.d.ts.map +1 -0
  66. package/lib/glsp-request-model-args.js +19 -0
  67. package/lib/glsp-request-model-args.js.map +1 -0
  68. package/lib/glsp-save-model-actions.d.ts +50 -0
  69. package/lib/glsp-save-model-actions.d.ts.map +1 -0
  70. package/lib/glsp-save-model-actions.js +28 -0
  71. package/lib/glsp-save-model-actions.js.map +1 -0
  72. package/lib/index.d.ts +7 -0
  73. package/lib/index.d.ts.map +1 -1
  74. package/lib/index.js +10 -0
  75. package/lib/index.js.map +1 -1
  76. package/lib/latency-collector.d.ts +8 -4
  77. package/lib/latency-collector.d.ts.map +1 -1
  78. package/lib/latency-collector.js.map +1 -1
  79. package/lib/logger.d.ts +22 -1
  80. package/lib/logger.d.ts.map +1 -1
  81. package/lib/logger.js +31 -3
  82. package/lib/logger.js.map +1 -1
  83. package/lib/messages/index.d.ts +30 -0
  84. package/lib/messages/index.d.ts.map +1 -0
  85. package/lib/messages/index.js +62 -0
  86. package/lib/messages/index.js.map +1 -0
  87. package/lib/messages/primitives.d.ts +188 -0
  88. package/lib/messages/primitives.d.ts.map +1 -0
  89. package/lib/messages/primitives.js +161 -0
  90. package/lib/messages/primitives.js.map +1 -0
  91. package/lib/model-server.d.ts +60 -13
  92. package/lib/model-server.d.ts.map +1 -1
  93. package/lib/model-server.js +4 -2
  94. package/lib/model-server.js.map +1 -1
  95. package/lib/model-service/base-version.d.ts +64 -0
  96. package/lib/model-service/base-version.d.ts.map +1 -0
  97. package/lib/model-service/base-version.js +43 -0
  98. package/lib/model-service/base-version.js.map +1 -0
  99. package/lib/model-service/index.d.ts +1 -1
  100. package/lib/model-service/index.d.ts.map +1 -1
  101. package/lib/model-service/index.js +4 -5
  102. package/lib/model-service/index.js.map +1 -1
  103. package/lib/model-service/reference-candidate.d.ts +5 -3
  104. package/lib/model-service/reference-candidate.d.ts.map +1 -1
  105. package/lib/{model-service/args.js → node/index.d.ts} +2 -3
  106. package/lib/node/index.d.ts.map +1 -0
  107. package/lib/node/index.js +29 -0
  108. package/lib/node/index.js.map +1 -0
  109. package/lib/node/process-memory.d.ts +66 -0
  110. package/lib/node/process-memory.d.ts.map +1 -0
  111. package/lib/node/process-memory.js +291 -0
  112. package/lib/node/process-memory.js.map +1 -0
  113. package/lib/noop-logger.d.ts.map +1 -1
  114. package/lib/noop-logger.js.map +1 -1
  115. package/lib/observable-value.js.map +1 -1
  116. package/lib/patch-merge.d.ts +35 -32
  117. package/lib/patch-merge.d.ts.map +1 -1
  118. package/lib/patch-merge.js +67 -23
  119. package/lib/patch-merge.js.map +1 -1
  120. package/lib/profile-session.d.ts +8 -4
  121. package/lib/profile-session.d.ts.map +1 -1
  122. package/lib/profile-session.js.map +1 -1
  123. package/lib/random-uuid.d.ts +14 -0
  124. package/lib/random-uuid.d.ts.map +1 -0
  125. package/lib/random-uuid.js +24 -0
  126. package/lib/random-uuid.js.map +1 -0
  127. package/lib/reconcile-write.d.ts +65 -0
  128. package/lib/reconcile-write.d.ts.map +1 -0
  129. package/lib/reconcile-write.js +67 -0
  130. package/lib/reconcile-write.js.map +1 -0
  131. package/lib/rpc/bind-rpc-methods.d.ts +33 -3
  132. package/lib/rpc/bind-rpc-methods.d.ts.map +1 -1
  133. package/lib/rpc/bind-rpc-methods.js +32 -3
  134. package/lib/rpc/bind-rpc-methods.js.map +1 -1
  135. package/lib/rpc/create-rpc-proxy.d.ts +10 -0
  136. package/lib/rpc/create-rpc-proxy.d.ts.map +1 -1
  137. package/lib/rpc/create-rpc-proxy.js +12 -2
  138. package/lib/rpc/create-rpc-proxy.js.map +1 -1
  139. package/lib/rpc/index.d.ts +1 -0
  140. package/lib/rpc/index.d.ts.map +1 -1
  141. package/lib/rpc/index.js +1 -0
  142. package/lib/rpc/index.js.map +1 -1
  143. package/lib/rpc/send-by-method-name.d.ts +76 -0
  144. package/lib/rpc/send-by-method-name.d.ts.map +1 -0
  145. package/lib/rpc/send-by-method-name.js +120 -0
  146. package/lib/rpc/send-by-method-name.js.map +1 -0
  147. package/lib/rpc/wire-prefix.js.map +1 -1
  148. package/lib/testing/catalogue-audit.d.ts +80 -0
  149. package/lib/testing/catalogue-audit.d.ts.map +1 -0
  150. package/lib/testing/catalogue-audit.js +94 -0
  151. package/lib/testing/catalogue-audit.js.map +1 -0
  152. package/lib/testing/data-doubles.d.ts +42 -15
  153. package/lib/testing/data-doubles.d.ts.map +1 -1
  154. package/lib/testing/data-doubles.js +58 -10
  155. package/lib/testing/data-doubles.js.map +1 -1
  156. package/lib/testing/fake-clock.d.ts +9 -1
  157. package/lib/testing/fake-clock.d.ts.map +1 -1
  158. package/lib/testing/fake-clock.js +54 -45
  159. package/lib/testing/fake-clock.js.map +1 -1
  160. package/lib/testing/index.d.ts +1 -0
  161. package/lib/testing/index.d.ts.map +1 -1
  162. package/lib/testing/index.js +5 -2
  163. package/lib/testing/index.js.map +1 -1
  164. package/lib/testing/node/duplex-connection.d.ts.map +1 -1
  165. package/lib/testing/node/duplex-connection.js +3 -2
  166. package/lib/testing/node/duplex-connection.js.map +1 -1
  167. package/lib/testing/node/duplex-stream.js.map +1 -1
  168. package/lib/testing/node/index.d.ts +1 -0
  169. package/lib/testing/node/index.d.ts.map +1 -1
  170. package/lib/testing/node/index.js +2 -2
  171. package/lib/testing/node/index.js.map +1 -1
  172. package/lib/testing/node/message-port-pair.d.ts +25 -0
  173. package/lib/testing/node/message-port-pair.d.ts.map +1 -0
  174. package/lib/testing/node/message-port-pair.js +26 -0
  175. package/lib/testing/node/message-port-pair.js.map +1 -0
  176. package/lib/testing/wait-for.js.map +1 -1
  177. package/lib/tracer.d.ts.map +1 -1
  178. package/lib/tracer.js.map +1 -1
  179. package/lib/transfer-diagnostic.d.ts +33 -0
  180. package/lib/transfer-diagnostic.d.ts.map +1 -1
  181. package/lib/transfer-diagnostic.js +23 -0
  182. package/lib/transfer-diagnostic.js.map +1 -1
  183. package/lib/transfer-document.d.ts +70 -32
  184. package/lib/transfer-document.d.ts.map +1 -1
  185. package/lib/transfer-document.js +17 -9
  186. package/lib/transfer-document.js.map +1 -1
  187. package/lib/uri.d.ts.map +1 -1
  188. package/lib/uri.js.map +1 -1
  189. package/lib/util.d.ts +8 -0
  190. package/lib/util.d.ts.map +1 -1
  191. package/lib/util.js +32 -0
  192. package/lib/util.js.map +1 -1
  193. package/package.json +29 -37
  194. package/src/abstract-logger.ts +8 -0
  195. package/src/client/data-connection.ts +502 -0
  196. package/src/client/data-events.ts +33 -1
  197. package/src/client/data-port.ts +29 -23
  198. package/src/client/data-session.ts +951 -126
  199. package/src/client/index.ts +14 -9
  200. package/src/client/message-relay.ts +29 -7
  201. package/src/client/post-message-transport.ts +219 -4
  202. package/src/client/rpc-connection.ts +281 -0
  203. package/src/client-ids.ts +49 -0
  204. package/src/clock.ts +56 -0
  205. package/src/console-logger.ts +39 -0
  206. package/src/data/data-protocol-methods.ts +13 -4
  207. package/src/data/data-server-protocol.ts +157 -41
  208. package/src/data/events.ts +123 -21
  209. package/src/data/requests.ts +74 -11
  210. package/src/errors.ts +322 -36
  211. package/src/glsp-request-model-args.ts +16 -0
  212. package/src/glsp-save-model-actions.ts +59 -0
  213. package/src/index.ts +10 -0
  214. package/src/latency-collector.ts +8 -3
  215. package/src/logger.ts +28 -2
  216. package/src/messages/index.ts +37 -0
  217. package/src/messages/primitives.ts +271 -0
  218. package/src/model-server.ts +63 -18
  219. package/src/model-service/base-version.ts +72 -0
  220. package/src/model-service/index.ts +4 -5
  221. package/src/model-service/reference-candidate.ts +5 -3
  222. package/src/node/index.ts +14 -0
  223. package/src/node/process-memory.ts +299 -0
  224. package/src/patch-merge.ts +97 -42
  225. package/src/profile-session.ts +9 -4
  226. package/src/random-uuid.ts +21 -0
  227. package/src/reconcile-write.ts +124 -0
  228. package/src/rpc/README.md +4 -5
  229. package/src/rpc/bind-rpc-methods.ts +59 -4
  230. package/src/rpc/create-rpc-proxy.ts +20 -2
  231. package/src/rpc/index.ts +1 -0
  232. package/src/rpc/send-by-method-name.ts +140 -0
  233. package/src/testing/catalogue-audit.ts +111 -0
  234. package/src/testing/data-doubles.ts +145 -25
  235. package/src/testing/fake-clock.ts +62 -47
  236. package/src/testing/index.ts +5 -2
  237. package/src/testing/node/duplex-connection.ts +3 -2
  238. package/src/testing/node/index.ts +2 -2
  239. package/src/testing/node/message-port-pair.ts +40 -0
  240. package/src/transfer-diagnostic.ts +40 -0
  241. package/src/transfer-document.ts +87 -34
  242. package/src/util.ts +33 -0
  243. package/lib/model-service/args.d.ts +0 -64
  244. package/lib/model-service/args.d.ts.map +0 -1
  245. package/lib/model-service/args.js.map +0 -1
  246. package/src/model-service/args.ts +0 -67
@@ -13,28 +13,33 @@
13
13
  *
14
14
  * Where `./data` is the wire *contract* and `./rpc` is the machinery that lowers
15
15
  * it onto a connection, this is what a client wraps around both: the seam a host
16
- * fills in (`DataPort`), the lifecycle above it (`DataSession` —
17
- * readiness gate, open/watch ordering, echo recognition, reconnect), the inbound
16
+ * fills in (`DataPort`), the connection above it (`DataConnection` — readiness
17
+ * gate and reconnect), the participants on that connection (`DataSession` —
18
+ * identity, open/watch ordering, echo recognition), the inbound
18
19
  * fan-out (`DataEvents`), and the two halves of the hop for hosts whose
19
20
  * client cannot hold a socket — `createPostMessageTransport` on the client
20
- * side and `relayToPostMessageChannel` on the side that does hold it.
21
+ * side and `relayToPostMessageChannel` on the side that does hold it, and
22
+ * `createMessagePortTransport` for a head's worker `MessagePort`, at either
23
+ * end, which the GLSP worker head uses too.
21
24
  *
22
25
  * **Neutral, and gate-enforced so.** Nothing here imports a host package or a
23
26
  * Node builtin, which is what lets one client tier serve a Theia frontend, a VS
24
27
  * Code extension host, a VS Code webview and a plain browser app. `npm run
25
- * check:neutral` bundles these modules for the browser; `scripts/check-neutral-bundles.mjs`
28
+ * check:neutral` bundles these modules for the browser; `scripts/check-neutral-bundles.mts`
26
29
  * carries the entries.
27
30
  *
28
31
  * The Theia-specific mounting of the same contract lives in
29
- * `@hydranium/data-client-theia`: its `AbstractDataServiceFrontend` solves the
30
- * same problem against Theia's channel transport, and its `EmitterDataClient`
31
- * is the Theia-bound counterpart of `DataEvents`. Prefer this tier for
32
- * anything new, and reach for the Theia package only for what genuinely needs
33
- * Theia DI.
32
+ * `@hydranium/data-client-theia`, and is now only the TRANSPORT: its
33
+ * `ChannelDataPort` fills the `DataPort` seam over a Theia channel, and its
34
+ * `EmitterDataClient` is the Theia-bound counterpart of `DataEvents`.
35
+ * Everything above the port is here, so a Theia frontend and a browser page
36
+ * differ by which port they construct and nothing else.
34
37
  */
35
38
 
39
+ export * from './data-connection';
36
40
  export * from './data-events';
37
41
  export * from './data-port';
38
42
  export * from './data-session';
39
43
  export * from './message-relay';
40
44
  export * from './post-message-transport';
45
+ export * from './rpc-connection';
@@ -8,8 +8,29 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import { Emitter, type Disposable, type Event, type Message, type MessageReader, type MessageWriter } from 'vscode-jsonrpc';
11
+ import { defineMessage, describeError, resolve, type ResolvedMessage } from '../messages/primitives';
11
12
  import type { PostMessageChannel } from './post-message-transport';
12
13
 
14
+ export const RELAY_TRANSPORT_OPEN_FAILED = defineMessage(
15
+ 'hydranium/protocol/relay-transport-open-failed',
16
+ 'Could not open the transport to relay: {detail}'
17
+ );
18
+
19
+ export const RELAY_TRANSPORT_READ_FAILED = defineMessage(
20
+ 'hydranium/protocol/relay-transport-read-failed',
21
+ 'Could not read from the relayed transport: {detail}'
22
+ );
23
+
24
+ export const RELAY_TRANSPORT_WRITE_FAILED = defineMessage(
25
+ 'hydranium/protocol/relay-transport-write-failed',
26
+ 'Could not write to the relayed transport: {detail}'
27
+ );
28
+
29
+ export const RELAY_REPLAY_FAILED = defineMessage(
30
+ 'hydranium/protocol/relay-replay-failed',
31
+ 'Could not replay a buffered message to the relayed transport: {detail}'
32
+ );
33
+
13
34
  /**
14
35
  * The framed side of a relay: the reader/writer pair over whatever transport the
15
36
  * host actually holds — a TCP socket to the data-server, a child process' stdio,
@@ -30,13 +51,14 @@ export interface RelayTransport {
30
51
  export interface MessageRelayOptions {
31
52
  /**
32
53
  * Surface a failure the way the host does. Same contract as
33
- * `DataPort.reportError`: `context` names what was being attempted.
54
+ * `DataPort.reportError`: `reported` is a complete sentence plus the identity
55
+ * needed to render it in another language.
34
56
  *
35
57
  * A relay has no other way to report — it sits between two transports and
36
58
  * owns neither, so a swallowed error here presents as a form that never
37
59
  * populates.
38
60
  */
39
- readonly reportError?: (error: unknown, context: string) => void;
61
+ readonly reportError?: (error: unknown, reported: ResolvedMessage) => void;
40
62
  }
41
63
 
42
64
  /** A live relay. Dispose to tear both directions down. */
@@ -88,7 +110,7 @@ export interface MessageRelay extends Disposable {
88
110
  * imports the entrypoint that frames its own transport (`vscode-jsonrpc/node`
89
111
  * for a socket) and the RAL that comes with it; this tier stays neutral and is
90
112
  * gated so by `npm run check:neutral`. Contrast
91
- * `@hydranium/data-client-theia`'s `SocketChannelForwarder`, which does build a
113
+ * `@hydranium/client-theia`'s `SocketChannelForwarder`, which does build a
92
114
  * connection only to borrow its `onClose`, and pays a Theia dependency for the
93
115
  * byte coding this shape does not need.
94
116
  *
@@ -143,7 +165,7 @@ export function relayToPostMessageChannel(
143
165
  bufferSubscription?.dispose();
144
166
  bufferSubscription = undefined;
145
167
  buffered.length = 0;
146
- options.reportError?.(error, 'opening the transport to relay');
168
+ options.reportError?.(error, resolve(RELAY_TRANSPORT_OPEN_FAILED, { detail: describeError(error) }));
147
169
  closeFramedSide();
148
170
  return false;
149
171
  }
@@ -166,12 +188,12 @@ export function relayToPostMessageChannel(
166
188
  opened.reader.listen(message => channel.post(message)),
167
189
  opened.reader.onClose(() => closeFramedSide()),
168
190
  opened.reader.onError(error => {
169
- options.reportError?.(error, 'reading from the relayed transport');
191
+ options.reportError?.(error, resolve(RELAY_TRANSPORT_READ_FAILED, { detail: describeError(error) }));
170
192
  closeFramedSide();
171
193
  }),
172
194
  channel.onMessage(message => {
173
195
  void opened.writer.write(message).catch((error: unknown) => {
174
- options.reportError?.(error, 'writing to the relayed transport');
196
+ options.reportError?.(error, resolve(RELAY_TRANSPORT_WRITE_FAILED, { detail: describeError(error) }));
175
197
  });
176
198
  })
177
199
  );
@@ -183,7 +205,7 @@ export function relayToPostMessageChannel(
183
205
 
184
206
  for (const message of buffered) {
185
207
  void opened.writer.write(message).catch((error: unknown) => {
186
- options.reportError?.(error, 'replaying a buffered message to the relayed transport');
208
+ options.reportError?.(error, resolve(RELAY_REPLAY_FAILED, { detail: describeError(error) }));
187
209
  });
188
210
  }
189
211
  buffered.length = 0;
@@ -11,6 +11,7 @@ import {
11
11
  AbstractMessageReader,
12
12
  AbstractMessageWriter,
13
13
  Emitter,
14
+ RAL,
14
15
  type DataCallback,
15
16
  type Disposable,
16
17
  type Message,
@@ -36,7 +37,8 @@ export interface PostMessageChannel {
36
37
  /**
37
38
  * Hand one JSON-RPC message to the other side. Fire-and-forget: delivery
38
39
  * failures surface through {@link onClose}, never as a rejection, because
39
- * `postMessage` has no completion to report.
40
+ * `postMessage` has no completion to report. A value the pipe refuses may
41
+ * throw, and the writer turns that into a rejected write and a writer error.
40
42
  */
41
43
  post(message: Message): void;
42
44
 
@@ -67,12 +69,18 @@ export interface PostMessageChannel {
67
69
  *
68
70
  * Keeping the factory out of this module is also what lets the module stay
69
71
  * browser-neutral: it touches only `AbstractMessageReader` /
70
- * `AbstractMessageWriter` / `Emitter`, none of which need a RAL.
72
+ * `AbstractMessageWriter` / `Emitter`, none of which need a RAL, and the RAL's
73
+ * timer, which {@link createMessagePortTransport} reads only once a message
74
+ * arrives, by when the host that built the connection has installed one in
75
+ * the same copy of `vscode-jsonrpc`.
71
76
  */
72
77
  export interface PostMessageTransport {
73
78
  readonly reader: MessageReader;
74
79
  readonly writer: MessageWriter;
75
- /** Release the channel subscriptions. */
80
+ /**
81
+ * Release the channel subscriptions. A {@link createMessagePortTransport}
82
+ * transport also signals its end to the other side.
83
+ */
76
84
  dispose(): void;
77
85
  }
78
86
 
@@ -112,6 +120,7 @@ class PostMessageReader extends AbstractMessageReader implements MessageReader {
112
120
  /** The dual of {@link PostMessageReader} — one `post` per JSON-RPC message. */
113
121
  class PostMessageWriter extends AbstractMessageWriter implements MessageWriter {
114
122
  protected readonly subscriptions: Disposable[] = [];
123
+ protected errorCount = 0;
115
124
 
116
125
  constructor(protected readonly channel: PostMessageChannel) {
117
126
  super();
@@ -122,7 +131,17 @@ class PostMessageWriter extends AbstractMessageWriter implements MessageWriter {
122
131
  }
123
132
 
124
133
  write(message: Message): Promise<void> {
125
- this.channel.post(message);
134
+ try {
135
+ this.channel.post(message);
136
+ } catch (error: unknown) {
137
+ // `postMessage` throws synchronously on a value structured clone
138
+ // refuses. Thrown on, it would escape from the caller's
139
+ // `sendNotification` rather than reject it, and the connection would
140
+ // never see a write error.
141
+ this.errorCount++;
142
+ this.fireError(error, message, this.errorCount);
143
+ return Promise.reject(error);
144
+ }
126
145
  // `postMessage` reports no completion, so the resolved promise means
127
146
  // "handed over", not "delivered". vscode-jsonrpc only needs the former.
128
147
  return Promise.resolve();
@@ -165,3 +184,199 @@ export function createPostMessageTransport(channel: PostMessageChannel): PostMes
165
184
  }
166
185
  };
167
186
  }
187
+
188
+ /**
189
+ * A `MessagePort` the host transferred into or out of a worker, described
190
+ * structurally.
191
+ *
192
+ * This package compiles without the DOM lib, so `MessagePort` has no name here
193
+ * and the contract has to be spelled out.
194
+ *
195
+ * **The member set admits a `MessagePort` and REJECTS a `Worker` or the worker
196
+ * global**, both of which would otherwise fit a `postMessage` pipe. Only a port
197
+ * needs starting, so `start` is what tells the three apart, and naming it here
198
+ * turns "never bind a head to the global" into a compile error at the call
199
+ * site. A head on the global receives every other head's traffic.
200
+ *
201
+ * **That compile error happens at the ADOPTER, not here.** This package
202
+ * resolves neither `MessagePort` nor `Worker` as a type, so nothing here can
203
+ * demonstrate the rejection; a host compiling its worker against
204
+ * `lib.webworker` (or a page against `lib.dom`) is where the names resolve and
205
+ * the guard bites. Measured there: passing the worker global fails with
206
+ * `Property 'start' is missing in type 'DedicatedWorkerGlobalScope'`.
207
+ */
208
+ export interface TransferredMessagePort {
209
+ postMessage(message: unknown): void;
210
+ addEventListener(type: 'message', listener: (event: unknown) => void, options?: unknown): void;
211
+ removeEventListener(type: 'message', listener: (event: unknown) => void, options?: unknown): void;
212
+ start(): void;
213
+ }
214
+
215
+ /**
216
+ * The value {@link createMessagePortTransport} posts in place of a JSON-RPC
217
+ * message to say its end is going away. A string, so no JSON-RPC message can
218
+ * be mistaken for it.
219
+ */
220
+ const MESSAGE_PORT_CLOSE_SIGNAL = 'hydranium/message-port-closed';
221
+
222
+ /**
223
+ * A {@link PostMessageChannel} over a {@link TransferredMessagePort}, whose
224
+ * {@link close} posts {@link MESSAGE_PORT_CLOSE_SIGNAL}.
225
+ *
226
+ * **A port reports nothing when its peer goes away**: Chromium ships no `close`
227
+ * event on `MessagePort`, Firefox and WebKit have not committed to one, and
228
+ * `messageerror` fires only for a message that cannot be deserialized. So the
229
+ * end is signalled in band.
230
+ *
231
+ * The port is never `close()`d. A later transport can still be built on it, at
232
+ * both ends, and whether messages posted just before a `close()` are delivered
233
+ * is not something the platform settles.
234
+ */
235
+ class MessagePortChannel implements PostMessageChannel {
236
+ protected readonly messageEmitter = new Emitter<Message>();
237
+ protected readonly closeEmitter = new Emitter<void>();
238
+ /** Set once either end has signalled; this end then delivers nothing more and never signals again. */
239
+ protected closed = false;
240
+ /** Set when the peer's signal has arrived and its close waits on {@link backlog}. */
241
+ protected closePending = false;
242
+ /** Messages handed to the connection that its queue may not have dispatched yet. */
243
+ protected backlog = 0;
244
+ // A `MessageEvent`, which the listener's type cannot name without the DOM lib.
245
+ protected readonly listener = (event: unknown): void => this.receive((event as { readonly data: unknown }).data);
246
+
247
+ constructor(protected readonly port: TransferredMessagePort) {
248
+ port.addEventListener('message', this.listener);
249
+ }
250
+
251
+ post(message: Message): void {
252
+ if (!this.closed) {
253
+ this.port.postMessage(message);
254
+ }
255
+ }
256
+
257
+ onMessage(listener: (message: Message) => void): Disposable {
258
+ return this.messageEmitter.event(listener);
259
+ }
260
+
261
+ onClose(listener: () => void): Disposable {
262
+ return this.closeEmitter.event(listener);
263
+ }
264
+
265
+ /** Signal this end's close to the peer, once, after everything posted before it. */
266
+ close(): void {
267
+ if (this.closed) {
268
+ return;
269
+ }
270
+ this.end();
271
+ this.port.postMessage(MESSAGE_PORT_CLOSE_SIGNAL);
272
+ }
273
+
274
+ protected end(): void {
275
+ this.closed = true;
276
+ this.port.removeEventListener('message', this.listener);
277
+ }
278
+
279
+ /**
280
+ * Deliver one inbound value, or take the peer's signal.
281
+ *
282
+ * **The close waits until the connection has dispatched every message that
283
+ * came before it.** Firing it at once is safe under `vscode-jsonrpc`'s
284
+ * browser runtime, which drains the connection's queue on microtasks before
285
+ * the signal's own task runs. Under its Node runtime the queue gives out one
286
+ * message per `setImmediate` turn, so a close fired at once would overtake a
287
+ * `closeSession` still queued, and end that session as lost. So each
288
+ * delivered message is counted down on the same RAL timer, one per turn,
289
+ * scheduled after the connection's own, and the close fires when the count
290
+ * reaches zero: under either runtime, after the last dispatch.
291
+ */
292
+ protected receive(data: unknown): void {
293
+ if (this.closed) {
294
+ return;
295
+ }
296
+ if (data === MESSAGE_PORT_CLOSE_SIGNAL) {
297
+ this.end();
298
+ this.closePending = true;
299
+ if (this.backlog === 0) {
300
+ this.closeEmitter.fire();
301
+ }
302
+ return;
303
+ }
304
+ this.messageEmitter.fire(data as Message);
305
+ this.backlog++;
306
+ if (this.backlog === 1) {
307
+ this.countDownBacklog();
308
+ }
309
+ }
310
+
311
+ // Mirrors the connection's queue at its default of one message per turn. A
312
+ // connection built with a `maxParallelism`, or with a `messageStrategy` that
313
+ // defers `next`, can hold messages longer than this counts, and its close
314
+ // can then overtake them.
315
+ protected countDownBacklog(): void {
316
+ RAL().timer.setImmediate(() => {
317
+ this.backlog--;
318
+ if (this.backlog > 0) {
319
+ this.countDownBacklog();
320
+ } else if (this.closePending) {
321
+ this.closeEmitter.fire();
322
+ }
323
+ });
324
+ }
325
+ }
326
+
327
+ /**
328
+ * A {@link PostMessageWriter} whose dispose, which a `MessageConnection`'s
329
+ * `dispose` and `end` both reach, signals this end's close to the peer.
330
+ */
331
+ class MessagePortWriter extends PostMessageWriter {
332
+ constructor(protected override readonly channel: MessagePortChannel) {
333
+ super(channel);
334
+ }
335
+
336
+ override dispose(): void {
337
+ this.channel.close();
338
+ super.dispose();
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Build the reader/writer pair for a head's `MessagePort`, at either end.
344
+ *
345
+ * Disposing the writer (which the connection's own `dispose` does) posts a
346
+ * close signal after everything it wrote, and the other end's reader and writer
347
+ * then fire close, so a head tears down on its client's dispose as it does on a
348
+ * socket's close. For a connection at its default parallelism and message
349
+ * strategy, the close fires only once that end's connection has dispatched
350
+ * every message sent before the signal, under either `vscode-jsonrpc` runtime,
351
+ * so a `closeSession` sent just before the dispose still ends its session as
352
+ * closed. Nothing reports a page or a worker that dies, because the port
353
+ * cannot.
354
+ *
355
+ * **The transport and the connection must come from the same copy of
356
+ * `vscode-jsonrpc`.** The close is scheduled through the runtime abstraction
357
+ * layer of the copy this package imports. If the host builds its connection
358
+ * from another copy, that layer was never installed: the first message it
359
+ * delivers throws, and the peer's close never fires.
360
+ *
361
+ * **Both ends must use this.** A plain `BrowserMessageReader` peer hands the
362
+ * signal to its connection as a message it does not understand, and its own
363
+ * dispose posts nothing, so this end never learns that it went.
364
+ *
365
+ * Starts the port, because a listener added with `addEventListener` does not.
366
+ * From then on a message that arrives before the connection listens is dropped,
367
+ * so listen in the same task.
368
+ */
369
+ export function createMessagePortTransport(port: TransferredMessagePort): PostMessageTransport {
370
+ const channel = new MessagePortChannel(port);
371
+ const reader = new PostMessageReader(channel);
372
+ const writer = new MessagePortWriter(channel);
373
+ port.start();
374
+ return {
375
+ reader,
376
+ writer,
377
+ dispose(): void {
378
+ reader.dispose();
379
+ writer.dispose();
380
+ }
381
+ };
382
+ }
@@ -0,0 +1,281 @@
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 ResolvedMessage, defineMessage, describeError, resolve } from '../messages/primitives';
12
+ import { type RpcProxy, createRpcProxy } from '../rpc';
13
+ import type { DataPort } from './data-port';
14
+
15
+ /**
16
+ * The transport never opened. A complete sentence rather than a fragment: a
17
+ * fragment is nested inside a sentence the framework does not own, so no
18
+ * translator controls the whole and the composition cannot be made to read
19
+ * correctly in every language.
20
+ */
21
+ export const DATA_SERVER_CONNECT_FAILED = defineMessage(
22
+ 'hydranium/protocol/data-server-connect-failed',
23
+ 'Could not connect to the data server: {detail}'
24
+ );
25
+
26
+ export const DATA_SERVER_NOT_READY = defineMessage(
27
+ 'hydranium/protocol/data-server-not-ready',
28
+ 'The data server did not become ready: {detail}'
29
+ );
30
+
31
+ /** The one method a connection needs of any server: its startup gate. */
32
+ export interface ReadyServer {
33
+ waitForReady(): Promise<void>;
34
+ }
35
+
36
+ /**
37
+ * Lifecycle reporting, for a host that raises warm-up UI around the two waits.
38
+ *
39
+ * Nothing fires until something asks for the connection — the generation is
40
+ * built on the first {@link RpcConnection.connected}. A host that wants the
41
+ * sequence at startup drives that call itself, or a workspace where nobody
42
+ * opens a document reports neither the connect nor the readiness its UI waits
43
+ * on.
44
+ */
45
+ export interface RpcConnectionLifecycle {
46
+ /** A generation is opening its transport, including on each reconnect. */
47
+ readonly onConnecting?: () => void;
48
+ /** The server's readiness gate has settled for a generation. */
49
+ readonly onReady?: () => void;
50
+ /**
51
+ * A generation failed to connect or to become ready. The failure is still
52
+ * reported through {@link DataPort.reportError} and still rejects the
53
+ * awaiting caller; this is for a host that also drives its own UI.
54
+ */
55
+ readonly onFailed?: (error: unknown) => void;
56
+ }
57
+
58
+ /** Calls each distinct lifecycle's hooks in order; one passed twice is called once. */
59
+ function composeLifecycles(...lifecycles: (RpcConnectionLifecycle | undefined)[]): RpcConnectionLifecycle {
60
+ const present = [...new Set(lifecycles)].filter((lifecycle): lifecycle is RpcConnectionLifecycle => lifecycle !== undefined);
61
+ return {
62
+ onConnecting: () => present.forEach(lifecycle => lifecycle.onConnecting?.()),
63
+ onReady: () => present.forEach(lifecycle => lifecycle.onReady?.()),
64
+ onFailed: error => present.forEach(lifecycle => lifecycle.onFailed?.(error))
65
+ };
66
+ }
67
+
68
+ /** Everything {@link RpcConnection} needs once a subclass has resolved its defaults. */
69
+ export interface ResolvedRpcConnectionOptions<TClient extends object> {
70
+ readonly methodNamespace: string;
71
+ readonly clientMethods: readonly (keyof TClient & string)[];
72
+ readonly lifecycle: RpcConnectionLifecycle;
73
+ }
74
+
75
+ /**
76
+ * One connection generation: its connection, its proxy, and its readiness.
77
+ *
78
+ * Named by the `protected` reconnect seams {@link RpcConnection.currentGeneration}
79
+ * and {@link RpcConnection.awaitReady}, so an override has to name it too.
80
+ */
81
+ export interface RpcConnectionGeneration<TServer extends object> {
82
+ readonly connection: Promise<MessageConnection>;
83
+ readonly server: RpcProxy<TServer>;
84
+ /** Set on first use; the shared readiness gate for this generation. */
85
+ ready?: Promise<void>;
86
+ }
87
+
88
+ /**
89
+ * One JSON-RPC connection to a head, with the three jobs every host adapter
90
+ * would otherwise re-derive above {@link DataPort}:
91
+ *
92
+ * 1. **Build the typed proxy** over the port's connection, with the caller's
93
+ * wire prefix and client-method allowlist.
94
+ * 2. **Own the readiness gate** — `waitForReady` once per connection, shared
95
+ * across concurrent callers. A client can connect before the workspace walk
96
+ * finishes, and an early request is then answered correctly from an empty
97
+ * registry, which reads as a broken project tier rather than as a race.
98
+ * 3. **Own the reconnect policy**, by dropping its generation when the port
99
+ * disposes and building a fresh one on the next request.
100
+ *
101
+ * Bounded only by {@link ReadyServer}, so a head serving a slice of the data
102
+ * protocol — diagnostics alone, or one with methods excluded — is still a
103
+ * legal server here. `DataConnection` narrows the bound because its sessions
104
+ * call the document methods; nothing at this layer does.
105
+ */
106
+ export class RpcConnection<TServer extends ReadyServer, TClient extends object> {
107
+ protected readonly methodNamespace: string;
108
+ protected readonly clientMethods: readonly (keyof TClient & string)[];
109
+ protected readonly lifecycle: RpcConnectionLifecycle;
110
+ /** The current generation, or `undefined` before the first request / after a teardown. */
111
+ protected generation?: RpcConnectionGeneration<TServer>;
112
+ protected disposed = false;
113
+ protected readonly portDisposeListener: { dispose(): void };
114
+
115
+ constructor(
116
+ protected readonly port: DataPort,
117
+ protected readonly client: TClient,
118
+ options: ResolvedRpcConnectionOptions<TClient>
119
+ ) {
120
+ this.methodNamespace = options.methodNamespace;
121
+ this.clientMethods = options.clientMethods;
122
+ this.lifecycle = composeLifecycles(port.connectionLifecycle, options.lifecycle);
123
+ this.portDisposeListener = this.port.onDispose(() => this.dropGeneration());
124
+ }
125
+
126
+ /**
127
+ * The connected, READY server proxy.
128
+ *
129
+ * Returns the proxy rather than `void` on purpose. A reconnect replaces the
130
+ * proxy, so a caller that cached one from an earlier call would go on
131
+ * addressing a dead connection with no error — handing it back per call
132
+ * makes the stale reference unrepresentable.
133
+ *
134
+ * Concurrent callers share one readiness promise, so `waitForReady` is
135
+ * awaited once per generation and not once per caller.
136
+ */
137
+ async connected(): Promise<RpcProxy<TServer>> {
138
+ this.assertLive();
139
+ const generation = this.currentGeneration();
140
+ if (!generation.ready) {
141
+ generation.ready = this.awaitReady(generation);
142
+ }
143
+ await generation.ready;
144
+ // Dropped while it opened: its transport is gone, so the call goes to the
145
+ // next generation, or rejects once this connection is disposed.
146
+ if (this.generation !== generation) {
147
+ return this.connected();
148
+ }
149
+ return generation.server;
150
+ }
151
+
152
+ /**
153
+ * The current generation's proxy WITHOUT awaiting readiness — calls queue
154
+ * against the connection promise.
155
+ *
156
+ * **Protected, and that is the point.** An early request is answered
157
+ * correctly from a registry the workspace walk has not filled yet, which
158
+ * reads as a broken project tier rather than as a race — so every caller has
159
+ * to interpose the gate, and every caller forgetting to is a silent bug.
160
+ * {@link connected} is the public route and returns this same proxy once the
161
+ * gate has settled, which leaves nothing to forget. The one caller that
162
+ * cannot use it is {@link awaitReady}, whose whole job is running the gate.
163
+ *
164
+ * Read per access, never cached: a reconnect replaces the generation, and a
165
+ * held reference would address the dead one.
166
+ */
167
+ protected get server(): RpcProxy<TServer> {
168
+ this.assertLive();
169
+ return this.currentGeneration().server;
170
+ }
171
+
172
+ /**
173
+ * Surface a failure the way this host does, through the port's sink.
174
+ *
175
+ * Here rather than only on the port so every participant sharing the
176
+ * connection reports through one route without being handed the transport.
177
+ */
178
+ reportError(error: unknown, reported: ResolvedMessage): void {
179
+ this.port.reportError(error, reported);
180
+ }
181
+
182
+ /** Tear down the current connection and stop tracking the port. Idempotent. */
183
+ dispose(): void {
184
+ if (this.disposed) {
185
+ return;
186
+ }
187
+ this.disposed = true;
188
+ this.portDisposeListener.dispose();
189
+ this.dropGeneration();
190
+ }
191
+
192
+ /** The live generation, building one if there is none. */
193
+ protected currentGeneration(): RpcConnectionGeneration<TServer> {
194
+ if (this.generation) {
195
+ return this.generation;
196
+ }
197
+ this.lifecycle.onConnecting?.();
198
+ const connection = this.port.connect();
199
+ // Rejection is reported here rather than left to float: an unhandled
200
+ // rejection on a connection promise is the failure mode that reads as
201
+ // "the model is empty" instead of "the transport never opened".
202
+ connection.catch((error: unknown) =>
203
+ this.port.reportError(error, resolve(DATA_SERVER_CONNECT_FAILED, { detail: describeError(error) }))
204
+ );
205
+ const server = createRpcProxy<TServer, TClient>(connection, {
206
+ methodNamespace: this.methodNamespace,
207
+ localTarget: this.localTarget(),
208
+ localMethods: this.clientMethods
209
+ });
210
+ this.generation = { connection, server };
211
+ return this.generation;
212
+ }
213
+
214
+ /**
215
+ * The object whose {@link clientMethods} answer the server on each new
216
+ * generation: {@link client} itself. A subclass that must see a call before
217
+ * the client does returns an object that forwards each of them to it.
218
+ */
219
+ protected localTarget(): TClient {
220
+ return this.client;
221
+ }
222
+
223
+ /** Await the connection and the server's startup gate for one generation. */
224
+ protected async awaitReady(generation: RpcConnectionGeneration<TServer>): Promise<void> {
225
+ try {
226
+ await generation.connection;
227
+ // Dropped meanwhile: writing the request would hit a closed transport.
228
+ if (this.generation !== generation) {
229
+ return;
230
+ }
231
+ await generation.server.waitForReady();
232
+ // Before `onReady`, so a failing restore reports a failure alone.
233
+ this.generationReady(generation);
234
+ this.lifecycle.onReady?.();
235
+ } catch (error: unknown) {
236
+ // A dropped generation's request fails with its transport, which says
237
+ // nothing about whether the server is ready.
238
+ if (this.generation !== generation) {
239
+ return;
240
+ }
241
+ this.lifecycle.onFailed?.(error);
242
+ // Drop the generation so the next request retries rather than
243
+ // re-awaiting a settled rejection forever.
244
+ if (this.generation === generation) {
245
+ this.generation = undefined;
246
+ }
247
+ this.port.reportError(error, resolve(DATA_SERVER_NOT_READY, { detail: describeError(error) }));
248
+ throw error;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Called once `generation` has passed its readiness gate, whichever request
254
+ * brought it up. A generation can replace one that failed at readiness
255
+ * without {@link dropGeneration}, so this, not a drop, is where state kept
256
+ * on the server is put back. It runs inside the readiness gate, so a throw
257
+ * from an override fails the generation as a refused gate would.
258
+ */
259
+ protected generationReady(_generation: RpcConnectionGeneration<TServer>): void {
260
+ // Nothing to put back at this layer.
261
+ }
262
+
263
+ /**
264
+ * Discard the current generation, disposing its connection if it opened.
265
+ * The next {@link connected} builds a fresh one.
266
+ */
267
+ protected dropGeneration(): void {
268
+ const generation = this.generation;
269
+ this.generation = undefined;
270
+ if (!generation) {
271
+ return;
272
+ }
273
+ generation.connection.then(connection => connection.dispose()).catch(() => undefined);
274
+ }
275
+
276
+ protected assertLive(): void {
277
+ if (this.disposed) {
278
+ throw new Error(`${this.constructor.name} is disposed`);
279
+ }
280
+ }
281
+ }