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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +44 -83
  2. package/lib/abstract-logger.d.ts +11 -0
  3. package/lib/abstract-logger.d.ts.map +1 -1
  4. package/lib/abstract-logger.js +16 -0
  5. package/lib/abstract-logger.js.map +1 -1
  6. package/lib/client/data-connection.d.ts +303 -0
  7. package/lib/client/data-connection.d.ts.map +1 -0
  8. package/lib/client/data-connection.js +534 -0
  9. package/lib/client/data-connection.js.map +1 -0
  10. package/lib/client/data-events.d.ts +18 -1
  11. package/lib/client/data-events.d.ts.map +1 -1
  12. package/lib/client/data-events.js +26 -0
  13. package/lib/client/data-events.js.map +1 -1
  14. package/lib/client/data-port.d.ts +34 -24
  15. package/lib/client/data-port.d.ts.map +1 -1
  16. package/lib/client/data-session.d.ts +691 -89
  17. package/lib/client/data-session.d.ts.map +1 -1
  18. package/lib/client/data-session.js +1155 -115
  19. package/lib/client/data-session.js.map +1 -1
  20. package/lib/client/index.d.ts +15 -9
  21. package/lib/client/index.d.ts.map +1 -1
  22. package/lib/client/index.js +15 -9
  23. package/lib/client/index.js.map +1 -1
  24. package/lib/client/message-relay.d.ts +6 -4
  25. package/lib/client/message-relay.d.ts.map +1 -1
  26. package/lib/client/message-relay.js +4 -4
  27. package/lib/client/message-relay.js.map +1 -1
  28. package/lib/client/post-message-transport.d.ts +69 -3
  29. package/lib/client/post-message-transport.d.ts.map +1 -1
  30. package/lib/client/post-message-transport.js +175 -1
  31. package/lib/client/post-message-transport.js.map +1 -1
  32. package/lib/client/rpc-connection.d.ts +173 -0
  33. package/lib/client/rpc-connection.d.ts.map +1 -0
  34. package/lib/client/rpc-connection.js +225 -0
  35. package/lib/client/rpc-connection.js.map +1 -0
  36. package/lib/client/worker-handshake.d.ts +94 -0
  37. package/lib/client/worker-handshake.d.ts.map +1 -0
  38. package/lib/client/worker-handshake.js +51 -0
  39. package/lib/client/worker-handshake.js.map +1 -0
  40. package/lib/client-ids.d.ts +45 -0
  41. package/lib/client-ids.d.ts.map +1 -0
  42. package/lib/client-ids.js +48 -0
  43. package/lib/client-ids.js.map +1 -0
  44. package/lib/clock.d.ts +50 -10
  45. package/lib/clock.d.ts.map +1 -1
  46. package/lib/clock.js +36 -1
  47. package/lib/clock.js.map +1 -1
  48. package/lib/console-logger.d.ts +23 -0
  49. package/lib/console-logger.d.ts.map +1 -0
  50. package/lib/console-logger.js +39 -0
  51. package/lib/console-logger.js.map +1 -0
  52. package/lib/data/data-protocol-methods.d.ts +4 -4
  53. package/lib/data/data-protocol-methods.d.ts.map +1 -1
  54. package/lib/data/data-protocol-methods.js +13 -1
  55. package/lib/data/data-protocol-methods.js.map +1 -1
  56. package/lib/data/data-server-protocol.d.ts +171 -48
  57. package/lib/data/data-server-protocol.d.ts.map +1 -1
  58. package/lib/data/events.d.ts +144 -33
  59. package/lib/data/events.d.ts.map +1 -1
  60. package/lib/data/requests.d.ts +76 -11
  61. package/lib/data/requests.d.ts.map +1 -1
  62. package/lib/debouncer.d.ts +7 -4
  63. package/lib/debouncer.d.ts.map +1 -1
  64. package/lib/debouncer.js +10 -10
  65. package/lib/debouncer.js.map +1 -1
  66. package/lib/errors.d.ts +218 -41
  67. package/lib/errors.d.ts.map +1 -1
  68. package/lib/errors.js +364 -55
  69. package/lib/errors.js.map +1 -1
  70. package/lib/format-log-arg.d.ts +33 -0
  71. package/lib/format-log-arg.d.ts.map +1 -0
  72. package/lib/format-log-arg.js +158 -0
  73. package/lib/format-log-arg.js.map +1 -0
  74. package/lib/glsp-edits-settled-actions.d.ts +54 -0
  75. package/lib/glsp-edits-settled-actions.d.ts.map +1 -0
  76. package/lib/glsp-edits-settled-actions.js +28 -0
  77. package/lib/glsp-edits-settled-actions.js.map +1 -0
  78. package/lib/glsp-request-model-args.d.ts +16 -0
  79. package/lib/glsp-request-model-args.d.ts.map +1 -0
  80. package/lib/glsp-request-model-args.js +19 -0
  81. package/lib/glsp-request-model-args.js.map +1 -0
  82. package/lib/glsp-save-model-actions.d.ts +55 -0
  83. package/lib/glsp-save-model-actions.d.ts.map +1 -0
  84. package/lib/glsp-save-model-actions.js +28 -0
  85. package/lib/glsp-save-model-actions.js.map +1 -0
  86. package/lib/index.d.ts +11 -0
  87. package/lib/index.d.ts.map +1 -1
  88. package/lib/index.js +11 -0
  89. package/lib/index.js.map +1 -1
  90. package/lib/latency-collector.d.ts +10 -4
  91. package/lib/latency-collector.d.ts.map +1 -1
  92. package/lib/latency-collector.js +2 -0
  93. package/lib/latency-collector.js.map +1 -1
  94. package/lib/logger.d.ts +38 -7
  95. package/lib/logger.d.ts.map +1 -1
  96. package/lib/logger.js +42 -4
  97. package/lib/logger.js.map +1 -1
  98. package/lib/messages/index.d.ts +4 -2
  99. package/lib/messages/index.d.ts.map +1 -1
  100. package/lib/messages/index.js +16 -4
  101. package/lib/messages/index.js.map +1 -1
  102. package/lib/messages/primitives.d.ts +59 -4
  103. package/lib/messages/primitives.d.ts.map +1 -1
  104. package/lib/messages/primitives.js +97 -4
  105. package/lib/messages/primitives.js.map +1 -1
  106. package/lib/model-server.d.ts +60 -13
  107. package/lib/model-server.d.ts.map +1 -1
  108. package/lib/model-server.js +4 -2
  109. package/lib/model-server.js.map +1 -1
  110. package/lib/model-service/base-version.d.ts +67 -0
  111. package/lib/model-service/base-version.d.ts.map +1 -0
  112. package/lib/model-service/base-version.js +44 -0
  113. package/lib/model-service/base-version.js.map +1 -0
  114. package/lib/model-service/index.d.ts +1 -1
  115. package/lib/model-service/index.d.ts.map +1 -1
  116. package/lib/model-service/index.js +4 -5
  117. package/lib/model-service/index.js.map +1 -1
  118. package/lib/model-service/reference-candidate.d.ts +5 -3
  119. package/lib/model-service/reference-candidate.d.ts.map +1 -1
  120. package/lib/node/index.d.ts +10 -0
  121. package/lib/node/index.d.ts.map +1 -0
  122. package/lib/node/index.js +29 -0
  123. package/lib/node/index.js.map +1 -0
  124. package/lib/node/process-memory.d.ts +66 -0
  125. package/lib/node/process-memory.d.ts.map +1 -0
  126. package/lib/node/process-memory.js +291 -0
  127. package/lib/node/process-memory.js.map +1 -0
  128. package/lib/noop-logger.d.ts.map +1 -1
  129. package/lib/noop-logger.js.map +1 -1
  130. package/lib/observable-value.js.map +1 -1
  131. package/lib/patch-merge.d.ts +63 -35
  132. package/lib/patch-merge.d.ts.map +1 -1
  133. package/lib/patch-merge.js +171 -25
  134. package/lib/patch-merge.js.map +1 -1
  135. package/lib/pending-work-tracker.d.ts +30 -0
  136. package/lib/pending-work-tracker.d.ts.map +1 -0
  137. package/lib/pending-work-tracker.js +47 -0
  138. package/lib/pending-work-tracker.js.map +1 -0
  139. package/lib/profile-session.d.ts +24 -8
  140. package/lib/profile-session.d.ts.map +1 -1
  141. package/lib/profile-session.js +13 -4
  142. package/lib/profile-session.js.map +1 -1
  143. package/lib/profiling.d.ts +15 -2
  144. package/lib/profiling.d.ts.map +1 -1
  145. package/lib/profiling.js.map +1 -1
  146. package/lib/project.d.ts +4 -1
  147. package/lib/project.d.ts.map +1 -1
  148. package/lib/project.js.map +1 -1
  149. package/lib/random-uuid.d.ts +14 -0
  150. package/lib/random-uuid.d.ts.map +1 -0
  151. package/lib/random-uuid.js +24 -0
  152. package/lib/random-uuid.js.map +1 -0
  153. package/lib/reconcile-write.d.ts +70 -0
  154. package/lib/reconcile-write.d.ts.map +1 -0
  155. package/lib/reconcile-write.js +69 -0
  156. package/lib/reconcile-write.js.map +1 -0
  157. package/lib/rpc/bind-rpc-methods.d.ts +6 -2
  158. package/lib/rpc/bind-rpc-methods.d.ts.map +1 -1
  159. package/lib/rpc/bind-rpc-methods.js +17 -7
  160. package/lib/rpc/bind-rpc-methods.js.map +1 -1
  161. package/lib/rpc/create-rpc-proxy.d.ts +3 -0
  162. package/lib/rpc/create-rpc-proxy.d.ts.map +1 -1
  163. package/lib/rpc/create-rpc-proxy.js +6 -1
  164. package/lib/rpc/create-rpc-proxy.js.map +1 -1
  165. package/lib/rpc/index.d.ts +1 -0
  166. package/lib/rpc/index.d.ts.map +1 -1
  167. package/lib/rpc/index.js +1 -0
  168. package/lib/rpc/index.js.map +1 -1
  169. package/lib/rpc/send-by-method-name.d.ts +76 -0
  170. package/lib/rpc/send-by-method-name.d.ts.map +1 -0
  171. package/lib/rpc/send-by-method-name.js +120 -0
  172. package/lib/rpc/send-by-method-name.js.map +1 -0
  173. package/lib/rpc/wire-prefix.js.map +1 -1
  174. package/lib/save-text-document.d.ts +40 -0
  175. package/lib/save-text-document.d.ts.map +1 -0
  176. package/lib/save-text-document.js +34 -0
  177. package/lib/save-text-document.js.map +1 -0
  178. package/lib/testing/catalogue-audit.js.map +1 -1
  179. package/lib/testing/data-doubles.d.ts +41 -12
  180. package/lib/testing/data-doubles.d.ts.map +1 -1
  181. package/lib/testing/data-doubles.js +56 -8
  182. package/lib/testing/data-doubles.js.map +1 -1
  183. package/lib/testing/fake-clock.d.ts +17 -2
  184. package/lib/testing/fake-clock.d.ts.map +1 -1
  185. package/lib/testing/fake-clock.js +61 -46
  186. package/lib/testing/fake-clock.js.map +1 -1
  187. package/lib/testing/index.d.ts +1 -0
  188. package/lib/testing/index.d.ts.map +1 -1
  189. package/lib/testing/index.js +4 -3
  190. package/lib/testing/index.js.map +1 -1
  191. package/lib/testing/node/duplex-connection.d.ts.map +1 -1
  192. package/lib/testing/node/duplex-connection.js +3 -2
  193. package/lib/testing/node/duplex-connection.js.map +1 -1
  194. package/lib/testing/node/duplex-stream.js.map +1 -1
  195. package/lib/testing/node/index.d.ts +1 -0
  196. package/lib/testing/node/index.d.ts.map +1 -1
  197. package/lib/testing/node/index.js +2 -2
  198. package/lib/testing/node/index.js.map +1 -1
  199. package/lib/testing/node/message-port-pair.d.ts +25 -0
  200. package/lib/testing/node/message-port-pair.d.ts.map +1 -0
  201. package/lib/testing/node/message-port-pair.js +26 -0
  202. package/lib/testing/node/message-port-pair.js.map +1 -0
  203. package/lib/testing/transfer-node-builder.d.ts +81 -0
  204. package/lib/testing/transfer-node-builder.d.ts.map +1 -0
  205. package/lib/testing/transfer-node-builder.js +47 -0
  206. package/lib/testing/transfer-node-builder.js.map +1 -0
  207. package/lib/testing/wait-for.d.ts +9 -3
  208. package/lib/testing/wait-for.d.ts.map +1 -1
  209. package/lib/testing/wait-for.js +57 -11
  210. package/lib/testing/wait-for.js.map +1 -1
  211. package/lib/tracer.d.ts.map +1 -1
  212. package/lib/tracer.js +5 -9
  213. package/lib/tracer.js.map +1 -1
  214. package/lib/transfer-diagnostic.js.map +1 -1
  215. package/lib/transfer-document.d.ts +96 -32
  216. package/lib/transfer-document.d.ts.map +1 -1
  217. package/lib/transfer-document.js +17 -9
  218. package/lib/transfer-document.js.map +1 -1
  219. package/lib/transfer-element.d.ts +4 -4
  220. package/lib/types.d.ts +32 -0
  221. package/lib/types.d.ts.map +1 -0
  222. package/lib/{model-service/args.js → types.js} +2 -2
  223. package/lib/types.js.map +1 -0
  224. package/lib/uri.d.ts.map +1 -1
  225. package/lib/uri.js.map +1 -1
  226. package/lib/util.d.ts +8 -2
  227. package/lib/util.d.ts.map +1 -1
  228. package/lib/util.js +32 -4
  229. package/lib/util.js.map +1 -1
  230. package/package.json +24 -40
  231. package/src/abstract-logger.ts +18 -0
  232. package/src/client/data-connection.ts +654 -0
  233. package/src/client/data-events.ts +38 -1
  234. package/src/client/data-port.ts +36 -25
  235. package/src/client/data-session.ts +1476 -137
  236. package/src/client/index.ts +15 -9
  237. package/src/client/message-relay.ts +6 -4
  238. package/src/client/post-message-transport.ts +224 -4
  239. package/src/client/rpc-connection.ts +302 -0
  240. package/src/client/worker-handshake.ts +121 -0
  241. package/src/client-ids.ts +49 -0
  242. package/src/clock.ts +68 -10
  243. package/src/console-logger.ts +39 -0
  244. package/src/data/data-protocol-methods.ts +14 -4
  245. package/src/data/data-server-protocol.ts +198 -48
  246. package/src/data/events.ts +150 -33
  247. package/src/data/requests.ts +82 -11
  248. package/src/debouncer.ts +11 -11
  249. package/src/errors.ts +453 -65
  250. package/src/format-log-arg.ts +176 -0
  251. package/src/glsp-edits-settled-actions.ts +62 -0
  252. package/src/glsp-request-model-args.ts +16 -0
  253. package/src/glsp-save-model-actions.ts +64 -0
  254. package/src/index.ts +11 -0
  255. package/src/latency-collector.ts +10 -3
  256. package/src/logger.ts +44 -8
  257. package/src/messages/index.ts +11 -2
  258. package/src/messages/primitives.ts +129 -5
  259. package/src/model-server.ts +64 -19
  260. package/src/model-service/base-version.ts +75 -0
  261. package/src/model-service/index.ts +4 -5
  262. package/src/model-service/reference-candidate.ts +5 -3
  263. package/src/node/index.ts +14 -0
  264. package/src/node/process-memory.ts +299 -0
  265. package/src/patch-merge.ts +230 -45
  266. package/src/pending-work-tracker.ts +47 -0
  267. package/src/profile-session.ts +31 -11
  268. package/src/profiling.ts +15 -2
  269. package/src/project.ts +4 -1
  270. package/src/random-uuid.ts +21 -0
  271. package/src/reconcile-write.ts +131 -0
  272. package/src/rpc/README.md +6 -6
  273. package/src/rpc/bind-rpc-methods.ts +18 -8
  274. package/src/rpc/create-rpc-proxy.ts +6 -1
  275. package/src/rpc/index.ts +1 -0
  276. package/src/rpc/send-by-method-name.ts +140 -0
  277. package/src/save-text-document.ts +39 -0
  278. package/src/testing/data-doubles.ts +141 -19
  279. package/src/testing/fake-clock.ts +76 -48
  280. package/src/testing/index.ts +4 -3
  281. package/src/testing/node/duplex-connection.ts +3 -2
  282. package/src/testing/node/index.ts +2 -2
  283. package/src/testing/node/message-port-pair.ts +40 -0
  284. package/src/testing/transfer-node-builder.ts +107 -0
  285. package/src/testing/wait-for.ts +61 -13
  286. package/src/tracer.ts +5 -9
  287. package/src/transfer-document.ts +116 -34
  288. package/src/transfer-element.ts +4 -4
  289. package/src/types.ts +30 -0
  290. package/src/util.ts +33 -6
  291. package/lib/model-service/args.d.ts +0 -64
  292. package/lib/model-service/args.d.ts.map +0 -1
  293. package/lib/model-service/args.js.map +0 -1
  294. package/src/model-service/args.ts +0 -67
