@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,231 @@
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
+ * Per-operation latency/throughput accumulator — the time/throughput axis of a
12
+ * profiling run (the CPU profile says "where time went in aggregate"; this says
13
+ * "method X ran N times at p99 Y ms"). It sits on the framework's timing
14
+ * primitives (`Clock`), not on a Node inspector session, so it is neutral and
15
+ * lives here in `protocol`: the RPC binder feeds it at one chokepoint and the
16
+ * LSP connection decorator feeds it at another, both cross-head. A running head
17
+ * holds one collector and reads `report()` out (e.g. over the diagnostics wire
18
+ * or into a bundle's `server-latency.json`).
19
+ */
20
+
21
+ import { type Clock, type Stopwatch, SystemClock } from './clock.js';
22
+
23
+ /** Latency/throughput for one method over the collection window. */
24
+ export interface MethodLatency {
25
+ /**
26
+ * Whatever key the caller passed to {@link LatencyCollector.record} or
27
+ * {@link LatencyCollector.time} — the framework feeds it RPC and LSP method
28
+ * names, but nothing enforces that, so two feeds using different spellings
29
+ * for one operation report as two methods.
30
+ */
31
+ method: string;
32
+ /**
33
+ * Calls since construction or the last {@link LatencyCollector.reset}, which
34
+ * is the only thing that clears it. Unaffected by retention: a ring-buffer
35
+ * collector keeps counting long after it stops keeping samples. Failed calls
36
+ * are counted — a call that threw still consumed time.
37
+ */
38
+ count: number;
39
+ /**
40
+ * Milliseconds, nearest-rank over the RETAINED samples rather than over
41
+ * {@link count} calls, so under `ring-buffer` retention it describes only the
42
+ * trailing window and can sit well below {@link maxMs}. `0` means no samples
43
+ * were retained, not that calls were instant.
44
+ */
45
+ p50Ms: number;
46
+ /** Milliseconds, nearest-rank; same retention caveat as {@link p50Ms}. */
47
+ p99Ms: number;
48
+ /**
49
+ * Milliseconds, the slowest single call since the last reset. A lifetime
50
+ * figure, so a spike that has aged out of a ring buffer still shows here
51
+ * after the percentiles have forgotten it.
52
+ */
53
+ maxMs: number;
54
+ /**
55
+ * Milliseconds summed across every call since the last reset, retention
56
+ * notwithstanding. This is what {@link LatencyCollector.report} ranks on, so
57
+ * it orders by total cost rather than by per-call slowness.
58
+ */
59
+ totalMs: number;
60
+ }
61
+
62
+ /** A collector snapshot: the window length and the per-method latencies, hottest first. */
63
+ export interface LatencyReport {
64
+ /**
65
+ * Milliseconds elapsed since construction or the last
66
+ * {@link LatencyCollector.reset}, off the monotonic stopwatch. It bounds the
67
+ * throughput a caller can derive; it is not the sum of the method totals,
68
+ * which overlap and can exceed it under concurrency.
69
+ */
70
+ windowMs: number;
71
+ /**
72
+ * Ordered by {@link MethodLatency.totalMs} descending. A method that was
73
+ * never called is absent rather than present with zeroes, so an empty array
74
+ * means nothing ran in the window.
75
+ */
76
+ methods: MethodLatency[];
77
+ }
78
+
79
+ /**
80
+ * How many samples a collector retains for percentile estimation. `keep-all`
81
+ * (the default) stores every duration for exact nearest-rank percentiles — sized
82
+ * for a bounded diagnostics window that gets read then {@link LatencyCollector.reset}.
83
+ * `ring-buffer` caps retention at `maxSamplesPerMethod`, keeping only the most
84
+ * recent samples so a collector left running indefinitely cannot grow without
85
+ * bound; percentiles then cover that trailing window while `count`/`totalMs`/
86
+ * `maxMs` remain lifetime totals.
87
+ */
88
+ export type LatencyRetention = { readonly kind: 'keep-all' } | { readonly kind: 'ring-buffer'; readonly maxSamplesPerMethod: number };
89
+
90
+ /** Per-method state: retained samples (bounded in ring-buffer mode) plus lifetime totals. */
91
+ interface MethodAccumulator {
92
+ /** Retained durations for percentile estimation; capped in ring-buffer mode. Order is irrelevant (report sorts). */
93
+ readonly samples: number[];
94
+ /** Next slot to overwrite once the ring buffer is full (ring-buffer mode only). */
95
+ writeIndex: number;
96
+ /** Lifetime call count, unaffected by retention. */
97
+ count: number;
98
+ /** Lifetime total duration, unaffected by retention. */
99
+ totalMs: number;
100
+ /** Lifetime maximum duration, so a spike that ages out of the window still shows. */
101
+ maxMs: number;
102
+ }
103
+
104
+ /** Nearest-rank percentile of an ascending-sorted array (empty → 0). */
105
+ function percentile(sortedAscending: readonly number[], percent: number): number {
106
+ if (sortedAscending.length === 0) {
107
+ return 0;
108
+ }
109
+ const rank = Math.ceil((percent / 100) * sortedAscending.length);
110
+ return sortedAscending[Math.min(sortedAscending.length - 1, Math.max(0, rank - 1))];
111
+ }
112
+
113
+ /**
114
+ * Accumulates per-method call durations and reports count + latency percentiles.
115
+ * Exact (keeps every duration per method) — sized for a diagnostics window, not
116
+ * unbounded production telemetry; {@link reset} drops the samples and restarts
117
+ * the window. Feed it via {@link time} (wraps a call) or {@link record} (a
118
+ * pre-measured duration). Timing rides an injectable {@link Clock} so tests are
119
+ * deterministic on a fake clock.
120
+ */
121
+ export class LatencyCollector {
122
+ protected readonly durations = new Map<string, MethodAccumulator>();
123
+ protected window: Stopwatch;
124
+
125
+ constructor(
126
+ protected readonly clock: Clock = new SystemClock(),
127
+ protected readonly retention: LatencyRetention = { kind: 'keep-all' }
128
+ ) {
129
+ if (retention.kind === 'ring-buffer' && retention.maxSamplesPerMethod < 1) {
130
+ throw new RangeError(`ring-buffer maxSamplesPerMethod must be >= 1, got ${retention.maxSamplesPerMethod}`);
131
+ }
132
+ this.window = this.clock.stopwatch();
133
+ }
134
+
135
+ /** Record a pre-measured call duration (ms) for `method`. */
136
+ record(method: string, durationMs: number): void {
137
+ let accumulator = this.durations.get(method);
138
+ if (!accumulator) {
139
+ accumulator = { samples: [], writeIndex: 0, count: 0, totalMs: 0, maxMs: 0 };
140
+ this.durations.set(method, accumulator);
141
+ }
142
+ accumulator.count++;
143
+ accumulator.totalMs += durationMs;
144
+ if (durationMs > accumulator.maxMs) {
145
+ accumulator.maxMs = durationMs;
146
+ }
147
+ if (this.retention.kind === 'ring-buffer' && accumulator.samples.length >= this.retention.maxSamplesPerMethod) {
148
+ // Buffer full: overwrite the oldest slot in O(1) (percentiles sort, so order does not matter).
149
+ accumulator.samples[accumulator.writeIndex] = durationMs;
150
+ accumulator.writeIndex = (accumulator.writeIndex + 1) % this.retention.maxSamplesPerMethod;
151
+ } else {
152
+ accumulator.samples.push(durationMs);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Run `call`, record how long it took under `method`, and return its result.
158
+ * The duration is recorded even when the call throws/rejects (a failed call
159
+ * still consumed time), then the error propagates.
160
+ */
161
+ time<T>(method: string, call: () => Promise<T>): Promise<T>;
162
+ time<T>(method: string, call: () => T): T;
163
+ time<T>(method: string, call: () => T | Promise<T>): T | Promise<T> {
164
+ const stopwatch = this.clock.stopwatch();
165
+ const finish = (): void => this.record(method, stopwatch.elapsedMs);
166
+ let result: T | Promise<T>;
167
+ try {
168
+ result = call();
169
+ } catch (error) {
170
+ finish();
171
+ throw error;
172
+ }
173
+ if (result instanceof Promise) {
174
+ return result.then(
175
+ value => {
176
+ finish();
177
+ return value;
178
+ },
179
+ error => {
180
+ finish();
181
+ throw error;
182
+ }
183
+ );
184
+ }
185
+ finish();
186
+ return result;
187
+ }
188
+
189
+ /** Non-destructive snapshot: per-method latencies ordered by total time descending (use {@link reset} to drain). */
190
+ report(): LatencyReport {
191
+ const methods: MethodLatency[] = [];
192
+ for (const [method, accumulator] of this.durations) {
193
+ const sorted = [...accumulator.samples].sort((left, right) => left - right);
194
+ methods.push({
195
+ method,
196
+ count: accumulator.count,
197
+ totalMs: accumulator.totalMs,
198
+ maxMs: accumulator.maxMs,
199
+ p50Ms: percentile(sorted, 50),
200
+ p99Ms: percentile(sorted, 99)
201
+ });
202
+ }
203
+ methods.sort((left, right) => right.totalMs - left.totalMs);
204
+ return { windowMs: this.window.elapsedMs, methods };
205
+ }
206
+
207
+ /** Drop all samples and restart the window. */
208
+ reset(): void {
209
+ this.durations.clear();
210
+ this.window = this.clock.stopwatch();
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Render a {@link LatencyReport} as a compact ranked table for a log line or a
216
+ * command channel — the latency counterpart to `formatProfileReport`.
217
+ */
218
+ export function formatLatencyReport(report: LatencyReport, label = 'RPC/LSP latency'): string {
219
+ const lines = [`${label} (window ${Math.round(report.windowMs)}ms):`];
220
+ if (report.methods.length === 0) {
221
+ lines.push(' (no calls recorded)');
222
+ return lines.join('\n');
223
+ }
224
+ for (const methodLatency of report.methods) {
225
+ lines.push(
226
+ ` ${methodLatency.method} n=${methodLatency.count} total ${Math.round(methodLatency.totalMs)}ms ` +
227
+ `p50 ${Math.round(methodLatency.p50Ms)}ms p99 ${Math.round(methodLatency.p99Ms)}ms max ${Math.round(methodLatency.maxMs)}ms`
228
+ );
229
+ }
230
+ return lines.join('\n');
231
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,172 @@
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
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
11
+ export type LogThreshold = 'off' | LogLevel;
12
+
13
+ export const LEVEL_ORDER: Record<LogThreshold, number> = { off: 0, error: 1, warn: 2, info: 3, debug: 4, trace: 5 };
14
+
15
+ /** Default labels rendered in the log line, padded to 5 chars so timestamps line up across sources. */
16
+ export const LEVEL_LABELS: Record<LogLevel, string> = {
17
+ error: 'Error',
18
+ warn: 'Warn',
19
+ info: 'Info',
20
+ debug: 'Debug',
21
+ trace: 'Trace'
22
+ };
23
+
24
+ /** Module-global threshold; shared by every `AbstractLogger` instance so derived child
25
+ * loggers pick up live updates without each having to subscribe to a configuration source. */
26
+ let currentLevel: LogThreshold = 'info';
27
+
28
+ /**
29
+ * Cross-side public logger contract. Every consumer of the framework — browser
30
+ * client extensions, LSP servers, headless CLIs, tests — programs against this
31
+ * interface; concrete implementations supply platform-specific delivery
32
+ * (`AbstractLogger` is the abstract base class that satisfies this contract).
33
+ *
34
+ * Global threshold configuration lives on the {@link Logger} namespace
35
+ * (`Logger.setLevel(...)`, `Logger.getLevel()`, `Logger.isLevelEnabled(...)`).
36
+ *
37
+ * Every emit returns this logger so calls can chain (`logger.error(a).debug(b)`),
38
+ * and every `for` / `sub` / `with` derivation preserves the implementation type.
39
+ */
40
+ export interface Logger {
41
+ /** Log a message at error level. */
42
+ error(message?: string, ...args: unknown[]): this;
43
+ /** Log a message at warn level. */
44
+ warn(message?: string, ...args: unknown[]): this;
45
+ /** Log a message at info level. */
46
+ info(message?: string, ...args: unknown[]): this;
47
+ /** Log a message at debug level. */
48
+ debug(message?: string, ...args: unknown[]): this;
49
+ /** High-volume diagnostic lines, only emitted at the `'trace'` threshold. */
50
+ trace(message?: string, ...args: unknown[]): this;
51
+ /** Log a message at info-level threshold with a `Log` label (rather than `Info`). */
52
+ log(message?: string, ...args: unknown[]): this;
53
+ /** Derive a child logger with `component` as a `Foo :: bar`-style suffix. */
54
+ for(component: string): this;
55
+ /** Derive a child logger by deepening the existing `Foo :: bar :: baz` chain. */
56
+ sub(component: string): this;
57
+ /** Derive a child logger appending `component` as a separate bracket: `[a] [b]`. */
58
+ with(component: string): this;
59
+ /**
60
+ * Derive a child logger labelled with the given URI string. Default behaviour
61
+ * appends the URI as a bracket via {@link with}; subclasses with workspace
62
+ * awareness override to display workspace-relative paths. Subclasses MAY
63
+ * widen the parameter type to accept URI objects directly (TypeScript method
64
+ * parameter bivariance permits this) — callers typed against the {@link Logger}
65
+ * interface stringify their URI at the call site.
66
+ */
67
+ withUri(uri: string): this;
68
+ /**
69
+ * Emit `message` at `threshold`, or do nothing when `threshold === 'off'`.
70
+ * Lets a caller holding a {@link LogThreshold} emit without a hand-written
71
+ * `if (level === 'off') return` guard.
72
+ */
73
+ logAt(threshold: LogThreshold, message: string): this;
74
+ }
75
+
76
+ /**
77
+ * Companion namespace for the {@link Logger} interface holding global
78
+ * threshold configuration.
79
+ *
80
+ * The threshold is process-wide: every {@link Logger} instance shares one
81
+ * value, so adopters don't have to wire it through each logger they
82
+ * construct. The timing facilities that build on a logger — `Tracer.time` /
83
+ * `Tracer.startTimer` — consult {@link Logger.isLevelEnabled} internally to
84
+ * short-circuit when the configured level suppresses output.
85
+ */
86
+ export namespace Logger {
87
+ /** Set the process-wide log threshold. Every {@link Logger} instance picks up the new value live. */
88
+ export function setLevel(level: LogThreshold): void {
89
+ currentLevel = level;
90
+ }
91
+ /** Read the current process-wide log threshold. */
92
+ export function getLevel(): LogThreshold {
93
+ return currentLevel;
94
+ }
95
+ /**
96
+ * Whether a message at `level` would be emitted at the current threshold.
97
+ * Use to guard expensive log-line construction:
98
+ * `if (Logger.isLevelEnabled('trace')) logger.trace(buildPayload())`.
99
+ */
100
+ export function isLevelEnabled(level: LogLevel): boolean {
101
+ return LEVEL_ORDER[level] <= LEVEL_ORDER[currentLevel];
102
+ }
103
+ /**
104
+ * Whether a message at `threshold` would be emitted: `false` for `'off'`,
105
+ * otherwise tracks {@link isLevelEnabled}. Use to gate work behind a
106
+ * {@link LogThreshold} (which {@link isLevelEnabled} cannot accept).
107
+ */
108
+ export function isThresholdEnabled(threshold: LogThreshold): boolean {
109
+ return threshold !== 'off' && isLevelEnabled(threshold);
110
+ }
111
+ }
112
+
113
+ /** True when `value` is one of `'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace'`. */
114
+ export function isLogThreshold(value: unknown): value is LogThreshold {
115
+ return typeof value === 'string' && value in LEVEL_ORDER;
116
+ }
117
+
118
+ /**
119
+ * Parse a string-typed configuration value into a {@link LogThreshold}, returning
120
+ * `undefined` for unset or invalid inputs. Case-insensitive — `'WARN'` / `'warn'`
121
+ * / `'Warn'` all normalise to `'warn'`. Use to consume LSP / env-var / CLI inputs
122
+ * without exposing callers to the case-fold detail.
123
+ */
124
+ export function parseLogLevel(value: unknown): LogThreshold | undefined {
125
+ const normalised = typeof value === 'string' ? value.toLowerCase() : undefined;
126
+ return normalised && isLogThreshold(normalised) ? normalised : undefined;
127
+ }
128
+
129
+ /**
130
+ * Names of the environment variables the server consults for its launch-time
131
+ * log configuration. They live in the shared protocol layer because they are
132
+ * the *contract* between whoever launches a server (the CLI, a test harness, a
133
+ * container) and the server that reads them — both sides reference one constant
134
+ * instead of duplicating the literal string.
135
+ */
136
+ export const DEFAULT_LOG_LEVEL_ENV = 'HYDRANIUM_LOG_LEVEL';
137
+ /** Env var the server reads its log file-tee target from. See {@link DEFAULT_LOG_LEVEL_ENV}. */
138
+ export const DEFAULT_LOG_FILE_ENV = 'HYDRANIUM_LOG_FILE';
139
+
140
+ /**
141
+ * Human-readable formatting helpers used in log lines and diagnostic output.
142
+ */
143
+ export namespace Format {
144
+ /** Format a date as `HH:MM:SS.mmm`. Defaults to `now`. */
145
+ export function timestamp(d: Date = new Date()): string {
146
+ const hh = String(d.getHours()).padStart(2, '0');
147
+ const mm = String(d.getMinutes()).padStart(2, '0');
148
+ const ss = String(d.getSeconds()).padStart(2, '0');
149
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
150
+ return `${hh}:${mm}:${ss}.${ms}`;
151
+ }
152
+
153
+ /** Format a millisecond duration: `<1s` as `Nms`, `>=1s` as `N.NNs`. */
154
+ export function elapsed(ms: number): string {
155
+ return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(2)}s`;
156
+ }
157
+
158
+ /** Format a byte count with a human-readable unit suffix (B/KB/MB/GB/TB). */
159
+ export function bytes(count: number): string {
160
+ if (count < 1024) {
161
+ return `${count}B`;
162
+ }
163
+ const units = ['KB', 'MB', 'GB', 'TB'];
164
+ let value = count / 1024;
165
+ let unit = 0;
166
+ while (value >= 1024 && unit < units.length - 1) {
167
+ value /= 1024;
168
+ unit++;
169
+ }
170
+ return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)}${units[unit]}`;
171
+ }
172
+ }