@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
package/src/tracer.ts ADDED
@@ -0,0 +1,277 @@
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, SystemClock } from './clock';
11
+ import { Format, Logger, type LogThreshold } from './logger';
12
+ import { NoopLogger } from './noop-logger';
13
+ import { DefaultProfileSession, type ProfileSession } from './profile-session';
14
+ import { Disposable } from './util';
15
+
16
+ /** Module-global so nested timers don't reuse ids. */
17
+ let nextTimerId = 0;
18
+
19
+ /** Ops completing before this threshold are silent on success — only failures emit a retroactive start. */
20
+ export const DEFER_START_MS = 5;
21
+
22
+ /** Optional settings for {@link Tracer.time}. */
23
+ export interface TimeOptions {
24
+ /** Defer the [start] line until the op has run this long. Set to 0 to always log. */
25
+ logAfterMs?: number;
26
+ /** Called with the correlation id before the op runs. */
27
+ captureId?: (id: number) => void;
28
+ /** Extra context included in the status brackets. Read on each emit so callers can mutate. */
29
+ tags?: string[];
30
+ /** Server-only: force the memory suffix on [done] when elapsed >= this many ms, ignoring the delta gate. */
31
+ forceMemoryAboveMs?: number;
32
+ }
33
+
34
+ /** Platform-specific heap readout. Returns `undefined` where unavailable (e.g. a browser outside Chromium). */
35
+ export interface MemoryInfo {
36
+ /** Bytes currently in use (heap used / `usedJSHeapSize`). */
37
+ usedBytes: number;
38
+ /** Optional capacity for a "used / total" rendering (heap total / `totalJSHeapSize`). */
39
+ totalBytes?: number;
40
+ }
41
+
42
+ /** Reads the current process/heap memory. The single per-head piece of {@link DefaultTracer}. */
43
+ export type MemoryReader = () => MemoryInfo | undefined;
44
+
45
+ /**
46
+ * Cross-head observability handle: everything a {@link Logger} does (emission,
47
+ * threshold gating, component-prefix derivation) **plus** timing an operation,
48
+ * reading memory, and opening aggregate {@link ProfileSession}s.
49
+ *
50
+ * `Tracer extends Logger` so a service that both logs and times holds a single
51
+ * handle (`this.tracer.debug(...)` / `this.tracer.time(...)`), and a Tracer is
52
+ * usable anywhere a Logger is wanted. The emission-only {@link Logger} stays
53
+ * the lean default for consumers that only log.
54
+ *
55
+ * The variance lives in the loggers (per-platform emit sinks); the Tracer
56
+ * **composes** a Logger and forwards emission to it, so one
57
+ * {@link DefaultTracer} serves every head with no per-platform subclass. The
58
+ * {@link for}/{@link sub}/{@link with}/{@link withUri} derivations produce a
59
+ * child Tracer over the correspondingly-derived logger, so
60
+ * `tracer.withUri(uri).time(...)` emits through that logger's rendering.
61
+ */
62
+ export interface Tracer extends Logger {
63
+ /** Read process/heap memory and emit it as a log line. Platform-specific via {@link MemoryReader}. */
64
+ memory(label?: string, logLevel?: LogThreshold): void;
65
+ /**
66
+ * Run `callback`, emit start / completion / failure with timings. Zero-cost
67
+ * when `logLevel` is suppressed (incl. `'off'`).
68
+ *
69
+ * A success completing under {@link TimeOptions.logAfterMs} is **silent** —
70
+ * this is "tell me when it is slow", not "record every occurrence". A caller
71
+ * that wants an unconditional per-occurrence line composes it from
72
+ * `Clock.measure` instead.
73
+ */
74
+ time<T>(label: string, callback: () => T, logLevel?: LogThreshold, options?: TimeOptions): T;
75
+ /** Start a deferred-start timer that logs elapsed on dispose. Zero-cost when `logLevel` is suppressed (incl. `'off'`). */
76
+ startTimer(label: string, logLevel?: LogThreshold, logAfterMs?: number): Disposable;
77
+ /** Open an aggregating self-time {@link ProfileSession} that dumps through the bound Logger. */
78
+ profile(identifier: string): ProfileSession;
79
+ /** Derive a child Tracer with `component` as the logger prefix. */
80
+ for(component: string): this;
81
+ /** Derive a child Tracer deepening the `Foo :: bar` prefix chain. */
82
+ sub(component: string): this;
83
+ /** Derive a child Tracer appending `component` as a separate `[a] [b]` bracket. */
84
+ with(component: string): this;
85
+ /** Derive a child Tracer labelled with the given URI (subclasses may render workspace-relative). */
86
+ withUri(uri: string): this;
87
+ }
88
+
89
+ /**
90
+ * Default cross-head {@link Tracer}. One concrete class serves every head:
91
+ * emission delegates to the composed {@link Logger}, timing/profiling go
92
+ * through the injected {@link Clock}, and the only per-head varying piece is
93
+ * the {@link MemoryReader} (default: none — heads that log memory pass a
94
+ * reader, or subclass for richer rendering, as the server's `ServerTracer`
95
+ * does). The `time` / `startTimer` bodies are zero-cost when the level is
96
+ * suppressed: the callback runs directly and no stopwatch / timer / id is
97
+ * allocated.
98
+ */
99
+ export class DefaultTracer implements Tracer {
100
+ constructor(
101
+ protected readonly logger: Logger = new NoopLogger(),
102
+ protected readonly clock: Clock = new SystemClock(),
103
+ protected readonly readMemory: MemoryReader = () => undefined
104
+ ) {}
105
+
106
+ // Emission — forwarded to the composed logger (the platform-specific sink).
107
+ // Each returns the Tracer (not the wrapped logger) so chained emits stay on
108
+ // the tracer and `tracer.for(x).trace(y)` yields a Tracer.
109
+ error(message?: string, ...args: unknown[]): this {
110
+ this.logger.error(message, ...args);
111
+ return this;
112
+ }
113
+ warn(message?: string, ...args: unknown[]): this {
114
+ this.logger.warn(message, ...args);
115
+ return this;
116
+ }
117
+ info(message?: string, ...args: unknown[]): this {
118
+ this.logger.info(message, ...args);
119
+ return this;
120
+ }
121
+ debug(message?: string, ...args: unknown[]): this {
122
+ this.logger.debug(message, ...args);
123
+ return this;
124
+ }
125
+ trace(message?: string, ...args: unknown[]): this {
126
+ this.logger.trace(message, ...args);
127
+ return this;
128
+ }
129
+ log(message?: string, ...args: unknown[]): this {
130
+ this.logger.log(message, ...args);
131
+ return this;
132
+ }
133
+ logAt(threshold: LogThreshold, message: string): this {
134
+ this.logger.logAt(threshold, message);
135
+ return this;
136
+ }
137
+
138
+ memory(label?: string, logLevel: LogThreshold = 'info'): void {
139
+ if (logLevel === 'off' || !Logger.isLevelEnabled(logLevel)) {
140
+ return;
141
+ }
142
+ const info = this.readMemory();
143
+ if (!info) {
144
+ // No memory stats available — a contentless "Memory:" line is noise.
145
+ return;
146
+ }
147
+ const used = Format.bytes(info.usedBytes);
148
+ const text = info.totalBytes !== undefined ? `${used}/${Format.bytes(info.totalBytes)}` : used;
149
+ this.logger[logLevel](`${label ? `${label}: ` : 'Memory: '}${text}`);
150
+ }
151
+
152
+ time<T>(label: string, callback: () => T, logLevel: LogThreshold = 'info', options: TimeOptions = {}): T {
153
+ if (logLevel === 'off' || !Logger.isLevelEnabled(logLevel)) {
154
+ return callback();
155
+ }
156
+ const { logAfterMs = DEFER_START_MS, captureId, tags } = options;
157
+ const id = ++nextTimerId;
158
+ captureId?.(id);
159
+ const stopwatch = this.clock.stopwatch();
160
+ let startEmitted = false;
161
+ let startTimer: Disposable | undefined;
162
+ const currentExtras = (): string => (tags && tags.length > 0 ? `, ${tags.join(', ')}` : '');
163
+ const emitStart = (): void => {
164
+ this.logger[logLevel](`${label} [#${id} start${currentExtras()}]`);
165
+ startEmitted = true;
166
+ };
167
+ if (logAfterMs <= 0) {
168
+ emitStart();
169
+ } else {
170
+ startTimer = this.clock.setTimer(emitStart, logAfterMs);
171
+ }
172
+ const emit = (status: 'done' | 'failed' | 'cancelled', error?: unknown): void => {
173
+ startTimer?.dispose();
174
+ if (!startEmitted) {
175
+ if (status === 'done') {
176
+ return;
177
+ }
178
+ emitStart();
179
+ }
180
+ const elapsed = stopwatch.elapsedMs;
181
+ const suffix = this.timingSuffix(status, elapsed, options);
182
+ this.logger[logLevel](`${label} [#${id} ${status}, ${Format.elapsed(elapsed)}${currentExtras()}${suffix}]`);
183
+ if (status === 'failed') {
184
+ // The timing line above sits at `logLevel`, which may be below the
185
+ // active threshold — and on its own it never carries *why* the op
186
+ // failed. Surface the error at warn so the failure is never silent,
187
+ // with the stack at debug. Cancellations are normal control flow.
188
+ const detail = error instanceof Error ? error.message : String(error);
189
+ this.logger.warn(`${label} [#${id}] failed: ${detail}`);
190
+ if (error instanceof Error && error.stack) {
191
+ this.logger.debug(error.stack);
192
+ }
193
+ }
194
+ };
195
+ let result: T;
196
+ try {
197
+ result = callback();
198
+ } catch (error) {
199
+ emit(this.categorizeError(error), error);
200
+ throw error;
201
+ }
202
+ if (result instanceof Promise) {
203
+ return result.then(
204
+ value => {
205
+ emit('done');
206
+ return value;
207
+ },
208
+ error => {
209
+ emit(this.categorizeError(error), error);
210
+ throw error;
211
+ }
212
+ ) as T;
213
+ }
214
+ emit('done');
215
+ return result;
216
+ }
217
+
218
+ startTimer(label: string, logLevel: LogThreshold = 'info', logAfterMs: number = DEFER_START_MS): Disposable {
219
+ if (logLevel === 'off' || !Logger.isLevelEnabled(logLevel)) {
220
+ return Disposable.create(() => {});
221
+ }
222
+ const id = ++nextTimerId;
223
+ const stopwatch = this.clock.stopwatch();
224
+ let startEmitted = false;
225
+ let startTimer: Disposable | undefined;
226
+ const emitStart = (): void => {
227
+ this.logger[logLevel](`${label} [#${id} start]`);
228
+ startEmitted = true;
229
+ };
230
+ if (logAfterMs <= 0) {
231
+ emitStart();
232
+ } else {
233
+ startTimer = this.clock.setTimer(emitStart, logAfterMs);
234
+ }
235
+ return Disposable.create(() => {
236
+ startTimer?.dispose();
237
+ if (!startEmitted) {
238
+ return;
239
+ }
240
+ const elapsed = stopwatch.elapsedMs;
241
+ const suffix = this.timingSuffix('done', elapsed, {});
242
+ this.logger[logLevel](`${label} [#${id} done, ${Format.elapsed(elapsed)}${suffix}]`);
243
+ });
244
+ }
245
+
246
+ profile(identifier: string): ProfileSession {
247
+ return new DefaultProfileSession(this.logger, this.clock, identifier);
248
+ }
249
+
250
+ for(component: string): this {
251
+ return this.derive(this.logger.for(component));
252
+ }
253
+ sub(component: string): this {
254
+ return this.derive(this.logger.sub(component));
255
+ }
256
+ with(component: string): this {
257
+ return this.derive(this.logger.with(component));
258
+ }
259
+ withUri(uri: string): this {
260
+ return this.derive(this.logger.withUri(uri));
261
+ }
262
+
263
+ /** Construct a sibling Tracer over a derived logger, preserving the concrete subclass. */
264
+ protected derive(logger: Logger): this {
265
+ return new DefaultTracer(logger, this.clock, this.readMemory) as this;
266
+ }
267
+
268
+ /** Hook for appending status-bracket suffixes like memory deltas. Default: none. */
269
+ protected timingSuffix(_status: 'done' | 'failed' | 'cancelled', _elapsedMs: number, _options: TimeOptions): string {
270
+ return '';
271
+ }
272
+
273
+ /** Hook to differentiate cancelled operations from failures. Default: all errors are failures. */
274
+ protected categorizeError(_error: unknown): 'failed' | 'cancelled' {
275
+ return 'failed';
276
+ }
277
+ }
@@ -0,0 +1,92 @@
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
+ * Generic, transport-friendly diagnostic shape used by the model-server protocol.
12
+ *
13
+ * The `element` path is a `/`-separated location string with `@` and `^` as index
14
+ * and property separators, matching the conventions used by Langium's
15
+ * `AstNodeLocator` service.
16
+ */
17
+ export interface TransferDiagnostic {
18
+ /**
19
+ * Which stage produced it. Coarse by construction: only the two syntactic
20
+ * stages are recognised by name and everything else — a framework integrity
21
+ * rule, an adopter check, a linker failure — arrives as `validation-error`.
22
+ * Use it to decide whether the document PARSED, not to identify a rule.
23
+ */
24
+ type: 'lexing-error' | 'parsing-error' | 'validation-error';
25
+ /**
26
+ * `AstNodeLocator`-style path to the offending node. **Empty string when the
27
+ * diagnostic carries no location** — the framework's own validator decorates
28
+ * every diagnostic, but a rebound one need not, and an empty path denotes
29
+ * "unlocated" rather than the document root.
30
+ */
31
+ element: string;
32
+ /**
33
+ * Names the offending property when the diagnostic is about one; absent when
34
+ * it is about the node as a whole. Append it to {@link element} with
35
+ * `ELEMENT_PROPERTY_SEPARATOR` rather than by hand — `getPath` does exactly
36
+ * that and handles the absent case.
37
+ */
38
+ property?: string;
39
+ /**
40
+ * Plain text, always. An LSP `MarkupContent` message is flattened on the way
41
+ * out, so no markup survives the wire and a consumer must not try to render
42
+ * it as rich text.
43
+ */
44
+ message: string;
45
+ /**
46
+ * Three levels against LSP's four: `Hint` and `Information` both arrive as
47
+ * `info`, so a client cannot recover the distinction. Anything that is not
48
+ * an error or a warning lands in `info`, including a diagnostic that carried
49
+ * no severity at all.
50
+ */
51
+ severity: 'error' | 'warning' | 'info';
52
+ /**
53
+ * The diagnostic's own code where it has one, otherwise Langium's internal
54
+ * code — which means it may simply restate {@link type} for a syntactic
55
+ * error. Absent when neither exists, and not unique across languages, so it
56
+ * is not usable as a rule identity on its own.
57
+ */
58
+ code?: number | string;
59
+ }
60
+
61
+ export namespace TransferDiagnostic {
62
+ /** Path separator between AST nodes within an element location. */
63
+ export const ELEMENT_SEGMENT_SEPARATOR = '/';
64
+ /** Index separator within an array property segment. */
65
+ export const ELEMENT_INDEX_SEPARATOR = '@';
66
+ /** Property separator suffixing the element path with the offending property name. */
67
+ export const ELEMENT_PROPERTY_SEPARATOR = '^';
68
+
69
+ export function isError(diagnostic: TransferDiagnostic): boolean {
70
+ return diagnostic.severity === 'error';
71
+ }
72
+
73
+ export function isParseError(diagnostic: TransferDiagnostic): boolean {
74
+ return diagnostic.type === 'parsing-error';
75
+ }
76
+
77
+ export function getPath(diagnostic: TransferDiagnostic): string {
78
+ return diagnostic.property ? `${diagnostic.element}${ELEMENT_PROPERTY_SEPARATOR}${diagnostic.property}` : diagnostic.element;
79
+ }
80
+
81
+ export function errors(diagnostics: TransferDiagnostic[]): TransferDiagnostic[] {
82
+ return diagnostics.filter(isError);
83
+ }
84
+
85
+ export function hasErrors(diagnostics: TransferDiagnostic[]): boolean {
86
+ return diagnostics.some(isError);
87
+ }
88
+
89
+ export function hasParseErrors(diagnostics: TransferDiagnostic[]): boolean {
90
+ return diagnostics.some(isParseError);
91
+ }
92
+ }
@@ -0,0 +1,99 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2023-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 { TransferDiagnostic } from './transfer-diagnostic';
11
+ import type { TransferElement } from './transfer-element';
12
+
13
+ /**
14
+ * Wire envelope exchanged between the model-server's data-server head and
15
+ * its clients. Carries a transfer-shaped root — never an AST root — plus
16
+ * the document's diagnostics in the adopter's wire-diagnostic shape.
17
+ *
18
+ * The constraint `TTransfer extends TransferElement` pins the type system
19
+ * to the wire data shape (no `$container` cycles, references as strings;
20
+ * see {@link TransferElement}). The paired server-internal envelope is
21
+ * `AstDocument<TAst extends AstNode, TDiagnostic>` in
22
+ * `@hydranium/core` — same structural shape, different generic
23
+ * constraint, used at the AST layer before the `TransferEncoder`
24
+ * translation produces a {@link TransferDocument}.
25
+ */
26
+ export interface TransferDocument<TTransfer extends TransferElement, TDiagnostic = TransferDiagnostic> {
27
+ uri: string;
28
+ /**
29
+ * The document version this snapshot was taken at — sourced from the
30
+ * server's text-document version counter. Callers that mutate the
31
+ * document subsequently pass this value back as `TransferUpdateArgs.baseVersion`
32
+ * (or `TransferSaveArgs.baseVersion`) so the server can detect stale-based
33
+ * updates and reject them with `ConflictError`.
34
+ *
35
+ * See `@hydranium/protocol#errors` for the conflict-detection contract.
36
+ */
37
+ version: number;
38
+ /**
39
+ * Absent when the document does not exist — the server answers an unknown URI
40
+ * with a shaped envelope rather than an error, so absence is an ordinary
41
+ * branch and not a catch. Declared required it reads as a guarantee, and
42
+ * every `root.x` compiles into a throw.
43
+ */
44
+ root?: TTransfer;
45
+ diagnostics: TDiagnostic[];
46
+ }
47
+
48
+ /**
49
+ * A {@link TransferDocument} the server actually had, so `root` is present.
50
+ * Narrowed to by {@link TransferDocument.isLoaded} / {@link TransferDocument.assertLoaded}.
51
+ */
52
+ export type LoadedTransferDocument<TTransfer extends TransferElement, TDiagnostic = TransferDiagnostic> = TransferDocument<
53
+ TTransfer,
54
+ TDiagnostic
55
+ > & {
56
+ root: TTransfer;
57
+ };
58
+
59
+ export namespace TransferDocument {
60
+ /**
61
+ * Whether the server had this document. Branch on it where absence is an
62
+ * ordinary state — a view bound to a file that may not exist yet.
63
+ */
64
+ export function isLoaded<TTransfer extends TransferElement, TDiagnostic = TransferDiagnostic>(
65
+ document: TransferDocument<TTransfer, TDiagnostic>
66
+ ): document is LoadedTransferDocument<TTransfer, TDiagnostic> {
67
+ return document.root !== undefined;
68
+ }
69
+
70
+ /**
71
+ * The same document with `root` no longer optional, or a throw. For call
72
+ * sites where absence is a bug rather than a branch; the whole envelope is
73
+ * returned because a caller needing `root` usually needs `uri` or `version`
74
+ * with it.
75
+ */
76
+ export function assertLoaded<TTransfer extends TransferElement, TDiagnostic = TransferDiagnostic>(
77
+ document: TransferDocument<TTransfer, TDiagnostic>
78
+ ): LoadedTransferDocument<TTransfer, TDiagnostic> {
79
+ if (!isLoaded(document)) {
80
+ throw new Error(`No document at ${document.uri}`);
81
+ }
82
+ return document;
83
+ }
84
+
85
+ /**
86
+ * Construct a {@link TransferDocument} envelope. `diagnostics` defaults
87
+ * to `[]` so test fixtures, fake protocol implementations, and the
88
+ * "no diagnostics yet" code paths don't need to repeat the empty array
89
+ * at every call site.
90
+ */
91
+ export function create<TTransfer extends TransferElement, TDiagnostic = TransferDiagnostic>(
92
+ uri: string,
93
+ version: number,
94
+ root: TTransfer,
95
+ diagnostics: TDiagnostic[] = []
96
+ ): TransferDocument<TTransfer, TDiagnostic> {
97
+ return { uri, version, root, diagnostics };
98
+ }
99
+ }
@@ -0,0 +1,43 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2023-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
+ * Base shapes for the transfer-model overlay generated per consumer language.
12
+ *
13
+ * Every node in a generated transfer model extends {@link TransferElement} and
14
+ * cross-references between nodes serialise as plain strings. These types carry no
15
+ * language-specific knowledge — language-specific types live in the consumer's
16
+ * generated `transfer-model.ts` overlay, emitted by the code generator from
17
+ * the consumer's grammar.
18
+ */
19
+
20
+ /** Base type of every transfer-model node — carries the AST type discriminator. */
21
+ export interface TransferElement {
22
+ readonly $type: string;
23
+ }
24
+
25
+ /**
26
+ * Type-level lookup: given an AST node type `T` (with a string-literal
27
+ * `$type` discriminator from Langium's generated AST) and a per-grammar
28
+ * `TTransferMap` overlay keyed by those `$type` strings, resolve to the
29
+ * declared wire shape — or fall back to the structural base
30
+ * {@link TransferElement} when no entry is declared.
31
+ *
32
+ * Adopters declare the overlay by interface merging or by generating an
33
+ * interface keyed on those `$type` strings. Types not in the overlay resolve to
34
+ * `TransferElement` — the framework default behaves correctly without an
35
+ * overlay (no autocomplete, but no compile error either).
36
+ *
37
+ * The lookup is compile-time only; no runtime cost.
38
+ */
39
+ export type TransferTypeFor<TAst extends { readonly $type: string }, TTransferMap> = TAst['$type'] extends keyof TTransferMap
40
+ ? TTransferMap[TAst['$type']] extends TransferElement
41
+ ? TTransferMap[TAst['$type']]
42
+ : TransferElement
43
+ : TransferElement;
package/src/uri.ts ADDED
@@ -0,0 +1,61 @@
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
+ * Branded URI-string types distinguishing the framework's two document-identity
12
+ * spaces. Both are `string` at runtime — the brand is a phantom field, erased by
13
+ * the compiler, so there is zero runtime cost and the values pass freely to any
14
+ * `string` sink (`UriUtils`, RPC payloads, logs). At compile time they are
15
+ * **nominally distinct and mutually non-assignable**, so one cannot be used
16
+ * where the other is expected.
17
+ *
18
+ * - {@link CanonicalUri} — *server-identity* space: the single canonical key a
19
+ * document is stored under (`LangiumDocuments` keying, AST-document event
20
+ * filters, request-path lookups). Minted ONLY by
21
+ * `DocumentUriPolicy.canonicalUri` — the single point that defines what
22
+ * "canonical" means, so every `CanonicalUri` has provable provenance.
23
+ * - {@link LanguageClientUri} — the URI the LSP *textual* language client
24
+ * (Monaco / VS Code) actually opened a document under, which may differ from
25
+ * its canonical identity (e.g. a symlinked path while the document is keyed by
26
+ * its real path). This is the URI `applyEditToLanguageClient` and diagnostics must
27
+ * address. Named for the *language client* specifically — the GLSP and
28
+ * data-server heads are also clients, but they are addressed canonically.
29
+ *
30
+ * Why both are real brands rather than aliases: a plain `type X = string` is
31
+ * assignable to and from any `string` (and a `CanonicalUri` *is* a `string`), so
32
+ * an alias would let a canonical URI flow into a language-client sink unchecked —
33
+ * exactly the class of bug where an outbound sync addressed the client by the
34
+ * canonical URI it never opened. The distinct `__uriSpace` discriminants make
35
+ * that a compile error, and make plain `string` assignable to neither — forcing
36
+ * values through a mint point.
37
+ *
38
+ * Scope: these are an **internal** server-identity discipline. RPC/wire fields
39
+ * and public service parameters stay plain `string` so the brands never leak to
40
+ * adopters or across the wire; conversion happens at the framework boundary.
41
+ */
42
+ export type CanonicalUri = string & { readonly __uriSpace: 'canonical' };
43
+
44
+ /** The LSP textual language client's document URI. See {@link CanonicalUri} for the full rationale. */
45
+ export type LanguageClientUri = string & { readonly __uriSpace: 'languageClient' };
46
+
47
+ /**
48
+ * Mint a {@link CanonicalUri}. The single sanctioned place the brand assertion
49
+ * lives — call this from the one component that defines canonicalisation
50
+ * (`DocumentUriPolicy.canonicalUri`) instead of scattering `as CanonicalUri`
51
+ * casts, so a call site reads as a deliberate identity mint rather than an
52
+ * escape hatch.
53
+ */
54
+ export const asCanonicalUri = (value: string): CanonicalUri => value as CanonicalUri;
55
+
56
+ /**
57
+ * Mint a {@link LanguageClientUri}. The single sanctioned place the brand
58
+ * assertion lives — call this from the component that owns the language-client
59
+ * URI space (the text store's `toLanguageClientUri`). See {@link asCanonicalUri}.
60
+ */
61
+ export const asLanguageClientUri = (value: string): LanguageClientUri => value as LanguageClientUri;