@@ -0,0 +1,176 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 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 { describeErrorWithStack } from './messages/primitives';
11
+
12
+ /** How deep an object argument is rendered; a deeper object reads `[Object]` or `[Array]`. */
13
+ const LOG_ARG_DEPTH = 2;
14
+ /** How many items or keys of one array or object are rendered. */
15
+ const LOG_ARG_ITEMS = 100;
16
+ /** How many characters of one string or key inside an object argument are rendered. */
17
+ const LOG_ARG_STRING_LENGTH = 10_000;
18
+ /**
19
+ * About how many characters one argument renders, since the limits above
20
+ * multiply. The entry that would pass it reads `...`; later ones are left out.
21
+ */
22
+ const LOG_ARG_LENGTH = 100_000;
23
+
24
+ /** Options of {@link formatLogArg}. An object, so that a `map` index cannot be passed as one. */
25
+ export interface FormatLogArgOptions {
26
+ /** The `space` argument of `JSON.stringify`; compact where omitted. */
27
+ readonly indent?: number;
28
+ }
29
+
30
+ /**
31
+ * Render a log argument as text for a log line:
32
+ * - a string as it is;
33
+ * - an error by its stack and causes;
34
+ * - another object as JSON, cut to a bounded size;
35
+ * - anything else by its description, as is an object JSON renders as nothing
36
+ * or cannot render.
37
+ *
38
+ * The cut is needed because the log call serialises synchronously and every
39
+ * Langium AST node is cyclic. The JSON stops at `LOG_ARG_DEPTH` levels,
40
+ * `LOG_ARG_ITEMS` items or keys and `LOG_ARG_STRING_LENGTH` characters per
41
+ * string or key, and at about `LOG_ARG_LENGTH` in all. A reference back to an
42
+ * object that contains it reads `[Circular]`, and a BigInt its digits; the
43
+ * markers are JSON strings. `util.inspect` would cut too, but is Node-only.
44
+ *
45
+ * Never throws, since a log call that throws loses the line and interrupts its
46
+ * caller. A `%s`-style specifier is not substituted.
47
+ */
48
+ export function formatLogArg(value: unknown, options: FormatLogArgOptions = {}): string {
49
+ if (typeof value === 'string') {
50
+ return value;
51
+ }
52
+ if (typeof value === 'object' && value !== null) {
53
+ try {
54
+ // `undefined` where `toJSON` returns nothing.
55
+ const json: string | undefined =
56
+ value instanceof Error ? undefined : JSON.stringify(value, boundedReplacer(options.indent), options.indent);
57
+ if (json !== undefined) {
58
+ return json;
59
+ }
60
+ } catch {
61
+ // A throwing getter or `toJSON`, or a revoked proxy; described below instead.
62
+ }
63
+ }
64
+ return describeErrorWithStack(value);
65
+ }
66
+
67
+ interface Visit {
68
+ readonly parent: object;
69
+ readonly depth: number;
70
+ /** The object a cut copy stands in for, which a cycle refers back to. */
71
+ readonly source: object;
72
+ }
73
+
74
+ function boundedReplacer(indent: number | undefined): (this: object, key: string, value: unknown) => unknown {
75
+ // Keyed by the object last entered; JSON is written depth first, so its entry is on the path being written.
76
+ const visits = new Map<object, Visit>();
77
+ const isOnPath = (value: object, holder: object | undefined): boolean =>
78
+ holder !== undefined && ((visits.get(holder)?.source ?? holder) === value || isOnPath(value, visits.get(holder)?.parent));
79
+ // As `JSON.stringify` clamps its `space`.
80
+ const space = Math.min(10, Math.max(0, Math.floor(indent ?? 0)));
81
+ let remaining = LOG_ARG_LENGTH;
82
+ let exhausted = false;
83
+ return function (this: object, key: string, raw: unknown): unknown {
84
+ if (exhausted) {
85
+ // In an object the entry is left out; in an array it reads `null`.
86
+ return undefined;
87
+ }
88
+ // A boxed primitive is written as its value, so it is cut as one.
89
+ const value = raw instanceof String || raw instanceof Number || raw instanceof Boolean || raw instanceof BigInt ? raw.valueOf() : raw;
90
+ const depth = (visits.get(this)?.depth ?? -1) + 1;
91
+ const rendered = render(value, depth, this);
92
+ const isObject = typeof rendered === 'object' && rendered !== null;
93
+ // What this entry writes: its value, its key unless it is an item, and the punctuation and indent around them.
94
+ const written = isObject ? undefined : JSON.stringify(rendered);
95
+ // A value JSON cannot write is left out of an object, key and all, and written as `null` in an array.
96
+ const leftOut = !isObject && written === undefined && !Array.isArray(this);
97
+ const valueLength = isObject ? 2 : (written?.length ?? 4);
98
+ const keyLength = depth === 0 || Array.isArray(this) ? 0 : JSON.stringify(key).length + 1 + (space > 0 ? 1 : 0);
99
+ const layoutLength = 1 + (space > 0 ? (1 + space * depth) * (isObject ? 2 : 1) : 0);
100
+ const length = leftOut ? 0 : valueLength + keyLength + layoutLength;
101
+ if (length > remaining) {
102
+ exhausted = true;
103
+ return '...';
104
+ }
105
+ remaining -= length;
106
+ if (isObject) {
107
+ visits.set(rendered, { parent: this, depth, source: value as object });
108
+ }
109
+ return rendered;
110
+ };
111
+
112
+ function render(value: unknown, depth: number, holder: object): unknown {
113
+ if (typeof value === 'string') {
114
+ return cutString(value);
115
+ }
116
+ if (typeof value === 'bigint') {
117
+ return String(value);
118
+ }
119
+ if (typeof value !== 'object' || value === null) {
120
+ return value;
121
+ }
122
+ if (isOnPath(value, holder)) {
123
+ return '[Circular]';
124
+ }
125
+ if (depth > LOG_ARG_DEPTH) {
126
+ return Array.isArray(value) || isTypedArray(value) ? '[Array]' : '[Object]';
127
+ }
128
+ return cut(value);
129
+ }
130
+ }
131
+
132
+ function cutString(value: string): string {
133
+ const more = value.length - LOG_ARG_STRING_LENGTH;
134
+ return more > 0 ? `${value.slice(0, LOG_ARG_STRING_LENGTH)}... ${more} more characters` : value;
135
+ }
136
+
137
+ function isTypedArray(value: object): value is ArrayLike<unknown> {
138
+ return ArrayBuffer.isView(value) && !(value instanceof DataView);
139
+ }
140
+
141
+ /**
142
+ * `value`, or a copy of its first {@link LOG_ARG_ITEMS} items or keys, keys cut
143
+ * as strings are, with a note of how many more there are. A typed array is
144
+ * copied to an array, which reads its items without listing its keys.
145
+ */
146
+ function cut(value: object): object {
147
+ if (Array.isArray(value) || isTypedArray(value)) {
148
+ const more = value.length - LOG_ARG_ITEMS;
149
+ const items = Array.from({ length: Math.min(value.length, LOG_ARG_ITEMS) }, (_, i) => value[i]);
150
+ return more > 0 ? [...items, `... ${more} more items`] : Array.isArray(value) ? value : items;
151
+ }
152
+ // Linear in the keys: no API lists the first few alone, and `JSON.stringify` would list them all too.
153
+ const keys = Object.keys(value);
154
+ const more = keys.length - LOG_ARG_ITEMS;
155
+ const kept = keys.slice(0, LOG_ARG_ITEMS);
156
+ if (more <= 0 && kept.every(key => key.length <= LOG_ARG_STRING_LENGTH)) {
157
+ return value;
158
+ }
159
+ // No prototype, so a `__proto__` key is an entry like any other.
160
+ const copy: Record<string, unknown> = Object.create(null);
161
+ // A cut key or the note can equal a key already written; numbered, so neither entry is lost.
162
+ const add = (key: string, entry: unknown): void => {
163
+ let unique = key;
164
+ for (let i = 2; unique in copy; i++) {
165
+ unique = `${key} (${i})`;
166
+ }
167
+ copy[unique] = entry;
168
+ };
169
+ for (const key of kept) {
170
+ add(cutString(key), (value as Record<string, unknown>)[key]);
171
+ }
172
+ if (more > 0) {
173
+ add(`... ${more} more keys`, '...');
174
+ }
175
+ return copy;
176
+ }
@@ -0,0 +1,62 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 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
+ // Declared field by field rather than extending GLSP's action types, as the
11
+ // save actions are: this package carries no GLSP dependency.
12
+
13
+ /**
14
+ * A request the server answers, with an {@link EditsSettledAction}, once every
15
+ * edit the client sent before it has been handled and its text has reached
16
+ * the language client, so the client knows those edits are answered.
17
+ *
18
+ * An operation has no answer of its own: the server replies with a model
19
+ * update and a dirty state, with a message, or with nothing at all when the
20
+ * operation changed nothing. The server handles a client's actions one at a
21
+ * time, so a request sent after the edits is answered after them, whatever
22
+ * each one answered. A server that fails it answers with a GLSP
23
+ * `RejectAction`, which ends the wait too.
24
+ */
25
+ export interface RequestEditsSettledAction {
26
+ /** Answered only by a server that advertises this kind. The framework's dispatcher sends none to a server that does not. */
27
+ kind: typeof RequestEditsSettledAction.KIND;
28
+ /** The id the response echoes as its `responseId`. Empty until the dispatcher assigns one. */
29
+ requestId: string;
30
+ /**
31
+ * How long the server waits before it rejects the request, as it does any
32
+ * request; a rejection then means not settled, and the answer that follows
33
+ * is dropped. GLSP's `requestUntil` sets one; `request` leaves it unset.
34
+ */
35
+ timeout?: number;
36
+ /** GLSP's typing marker for the response. Never set. */
37
+ readonly _?: EditsSettledAction;
38
+ }
39
+
40
+ export namespace RequestEditsSettledAction {
41
+ export const KIND = 'hydraniumRequestEditsSettled';
42
+
43
+ export function create(options: { requestId?: string } = {}): RequestEditsSettledAction {
44
+ return { kind: KIND, requestId: '', ...options };
45
+ }
46
+ }
47
+
48
+ /** The server's answer to a {@link RequestEditsSettledAction}. */
49
+ export interface EditsSettledAction {
50
+ /** No client handler takes this kind: the dispatcher settles the request by {@link responseId} instead. */
51
+ kind: typeof EditsSettledAction.KIND;
52
+ /** The `requestId` of the request this answers. The server's dispatcher sets it. */
53
+ responseId: string;
54
+ }
55
+
56
+ export namespace EditsSettledAction {
57
+ export const KIND = 'hydraniumEditsSettled';
58
+
59
+ export function create(options: { responseId?: string } = {}): EditsSettledAction {
60
+ return { kind: KIND, responseId: '', ...options };
61
+ }
62
+ }
@@ -0,0 +1,16 @@
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 `RequestModelAction` option carrying a diagram's resume token. A load
12
+ * whose token matches the one the live session under its client id was started
13
+ * with takes that session over, so a diagram that reconnects or reloads keeps
14
+ * its unsaved text; without it, the id held by another session is refused.
15
+ */
16
+ export const RESUME_TOKEN_ARG = 'hydraniumResumeToken';
@@ -0,0 +1,64 @@
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
+ // Declared field by field rather than extending GLSP's action types: this
11
+ // package carries no GLSP dependency, and both GLSP heads already depend on it.
12
+ // The fields are GLSP's own, so both actions are assignable to GLSP's
13
+ // `RequestAction` and `ResponseAction`.
14
+
15
+ /**
16
+ * A save of a diagram's model that the server answers, with a
17
+ * {@link ModelSavedAction} once the save has finished or a GLSP
18
+ * `RejectAction` when it fails.
19
+ *
20
+ * It carries what GLSP's `SaveModelAction` carries, and the server saves it the
21
+ * same way. GLSP's `SaveModelAction` is not answered: the server's only
22
+ * reply is a dirty-state change, which the client drops when the dirty flag
23
+ * does not change, so a client cannot tell which save an answer belongs to or
24
+ * whether a save failed.
25
+ */
26
+ export interface RequestSaveModelAction {
27
+ /**
28
+ * Answered only by a server that advertises this kind. The framework's
29
+ * Theia saveable sends GLSP's `SaveModelAction` to one that does not.
30
+ */
31
+ kind: typeof RequestSaveModelAction.KIND;
32
+ /** The id the response echoes as its `responseId`. Empty until the dispatcher assigns one. */
33
+ requestId: string;
34
+ /** How long the receiver waits for its own handler, in milliseconds. */
35
+ timeout?: number;
36
+ /** The file to save to, as in GLSP's `SaveModelAction`; absent saves to the diagram's source. */
37
+ fileUri?: string;
38
+ /** GLSP's typing marker for the response. Never set. */
39
+ readonly _?: ModelSavedAction;
40
+ }
41
+
42
+ export namespace RequestSaveModelAction {
43
+ export const KIND = 'hydraniumRequestSaveModel';
44
+
45
+ export function create(options: { fileUri?: string; requestId?: string } = {}): RequestSaveModelAction {
46
+ return { kind: KIND, requestId: '', ...options };
47
+ }
48
+ }
49
+
50
+ /** The server's answer to a {@link RequestSaveModelAction} that saved. */
51
+ export interface ModelSavedAction {
52
+ /** No client handler takes this kind: the dispatcher settles the save by {@link responseId} instead. */
53
+ kind: typeof ModelSavedAction.KIND;
54
+ /** The `requestId` of the save this answers. The server's dispatcher sets it. */
55
+ responseId: string;
56
+ }
57
+
58
+ export namespace ModelSavedAction {
59
+ export const KIND = 'hydraniumModelSaved';
60
+
61
+ export function create(options: { responseId?: string } = {}): ModelSavedAction {
62
+ return { kind: KIND, responseId: '', ...options };
63
+ }
64
+ }
package/src/index.ts CHANGED
@@ -15,12 +15,18 @@
15
15
  // every production bundle that imports the root.
