@hydranium/protocol 1.0.0-next.37 → 1.0.0-next.370

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