@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,48 @@
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
+ /**
11
+ * Validates that a wire-name prefix — the `methodNamespace` option shared
12
+ * by `bindRpcMethods` and
13
+ * `createRpcProxy` — carries a
14
+ * trailing `/`.
15
+ *
16
+ * **Why this exists.** Adopters reading the LSP-namespace analogy
17
+ * (`textDocument/*`, `workspace/*`) naturally pass `'mylang'` expecting
18
+ * `'mylang/getModelDocument'` on the wire. Concatenating verbatim instead
19
+ * yields `'mylanggetModelDocument'`, and because the proxy and binding sites
20
+ * pick the wrong prefix in lockstep the mismatch is silent on a single-process
21
+ * adopter, audible only as a per-method "Unhandled method" on the wire log.
22
+ *
23
+ * Throwing is preferred over coercion: coercion would disguise the case where
24
+ * the adopter intended a different separator (`'.'`, `':'`), and throwing at
25
+ * construction time surfaces the bug at the adopter's own callsite rather than
26
+ * at the first wire response.
27
+ *
28
+ * Accepts the empty string (explicit no-prefix mode) or any non-empty string
29
+ * ending with `/`; anything else throws {@link TypeError} synchronously.
30
+ *
31
+ * @param methodNamespace The configured prefix (e.g. `'mylang/'`,
32
+ * `'data-server/'`, `''`).
33
+ * @param caller Name of the validating function, embedded in the thrown
34
+ * message so the error reads as if it came from the adopter-facing
35
+ * function rather than this helper.
36
+ */
37
+ export function assertValidMethodNamespace(methodNamespace: string, caller: string): void {
38
+ if (methodNamespace === '' || methodNamespace.endsWith('/')) {
39
+ return;
40
+ }
41
+ throw new TypeError(
42
+ `${caller}: methodNamespace '${methodNamespace}' must end with '/' (or be empty). ` +
43
+ "Adopters reading the LSP-namespace analogy ('textDocument/*', 'workspace/*') often " +
44
+ `forget the trailing slash, producing wire names like '${methodNamespace}someMethod' ` +
45
+ `that do not match handler bindings under '${methodNamespace}/someMethod'. ` +
46
+ `Either pass '${methodNamespace}/' or use the empty string for no prefix.`
47
+ );
48
+ }
@@ -0,0 +1,178 @@
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
+ /**
11
+ * The two CLIENT-side doubles of the data head: a {@link DataPort} whose
12
+ * transport the test supplies, and a {@link DataClientProtocol} that records
13
+ * what arrived.
14
+ *
15
+ * # Why standalone, when a harness already builds one
16
+ *
17
+ * `makeDataServerHarness` builds a capturing client too, but fused into a
18
+ * server-bearing harness: taking it means taking a duplex pair, a `DataServer`
19
+ * and a proxy. That is the right shape for testing the server. It is the wrong
20
+ * shape — and not available at all — for testing the CLIENT half: a
21
+ * `DataSession`, a widget model, a host's port adapter. Those need to stand a
22
+ * double on one side of the boundary and something else entirely on the other,
23
+ * which is why both halves get hand-rolled per suite instead.
24
+ *
25
+ * # Runner-agnostic, deliberately
26
+ *
27
+ * No `jest.fn`, no `vi.fn`, no `expect`. The recorded arrays ARE the assertion
28
+ * surface, so the same double works under vitest, jest and a bare script. A
29
+ * mock-framework double would also make "was it called" the observable, when
30
+ * what a data-head test actually asserts is the CONTENT of what arrived —
31
+ * which `sourceClientId`, which version, which reason.
32
+ */
33
+
34
+ import { Emitter, type MessageConnection } from 'vscode-jsonrpc';
35
+ import type { DataPort } from '../client/data-port';
36
+ import type { DataClientProtocol } from '../data/data-server-protocol';
37
+ import type { ProjectsChangedEvent, TransferDocumentSavedEvent, TransferDocumentUpdatedEvent } from '../data/events';
38
+ import type { Project } from '../project';
39
+ import type { TransferDiagnostic } from '../transfer-diagnostic';
40
+ import type { TransferElement } from '../transfer-element';
41
+
42
+ /** Inputs to {@link makeFakeDataPort}. */
43
+ export interface FakeDataPortOptions {
44
+ /**
45
+ * Establish one transport generation and hand back a LISTENING connection —
46
+ * the port's whole contract. Called once per generation, so a test that
47
+ * asserts on reconnection must return a FRESH connection each time rather
48
+ * than closing over one.
49
+ *
50
+ * Throwing (or rejecting) here is the transport-construction failure path,
51
+ * which the consumer surfaces through {@link FakeDataPort.reported}.
52
+ */
53
+ connect(): MessageConnection | Promise<MessageConnection>;
54
+ /**
55
+ * Stable client identity. Defaults to `'fake-data-port'`, which avoids the
56
+ * three sentinels the framework reserves (`'language-client'`, `'unknown'`,
57
+ * `'revert-on-close'`); override it when a test needs two distinguishable
58
+ * clients on one server.
59
+ */
60
+ clientId?: string;
61
+ }
62
+
63
+ /** A {@link DataPort} that records what passed through it. */
64
+ export interface FakeDataPort extends DataPort {
65
+ /**
66
+ * Every connection {@link FakeDataPortOptions.connect} handed back, in
67
+ * order. Read from outside: "one connection shared across concurrent
68
+ * callers" and "a fresh connection after a teardown" are assertions about
69
+ * this length and about nothing else observable — a session that memoised a
70
+ * rejected promise, for instance, differs from one that retries ONLY here.
71
+ */
72
+ readonly connections: readonly MessageConnection[];
73
+ /**
74
+ * Every {@link DataPort.reportError} call, in order. Read from outside: this
75
+ * is the only place a transport failure surfaces, so a test for the failure
76
+ * path asserts on the `context` string here rather than on a rejection that
77
+ * the consumer may legitimately swallow.
78
+ */
79
+ readonly reported: readonly { readonly error: unknown; readonly context: string }[];
80
+ /**
81
+ * Fire {@link DataPort.onDispose} — the host tearing the transport down, a
82
+ * language-server restart being the case that forces the event to exist.
83
+ * Drives the consumer's reconnection path, which has no other trigger.
84
+ */
85
+ fireDispose(): void;
86
+ /** Release the emitter. Idempotent; does not close the connections, which the test owns. */
87
+ dispose(): void;
88
+ }
89
+
90
+ /**
91
+ * A {@link DataPort} over a transport the caller supplies.
92
+ *
93
+ * It deliberately does not create the transport itself. What varies between the
94
+ * cases worth testing is exactly that: an in-process duplex pair, a socket, a
95
+ * connection with no server behind it, a different one per generation.
96
+ */
97
+ export function makeFakeDataPort(options: FakeDataPortOptions): FakeDataPort {
98
+ const connections: MessageConnection[] = [];
99
+ const reported: { error: unknown; context: string }[] = [];
100
+ const disposeEmitter = new Emitter<void>();
101
+ return {
102
+ clientId: options.clientId ?? 'fake-data-port',
103
+ connections,
104
+ reported,
105
+ onDispose: disposeEmitter.event,
106
+ async connect(): Promise<MessageConnection> {
107
+ const connection = await options.connect();
108
+ connections.push(connection);
109
+ return connection;
110
+ },
111
+ reportError(error: unknown, context: string): void {
112
+ reported.push({ error, context });
113
+ },
114
+ fireDispose(): void {
115
+ disposeEmitter.fire(undefined);
116
+ },
117
+ dispose(): void {
118
+ disposeEmitter.dispose();
119
+ }
120
+ };
121
+ }
122
+
123
+ /** A {@link DataClientProtocol} plus the arrays it records into. */
124
+ export interface CapturingDataClient<
125
+ TTransfer extends TransferElement,
126
+ TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
127
+ TProject extends Project = Project
128
+ > {
129
+ /** The client to hand to a `DataSession` or bind as an RPC `localTarget`. */
130
+ readonly client: DataClientProtocol<TTransfer, TDiagnostic, TProject>;
131
+ /** Every `onDocumentUpdated` event, in arrival order. */
132
+ readonly updates: TransferDocumentUpdatedEvent<TTransfer, TDiagnostic>[];
133
+ /** Every `onDocumentSaved` event, in arrival order. */
134
+ readonly saves: TransferDocumentSavedEvent<TTransfer, TDiagnostic>[];
135
+ /** Every `onProjectsChanged` event, in arrival order. */
136
+ readonly projectsChanges: ProjectsChangedEvent<TProject>[];
137
+ }
138
+
139
+ /**
140
+ * A {@link DataClientProtocol} that records every notification it receives.
141
+ *
142
+ * Recording ALL THREE channels even when a suite reads one is deliberate: an
143
+ * event delivered on the wrong channel is a real defect of the data head, and a
144
+ * double that drops the other two turns it into silence on the one being
145
+ * watched.
146
+ *
147
+ * An override replaces a channel's recording rather than supplementing it, so
148
+ * the corresponding array stays empty — that is what makes an override usable
149
+ * as a barrier (rejecting, counting differently, throwing) rather than only as
150
+ * a spy.
151
+ */
152
+ export function makeCapturingDataClient<
153
+ TTransfer extends TransferElement,
154
+ TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
155
+ TProject extends Project = Project
156
+ >(overrides: Partial<DataClientProtocol<TTransfer, TDiagnostic, TProject>> = {}): CapturingDataClient<TTransfer, TDiagnostic, TProject> {
157
+ const updates: TransferDocumentUpdatedEvent<TTransfer, TDiagnostic>[] = [];
158
+ const saves: TransferDocumentSavedEvent<TTransfer, TDiagnostic>[] = [];
159
+ const projectsChanges: ProjectsChangedEvent<TProject>[] = [];
160
+ const client: DataClientProtocol<TTransfer, TDiagnostic, TProject> = {
161
+ onDocumentUpdated:
162
+ overrides.onDocumentUpdated ??
163
+ (event => {
164
+ updates.push(event);
165
+ }),
166
+ onDocumentSaved:
167
+ overrides.onDocumentSaved ??
168
+ (event => {
169
+ saves.push(event);
170
+ }),
171
+ onProjectsChanged:
172
+ overrides.onProjectsChanged ??
173
+ (event => {
174
+ projectsChanges.push(event);
175
+ })
176
+ };
177
+ return { client, updates, saves, projectsChanges };
178
+ }
@@ -0,0 +1,106 @@
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 Clock, type Stopwatch, type Timed } from '../clock';
11
+ import { Disposable } from '../util';
12
+
13
+ /** A {@link Clock} whose time only moves when the test calls {@link FakeClock.advance}. */
14
+ export interface FakeClock extends Clock {
15
+ /**
16
+ * Move virtual time forward by `ms`, firing every {@link Clock.setTimer}
17
+ * whose deadline falls within the window — in chronological order, each at
18
+ * its own deadline (so `now()` inside a callback reads the fire instant, not
19
+ * the advance target). Timers scheduled by a callback fire too if they fall
20
+ * within the same window.
21
+ */
22
+ advance(ms: number): void;
23
+ }
24
+
25
+ interface FakeTimer {
26
+ readonly at: number;
27
+ readonly callback: () => void;
28
+ disposed: boolean;
29
+ fired: boolean;
30
+ }
31
+
32
+ /**
33
+ * In-process {@link Clock} double for deterministic time. The real
34
+ * `SystemClock` has two underlying sources
35
+ * (`Date.now` for `now()`, `performance.now()` for the stopwatch); the fake
36
+ * collapses them onto ONE virtual time axis, so a single
37
+ * {@link FakeClock.advance} drives `now()`, every live stopwatch's elapsed, and
38
+ * any due timer together.
39
+ *
40
+ * Server-free and DI-free — lives in `@hydranium/protocol/testing` so every
41
+ * head can bind it without pulling in a server package.
42
+ */
43
+ export function makeFakeClock(options: { now?: number } = {}): FakeClock {
44
+ let current = options.now ?? 0;
45
+ const timers: FakeTimer[] = [];
46
+
47
+ const makeStopwatch = (): Stopwatch => {
48
+ const start = current;
49
+ let lastLap = current;
50
+ let stopped: number | undefined;
51
+ return {
52
+ get elapsedMs(): number {
53
+ return (stopped ?? current) - start;
54
+ },
55
+ lap(): number {
56
+ const at = stopped ?? current;
57
+ const split = at - lastLap;
58
+ lastLap = at;
59
+ return split;
60
+ },
61
+ stop(): number {
62
+ if (stopped === undefined) {
63
+ stopped = current;
64
+ }
65
+ return stopped - start;
66
+ }
67
+ };
68
+ };
69
+
70
+ return {
71
+ now: () => current,
72
+ setTimer(callback: () => void, ms: number): Disposable {
73
+ const timer: FakeTimer = { at: current + ms, callback, disposed: false, fired: false };
74
+ timers.push(timer);
75
+ return Disposable.create(() => {
76
+ timer.disposed = true;
77
+ });
78
+ },
79
+ stopwatch: makeStopwatch,
80
+ measure: (<T>(callback: () => T | Promise<T>): Timed<T> | Promise<Timed<T>> => {
81
+ const stopwatch = makeStopwatch();
82
+ const result = callback();
83
+ if (result instanceof Promise) {
84
+ return (result as Promise<T>).then(value => ({ result: value, elapsedMs: stopwatch.elapsedMs }));
85
+ }
86
+ return { result: result as T, elapsedMs: stopwatch.elapsedMs };
87
+ }) as Clock['measure'],
88
+ advance(ms: number): void {
89
+ const target = current + ms;
90
+ // Fire due timers one at a time, re-scanning after each so a callback
91
+ // that schedules a nearer timer still fires within this window.
92
+ for (;;) {
93
+ const next = timers
94
+ .filter(timer => !timer.disposed && !timer.fired && timer.at <= target)
95
+ .sort((left, right) => left.at - right.at)[0];
96
+ if (!next) {
97
+ break;
98
+ }
99
+ current = next.at;
100
+ next.fired = true;
101
+ next.callback();
102
+ }
103
+ current = target;
104
+ }
105
+ };
106
+ }
@@ -0,0 +1,24 @@
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
+ /**
11
+ * The uniform contract every framework test harness satisfies. A harness is
12
+ * a wired bundle around a system-under-test, driven through a seam; its full
13
+ * shape is `{ <subject>, <seam>, …captureArrays, dispose() }`. This marker
14
+ * pins the one member that is identical across every harness — `dispose()` —
15
+ * so teardown is always `harness.dispose()` regardless of which harness, and
16
+ * a `Harness` reference can release any of them without knowing its concrete
17
+ * type. Each concrete harness interface — per head, and per conformance driver
18
+ * port — `extends Harness` and adds its subject / seam / capture members on
19
+ * top.
20
+ */
21
+ export interface Harness {
22
+ /** Release every resource the harness holds. Idempotent; the uniform teardown hook. */
23
+ dispose(): void;
24
+ }
@@ -0,0 +1,26 @@
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
+ // Subpath barrel for `@hydranium/protocol/testing` — the BROWSER-NEUTRAL shared
11
+ // primitives: `makeFakeClock` (deterministic `Clock` double on one virtual time
12
+ // axis), `waitFor` / `tick`, the client-side data-head doubles
13
+ // (`makeFakeDataPort` / `makeCapturingDataClient`), and the `Harness` marker
14
+ // interface every framework harness extends. Kept out of the main barrel so production bundles don't pull
15
+ // the test scaffolding in by default; adopters opt in by importing from
16
+ // `@hydranium/protocol/testing`.
17
+ //
18
+ // Neutrality is gated (`scripts/check-neutral-bundles.mjs`), which is why the
19
+ // duplex transports are NOT here: a `PassThrough` pair is in their exported
20
+ // type, so they cannot be made portable and live at `./testing/node` instead.
21
+ // The same rule the package surface uses — the portable name is the short one.
22
+
23
+ export * from './data-doubles';
24
+ export * from './fake-clock';
25
+ export * from './harness';
26
+ export * from './wait-for';
@@ -0,0 +1,50 @@
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 { StreamMessageReader, StreamMessageWriter, createMessageConnection, type MessageConnection } from 'vscode-jsonrpc/node';
11
+ import { makeDuplexStreamPair } from './duplex-stream';
12
+
13
+ /**
14
+ * Bidirectional in-process vscode-jsonrpc bridge — two MessageConnections
15
+ * crossed over a PassThrough pair, both already listening. The framework's
16
+ * standard in-process test transport.
17
+ *
18
+ * Typical use: wire a `DataServer` to `pair.left` and exercise it from a
19
+ * client proxy on `pair.right` (or vice versa). Call `dispose` at test
20
+ * teardown to release the underlying streams.
21
+ */
22
+ export interface DuplexConnectionPair {
23
+ readonly left: MessageConnection;
24
+ readonly right: MessageConnection;
25
+ dispose(): void;
26
+ }
27
+
28
+ export function makeDuplexConnectionPair(): DuplexConnectionPair {
29
+ // `left` is the client side, `right` the server side: `clientToServer` is
30
+ // what `left` writes and `right` reads; `serverToClient` is what `right`
31
+ // writes and `left` reads.
32
+ const pair = makeDuplexStreamPair();
33
+ const { clientToServer, serverToClient } = pair;
34
+
35
+ const left = createMessageConnection(new StreamMessageReader(serverToClient), new StreamMessageWriter(clientToServer));
36
+ const right = createMessageConnection(new StreamMessageReader(clientToServer), new StreamMessageWriter(serverToClient));
37
+
38
+ left.listen();
39
+ right.listen();
40
+
41
+ return {
42
+ left,
43
+ right,
44
+ dispose(): void {
45
+ left.dispose();
46
+ right.dispose();
47
+ pair.dispose();
48
+ }
49
+ };
50
+ }
@@ -0,0 +1,51 @@
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 { PassThrough } from 'node:stream';
11
+
12
+ /**
13
+ * A crossed pair of in-process `PassThrough` streams modelling one
14
+ * bidirectional client/server transport. Each `PassThrough` is itself both
15
+ * readable and writable: the side named first writes, the side named second
16
+ * reads off the same object. The shared substrate under the framework's
17
+ * in-process test transports — `makeDuplexConnectionPair` layers a
18
+ * `MessageConnection` pair on top.
19
+ */
20
+ export interface DuplexStreamPair {
21
+ /** Client writes here; server reads here. */
22
+ readonly clientToServer: PassThrough;
23
+ /** Server writes here; client reads here. */
24
+ readonly serverToClient: PassThrough;
25
+ /** Destroy both streams. Idempotent. */
26
+ dispose(): void;
27
+ }
28
+
29
+ /**
30
+ * Build a fresh {@link DuplexStreamPair}. The two streams are independent;
31
+ * data written to one never appears on the other. Call `dispose` at test
32
+ * teardown to release them.
33
+ */
34
+ export function makeDuplexStreamPair(): DuplexStreamPair {
35
+ const clientToServer = new PassThrough();
36
+ const serverToClient = new PassThrough();
37
+
38
+ let disposed = false;
39
+ return {
40
+ clientToServer,
41
+ serverToClient,
42
+ dispose(): void {
43
+ if (disposed) {
44
+ return;
45
+ }
46
+ disposed = true;
47
+ clientToServer.destroy();
48
+ serverToClient.destroy();
49
+ }
50
+ };
51
+ }
@@ -0,0 +1,22 @@
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
+ // Subpath barrel for `@hydranium/protocol/testing/node` — the test transports
11
+ // that need a Node runtime: a crossed `PassThrough` pair and the in-process
12
+ // `MessageConnection` bridge over it.
13
+ //
14
+ // Separate from `./testing` because a `PassThrough` is in `DuplexStreamPair`'s
15
+ // exported type and `StreamMessageReader` comes from `vscode-jsonrpc/node`, so
16
+ // neither can be made portable — and keeping them in the neutral barrel taints
17
+ // every subpath that re-exports it, which is what made three of this repo's
18
+ // `./testing` entries unbundleable for a browser while having no Node code of
19
+ // their own.
20
+
21
+ export * from './duplex-connection';
22
+ export * from './duplex-stream';
@@ -0,0 +1,94 @@
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
+ /**
11
+ * Two test primitives for awaiting in-process asynchrony (RPC notifications
12
+ * crossing the duplex wire, `DocumentBuilder` phase events, etc.) WITHOUT a
13
+ * flaky fixed sleep.
14
+ *
15
+ * Decision rule:
16
+ * - Asserting an effect **will** happen ("eventually N events / a build ran /
17
+ * the doc reached a phase")? → {@link waitFor} with a predicate over the
18
+ * observable. Non-racy: resolves the instant it is true, tolerates a slow CI.
19
+ * - Asserting an effect **did not / has not yet** happened ("no event for an
20
+ * unsubscribed URI", "the gated call has not resolved")? → {@link tick}: a
21
+ * negative cannot be polled, so yield the loop a bounded moment to give the
22
+ * (unwanted) effect a chance, then assert its absence.
23
+ *
24
+ * Default to `waitFor`; reach for `tick` only when the assertion is an absence.
25
+ * A microtask flush (`await Promise.resolve()` / `queue-microtask`) is never the
26
+ * answer here — stream-delivered JSON-RPC notifications arrive on a macrotask/IO
27
+ * turn, after microtasks drain.
28
+ */
29
+
30
+ /** Options for {@link waitFor}. */
31
+ export interface WaitForOptions {
32
+ /** Reject after this many milliseconds if the predicate is still false. Default 2000. */
33
+ readonly timeoutMs?: number;
34
+ /** Poll the predicate every this-many milliseconds. Default 5. */
35
+ readonly intervalMs?: number;
36
+ /** Error message on timeout. Default a generic phrasing. */
37
+ readonly message?: string;
38
+ }
39
+
40
+ /**
41
+ * Resolve once `predicate()` returns true, polling at `intervalMs`; reject with
42
+ * `message` once `timeoutMs` elapses. The **non-racy** alternative to a fixed
43
+ * sleep for awaiting an asynchronous side effect that lands on an observable
44
+ * value.
45
+ *
46
+ * It resolves the instant the condition holds, so it is *faster* than a fixed
47
+ * delay on the happy path, and tolerates a slow event loop up to `timeoutMs`, so
48
+ * it does not flake under parallel CI where a fixed `setTimeout` can be starved
49
+ * past the test timeout.
50
+ *
51
+ * @example
52
+ * await proxy.updateModelDocument({ uri, clientId, model });
53
+ * await waitFor(() => harness.events.length === 1); // the notification crossed
54
+ * expect(harness.events[0].document.uri).toBe(uri);
55
+ */
56
+ export function waitFor(predicate: () => boolean, options: WaitForOptions = {}): Promise<void> {
57
+ const timeoutMs = options.timeoutMs ?? 2000;
58
+ const intervalMs = options.intervalMs ?? 5;
59
+ const message = options.message ?? `Timed out after ${timeoutMs}ms waiting for a condition`;
60
+ if (predicate()) {
61
+ return Promise.resolve();
62
+ }
63
+ return new Promise<void>((resolve, reject) => {
64
+ const deadline = setTimeout(() => {
65
+ clearInterval(poll);
66
+ reject(new Error(message));
67
+ }, timeoutMs);
68
+ const poll = setInterval(() => {
69
+ if (predicate()) {
70
+ clearInterval(poll);
71
+ clearTimeout(deadline);
72
+ resolve();
73
+ }
74
+ }, intervalMs);
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Yield the event loop for `ms` (default 10). The bounded counterpart to
80
+ * {@link waitFor}, for the rare assertion that something has NOT happened — a
81
+ * *negative* you cannot poll positively. Give async delivery a real chance to
82
+ * (wrongly) occur, then assert it did not.
83
+ *
84
+ * A fixed yield is unavoidable here, but the failure mode is a false-green, not
85
+ * the flaky-red a fixed delay causes on a positive wait.
86
+ *
87
+ * @example
88
+ * await proxy.saveModelDocument({ uri, clientId, model }); // no subscriber yet
89
+ * await tick(); // give a (wrongly) fired event a chance to land
90
+ * expect(savedEvents).toHaveLength(0); // assert the ABSENCE
91
+ */
92
+ export function tick(ms = 10): Promise<void> {
93
+ return new Promise<void>(resolve => setTimeout(resolve, ms));
94
+ }