16
16
 
17
17
  export * from './abstract-logger';
18
+ export * from './console-logger';
18
19
  export * from './client';
20
+ export * from './client-ids';
19
21
  export * from './clock';
20
22
  export * from './data';
21
23
  export * from './browser-runtime';
22
24
  export * from './debouncer';
25
+ export * from './glsp-edits-settled-actions';
26
+ export * from './glsp-request-model-args';
27
+ export * from './glsp-save-model-actions';
23
28
  export * from './errors';
29
+ export * from './format-log-arg';
24
30
  export * from './host-diagnostics';
25
31
  export * from './logger';
26
32
  export * from './latency-collector';
@@ -29,17 +35,22 @@ export * from './latency-collector';
29
35
  // the modules that raise them.
30
36
  export * from './messages/primitives';
31
37
  export * from './patch-merge';
38
+ export * from './reconcile-write';
32
39
  export * from './noop-logger';
33
40
  export * from './observable-value';
41
+ export * from './pending-work-tracker';
34
42
  export * from './profile-session';
35
43
  export * from './profiling';
36
44
  export * from './tracer';
37
45
  export * from './transfer-diagnostic';
38
46
  export * from './transfer-element';
47
+ export * from './types';
39
48
  export * from './model-service';
40
49
  export * from './transfer-document';
41
50
  export * from './model-server';
42
51
  export * from './project';
52
+ export * from './random-uuid';
53
+ export * from './save-text-document';
43
54
  export * from './rpc';
44
55
  export * from './uri';
45
56
  export * from './util';
@@ -87,8 +87,13 @@ export interface LatencyReport {
87
87
  */
88
88
  export type LatencyRetention = { readonly kind: 'keep-all' } | { readonly kind: 'ring-buffer'; readonly maxSamplesPerMethod: number };
89
89
 
90
- /** Per-method state: retained samples (bounded in ring-buffer mode) plus lifetime totals. */
91
- interface MethodAccumulator {
90
+ /**
91
+ * Per-method state: retained samples (bounded in ring-buffer mode) plus lifetime
92
+ * totals. The mutable tally behind {@link MethodLatency}, which is the derived
93
+ * report row. Held by the `protected` {@link LatencyCollector.durations}, so a
94
+ * subclass recording its own samples has to name it.
95
+ */
96
+ export interface MethodLatencyTally {
92
97
  /** Retained durations for percentile estimation; capped in ring-buffer mode. Order is irrelevant (report sorts). */
93
98
  readonly samples: number[];
94
99
  /** Next slot to overwrite once the ring buffer is full (ring-buffer mode only). */
@@ -119,7 +124,9 @@ function percentile(sortedAscending: readonly number[], percent: number): number
119
124
  * deterministic on a fake clock.
120
125
  */
121
126
  export class LatencyCollector {
122
- protected readonly durations = new Map<string, MethodAccumulator>();
127
+ /** One tally per method name. Emptied by {@link reset}. */
128
+ protected readonly durations = new Map<string, MethodLatencyTally>();
129
+ /** Restarted by {@link reset}. Its reading is the report's `windowMs`. */
123
130
  protected window: Stopwatch;
124
131
 
125
132
  constructor(
package/src/logger.ts CHANGED
@@ -24,6 +24,7 @@ export const LEVEL_LABELS: Record<LogLevel, string> = {
24
24
  /** Module-global threshold; shared by every `AbstractLogger` instance so derived child
25
25
  * loggers pick up live updates without each having to subscribe to a configuration source. */
26
26
  let currentLevel: LogThreshold = 'info';
27
+ let fileLevel: LogThreshold | undefined;
27
28
 
28
29
  /**
29
30
  * Cross-side public logger contract. Every consumer of the framework — browser
@@ -38,15 +39,15 @@ let currentLevel: LogThreshold = 'info';
38
39
  * and every `for` / `sub` / `with` derivation preserves the implementation type.
39
40
  */
40
41
  export interface Logger {
41
- /** Log a message at error level. */
42
+ /** Requested work failed; logged once, by the code that ends the failure, not by one that rethrows it. */
42
43
  error(message?: string, ...args: unknown[]): this;
43
- /** Log a message at warn level. */
44
+ /** An anomaly that was handled, or work declined by design, such as a conflict or a cap; a clean run logs none. */
44
45
  warn(message?: string, ...args: unknown[]): this;
45
- /** Log a message at info level. */
46
+ /** A milestone the person running the server would recognise without knowing the internals. */
46
47
  info(message?: string, ...args: unknown[]): this;
47
- /** Log a message at debug level. */
48
+ /** Diagnosis; its level decides whether the line is built, never which code runs. */
48
49
  debug(message?: string, ...args: unknown[]): this;
49
- /** High-volume diagnostic lines, only emitted at the `'trace'` threshold. */
50
+ /** High-volume diagnosis, emitted only at the `'trace'` threshold; as for `debug`, no code runs only for it. */
50
51
  trace(message?: string, ...args: unknown[]): this;
51
52
  /** Log a message at info-level threshold with a `Log` label (rather than `Info`). */
52
53
  log(message?: string, ...args: unknown[]): this;
@@ -93,12 +94,32 @@ export namespace Logger {
93
94
  return currentLevel;
94
95
  }
95
96
  /**
96
- * Whether a message at `level` would be emitted at the current threshold.
97
+ * Set the log file's own threshold, which {@link setLevel} does not move;
98
+ * `undefined` makes the file follow {@link getLevel}. A file sink compares
99
+ * against `getFileLevel() ?? getLevel()`, every other sink against
100
+ * {@link getLevel}.
101
+ *
102
+ * Set it through the file sink's own configuration, which clears it with
103
+ * the file: set here with no file configured, it widens
104
+ * {@link isLevelEnabled} for lines nothing writes.
105
+ */
106
+ export function setFileLevel(level: LogThreshold | undefined): void {
107
+ fileLevel = level;
108
+ }
109
+ /** Read the log file's own threshold, `undefined` while it follows {@link getLevel}. */
110
+ export function getFileLevel(): LogThreshold | undefined {
111
+ return fileLevel;
112
+ }
113
+ /**
114
+ * Whether any sink takes a message at `level`: the more verbose of
115
+ * {@link getLevel} and {@link getFileLevel} decides. A sink that admits less
116
+ * re-checks its own threshold before writing.
117
+ *
97
118
  * Use to guard expensive log-line construction:
98
119
  * `if (Logger.isLevelEnabled('trace')) logger.trace(buildPayload())`.
99
120
  */
100
121
  export function isLevelEnabled(level: LogLevel): boolean {
101
- return LEVEL_ORDER[level] <= LEVEL_ORDER[currentLevel];
122
+ return LEVEL_ORDER[level] <= Math.max(LEVEL_ORDER[currentLevel], fileLevel ? LEVEL_ORDER[fileLevel] : 0);
102
123
  }
103
124
  /**
104
125
  * Whether a message at `threshold` would be emitted: `false` for `'off'`,
@@ -134,8 +155,23 @@ export function parseLogLevel(value: unknown): LogThreshold | undefined {
134
155
  * instead of duplicating the literal string.
135
156
  */
136
157
  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}. */
158
+ /**
159
+ * Env var the server reads its log file-tee target from. The path may hold
160
+ * `{workspace}`, filled in once the server knows its workspace, and `{pid}`.
161
+ * See {@link DEFAULT_LOG_LEVEL_ENV}.
162
+ */
138
163
  export const DEFAULT_LOG_FILE_ENV = 'HYDRANIUM_LOG_FILE';
164
+ /**
165
+ * Env var the server reads the file tee's own threshold from, which the LSP
166
+ * log-level setting does not change. See {@link Logger.setFileLevel}.
167
+ */
168
+ export const DEFAULT_LOG_FILE_LEVEL_ENV = 'HYDRANIUM_LOG_FILE_LEVEL';
169
+ /**
170
+ * Env var naming the file a Node server writes its CPU profile to when it
171
+ * ends. Takes `{workspace}` and `{pid}` as {@link DEFAULT_LOG_FILE_ENV} does;
172
+ * the write replaces the file, so `{pid}` keeps one per server.
173
+ */
174
+ export const DEFAULT_CPU_PROFILE_FILE_ENV = 'HYDRANIUM_CPU_PROFILE_FILE';
139
175
 
140
176
  /**
141
177
  * Human-readable formatting helpers used in log lines and diagnostic output.
@@ -25,8 +25,17 @@
25
25
 
26
26
  export * from './primitives';
27
27
 
28
- export { STALE_BASED_UPDATE } from '../errors';
29
- export { DATA_SERVER_CONNECT_FAILED, DATA_SERVER_NOT_READY } from '../client/data-session';
28
+ export {
29
+ DOCUMENT_NOT_OPEN,
30
+ DUPLICATE_CLIENT_ID,
31
+ SAVE_FAILED,
32
+ SESSION_CLOSED,
33
+ STALE_BASE_VERSION_SAVE,
34
+ STALE_BASE_VERSION_UPDATE
35
+ } from '../errors';
36
+ export { DATA_CONNECTION_SESSION_RESTORE_FAILED, DATA_CONNECTION_WATCH_RESTORE_FAILED } from '../client/data-connection';
37
+ export { DATA_SERVER_CONNECT_FAILED, DATA_SERVER_NOT_READY } from '../client/rpc-connection';
38
+ export { DATA_SESSION_ANSWER_WITHOUT_MODEL, DATA_SESSION_RESTORE_FAILED, DATA_SESSION_UNSAVED_LOST } from '../client/data-session';
30
39
  export {
31
40
  RELAY_REPLAY_FAILED,
32
41
  RELAY_TRANSPORT_OPEN_FAILED,
@@ -162,11 +162,67 @@ export function messageError<S extends string>(
162
162
  }
163
163
 
164
164
  /**
165
- * Render on the side that knows the reading user's locale. `translations` is
166
- * whatever flat `code → template` map the host exposes; omitting it is how an
167
- * adopter without i18n opts out, and yields the English.
165
+ * The reading user's language, as the tag a client declared.
166
+ *
167
+ * Neither validated nor normalised anywhere: rejecting an unfamiliar tag would
168
+ * be selecting a locale, which the framework does not do.
169
+ *
170
+ * NOT a Langium language id, which names a grammar. The two are strings of the
171
+ * same shape reachable from the same services, so one used where the other
172
+ * belongs misses every lookup rather than failing.
173
+ */
174
+ export type Locale = string;
175
+
176
+ /**
177
+ * A host's templates for one locale, keyed by the whole message code.
178
+ *
179
+ * **Flat, not nested.** A code is one key here, `/` separators included; a host
180
+ * whose own catalogue format nests — Theia's does — flattens on the way in. A
181
+ * map that nests instead misses every lookup while type-checking, because the
182
+ * value this is indexed by is `ResolvedMessage.code`.
183
+ *
184
+ * **Partial by nature rather than by convention.** A code with no entry renders
185
+ * its English default, so a catalogue covering none of a package's codes is a
186
+ * valid catalogue and not a broken one — which is what lets an adopter translate
187
+ * as much or as little as they like.
188
+ *
189
+ * Deliberately NOT narrowed to the codes that exist, and a `Code extends string`
190
+ * parameter would not buy what it appears to: a catalogue is loaded as JSON, so
191
+ * it reaches a checked position as a variable rather than as an object literal,
192
+ * and excess-property checking — the only thing that would reject a mistyped
193
+ * key — does not run there. The gain is a compile error for a catalogue with no
194
+ * correct key at all; the cost is a code union to maintain by hand, since a
195
+ * declaration does not carry its code as a literal type.
168
196
  */
169
- export function renderFrameworkMessage(message: ResolvedMessage, translations?: Record<string, string>): string {
197
+ export type MessageCatalogue = Readonly<Record<string, string>>;
198
+
199
+ export namespace MessageCatalogue {
200
+ /**
201
+ * One catalogue from several, for a renderer whose override supplies entries
202
+ * over the ones it inherits.
203
+ *
204
+ * **Later wins**, so a caller passes the inherited answer before its own.
205
+ *
206
+ * **No source at all answers `undefined`, not `{}`.** A renderer holds that
207
+ * answer as the locale having no catalogue and renders straight through; an
208
+ * empty object is a catalogue that misses every lookup, so returning one
209
+ * turns the opt-out into a per-message search that yields the same English.
210
+ */
211
+ export function merge(...catalogues: ReadonlyArray<MessageCatalogue | undefined>): MessageCatalogue | undefined {
212
+ const present = catalogues.filter((catalogue): catalogue is MessageCatalogue => catalogue !== undefined);
213
+ if (present.length <= 1) {
214
+ return present[0];
215
+ }
216
+ return Object.assign({}, ...present) as MessageCatalogue;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Render on the side that knows the reading user's locale. Omitting
222
+ * `translations` is how an adopter without i18n opts out, and yields the
223
+ * English.
224
+ */
225
+ export function renderFrameworkMessage(message: ResolvedMessage, translations?: MessageCatalogue): string {
170
226
  const template = translations?.[message.code];
171
227
  return template ? interpolate(template, message.params) : message.text;
172
228
  }
@@ -180,9 +236,77 @@ export function resolvedFromResponseError(error: ResponseError<unknown>): Resolv
180
236
  * to pass as a parameter for the same reason a number is: it is not itself
181
237
  * translatable text, so it needs no code of its own. A PROSE fragment is not,
182
238
  * and must become one code per value instead.
239
+ *
240
+ * For a reader of the client; the log takes {@link describeErrorWithStack}.
183
241
  */
184
242
  export function describeError(error: unknown): string {
185
- return error instanceof Error ? error.message : String(error);
243
+ return describeSafely(error, () => (error instanceof Error ? error.message : error));
244
+ }
245
+
246
+ /**
247
+ * An error for the log: its stack where it has one, which names where it was
248
+ * thrown, and its causes, which name what failed when an error wraps another.
249
+ * Never for the client, whose reader cannot act on a stack.
250
+ */
251
+ export function describeErrorWithStack(error: unknown): string {
252
+ // A cause chain may loop back on itself.
253
+ return describeChain(error, new Set());
254
+ }
255
+
256
+ function describeChain(error: unknown, seen: Set<unknown>): string {
257
+ const lines: string[] = [];
258
+ let current: unknown = error;
259
+ let prefix = '';
260
+ try {
261
+ do {
262
+ seen.add(current);
263
+ const described = current;
264
+ lines.push(prefix + describeSafely(described, () => (described instanceof Error ? stackOrMessage(described) : described)));
265
+ // Node fails a connect to several addresses with one, whose own message is empty.
266
+ if (current instanceof AggregateError) {
267
+ try {
268
+ const errors: unknown[] = current.errors;
269
+ lines.push(...errors.filter(inner => !seen.has(inner)).map(inner => `Including: ${describeChain(inner, seen)}`));
270
+ } catch {
271
+ // An `errors` that is no list, or throws when read; the cause still follows.
272
+ }
273
+ }
274
+ current = current instanceof Error ? current.cause : undefined;
275
+ prefix = 'Caused by: ';
276
+ } while (current !== undefined && current !== null && !seen.has(current));
277
+ } catch {
278
+ // A getter or proxy trap threw while walking the chain; keep what was described.
279
+ }
280
+ return lines.join('\n');
281
+ }
282
+
283
+ /** `error`'s stack as a string, or its message where reading or converting the stack throws. */
284
+ function stackOrMessage(error: Error): string {
285
+ try {
286
+ return String(error.stack ?? error.message);
287
+ } catch {
288
+ return String(error.message);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * What `select()` picks from `value`, as a string, or where selecting or
294
+ * converting it throws, as `String` does for an object without a prototype,
295
+ * the `[object Type]` tag, or where reading the tag throws too, the value's
296
+ * type. Describing an error must not throw, or the failure it reports is lost;
297
+ * the conversion sits inside the guard because a thrown value's `message` or
298
+ * `stack` need not be a string.
299
+ */
300
+ function describeSafely(value: unknown, select: () => unknown): string {
301
+ try {
302
+ return String(select());
303
+ } catch {
304
+ try {
305
+ return Object.prototype.toString.call(value);
306
+ } catch {
307
+ return `[${typeof value}]`;
308
+ }
309
+ }
186
310
  }
187
311
 
188
312
  /**