@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/util.ts ADDED
@@ -0,0 +1,250 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2023 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
+ /** A user-defined type predicate. */
11
+ export type TypeGuard<T> = (item: unknown) => item is T;
12
+
13
+ /**
14
+ * Minimal disposable shape, intentionally identical to `vscode-jsonrpc.Disposable`
15
+ * by structural typing so it interoperates without an actual dependency on that
16
+ * library. Consumers may pass instances of this type wherever a `Disposable`
17
+ * is expected, and vice versa.
18
+ *
19
+ * Re-defined locally rather than re-exported from `vscode-jsonrpc` so adopters
20
+ * pulling this interface in do not transitively see `vscode-jsonrpc` types as
21
+ * part of the framework's public surface.
22
+ */
23
+ export interface Disposable {
24
+ dispose(): void;
25
+ }
26
+
27
+ export namespace Disposable {
28
+ export function create(dispose: () => void): Disposable {
29
+ return { dispose };
30
+ }
31
+ /**
32
+ * Shared no-op {@link Disposable} — `dispose()` does nothing. Use wherever a
33
+ * disposable is required but there is nothing to release (a subscription with
34
+ * no teardown, a listener registration in a test double) instead of a fresh
35
+ * `{ dispose: () => undefined }` literal. Frozen so the shared instance can't
36
+ * be mutated.
37
+ */
38
+ export const EMPTY: Disposable = Object.freeze({ dispose: () => undefined });
39
+ }
40
+
41
+ /**
42
+ * A collection of disposables that itself implements {@link Disposable}.
43
+ *
44
+ * - {@link dispose} is idempotent: repeat calls are safe.
45
+ * - {@link push} returns the added disposable for chaining.
46
+ * - Push-after-disposal disposes the added item immediately (prevents leaks
47
+ * from late arrivals on a collection that has already been torn down).
48
+ * - LIFO drain order: last-pushed disposes first. Matches the convention that
49
+ * later disposables typically depend on earlier ones (e.g., a subscription
50
+ * depends on a connection, so the subscription must dispose first before
51
+ * the connection tears down its transport).
52
+ * - A throwing dispose does not strand subsequent disposables in the drain.
53
+ *
54
+ * Fills the gap left by vscode-jsonrpc and Langium, which provide only the
55
+ * `Disposable` interface and a `Disposable.create()` factory.
56
+ */
57
+ export class DisposableCollection implements Disposable {
58
+ protected readonly disposables: Disposable[] = [];
59
+ protected _disposed = false;
60
+
61
+ get disposed(): boolean {
62
+ return this._disposed;
63
+ }
64
+
65
+ push(disposable: Disposable): Disposable {
66
+ if (this._disposed) {
67
+ disposable.dispose();
68
+ } else {
69
+ this.disposables.push(disposable);
70
+ }
71
+ return disposable;
72
+ }
73
+
74
+ dispose(): void {
75
+ if (this._disposed) {
76
+ return;
77
+ }
78
+ this._disposed = true;
79
+ while (this.disposables.length > 0) {
80
+ try {
81
+ this.disposables.pop()!.dispose();
82
+ } catch (_err) {
83
+ // Continue draining — one failed dispose shouldn't strand the rest.
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * A `Promise` paired with its `resolve` / `reject` callbacks, so producer
91
+ * and consumer code can be in different scopes. Use when a constructor
92
+ * needs to expose a `ready` promise that a later method resolves.
93
+ *
94
+ * Equivalent to Theia / GLSP / Langium's `Deferred` utility — re-implemented
95
+ * here so `@hydranium/protocol` stays runtime-dependency-free.
96
+ */
97
+ export class Deferred<T = void> {
98
+ readonly promise: Promise<T>;
99
+ resolve!: (value: T | PromiseLike<T>) => void;
100
+ reject!: (reason?: unknown) => void;
101
+
102
+ constructor() {
103
+ this.promise = new Promise<T>((resolve, reject) => {
104
+ this.resolve = resolve;
105
+ this.reject = reject;
106
+ });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * A value that is either `T` directly (sync) or a thenable that resolves to `T`
112
+ * (async). Used in framework hook signatures where adopter overrides may be
113
+ * either sync or async without forcing the synchronous fast path to pay an
114
+ * unconditional microtask tick.
115
+ *
116
+ * Uses the permissive `PromiseLike<T>` form so externally-produced thenables
117
+ * (non-native Promise implementations) also flow through.
118
+ *
119
+ * Pair with {@link isPromiseLike} at hot-loop call sites to preserve
120
+ * the sync fast path (an `await` on a non-promise still schedules a microtask).
121
+ */
122
+ export type MaybePromise<T> = T | PromiseLike<T>;
123
+
124
+ /**
125
+ * Type guard distinguishing the async branch of {@link MaybePromise}.
126
+ *
127
+ * At framework call sites in tight loops, guarding before the `await` keeps
128
+ * sync implementations on the synchronous fast path — `await` on
129
+ * a non-promise still schedules a microtask, which for per-node loops adds
130
+ * up. Outside of tight loops a naive `await maybe` is equivalent and
131
+ * shorter; reach for the guard only when the call sits on the build's
132
+ * critical path.
133
+ *
134
+ * Thenable check (`'then' in value`) rather than `instanceof Promise` so
135
+ * non-native Promise implementations also classify as async.
136
+ */
137
+ export function isPromiseLike<T = unknown>(value: MaybePromise<T>): value is PromiseLike<T> {
138
+ return !!value && typeof (value as PromiseLike<T>).then === 'function';
139
+ }
140
+
141
+ /** Removes `readonly` from all properties of `T`. */
142
+ export type Mutable<T> = { -readonly [P in keyof T]: T[P] };
143
+
144
+ /** Returns the same value typed as a {@link Mutable}. Compile-time only. */
145
+ export function asMutable<T>(item: T): Mutable<T> {
146
+ return item;
147
+ }
148
+
149
+ /** Type-guard variant: narrows `item` to `Mutable<T> | undefined` if `guard` accepts it. */
150
+ export function toMutable<T>(item: unknown, guard: TypeGuard<T>): item is Mutable<T> | undefined;
151
+ export function toMutable<T>(item: unknown, guard?: TypeGuard<T>): item is Mutable<T> | undefined {
152
+ return guard ? guard(item) : true;
153
+ }
154
+
155
+ /**
156
+ * Wrap `text` in `quoteChar`, escaping interior occurrences with `replaceChar`.
157
+ *
158
+ * - Empty input becomes a pair of quote characters.
159
+ * - Pre-existing surrounding quotes are not double-wrapped.
160
+ * - Interior `quoteChar` runs are mapped to `replaceChar`.
161
+ */
162
+ export function quote(text: string, quoteChar = '"', replaceChar = "'"): string {
163
+ if (text.length === 0) {
164
+ return quoteChar + quoteChar;
165
+ }
166
+ let quoted = text;
167
+ if (!quoted.startsWith(quoteChar)) {
168
+ quoted = quoteChar + quoted;
169
+ }
170
+ if (!quoted.endsWith(quoteChar)) {
171
+ quoted += quoteChar;
172
+ }
173
+ return (
174
+ quoteChar +
175
+ quoted
176
+ .substring(1, quoted.length - 1)
177
+ .split(quoteChar)
178
+ .join(replaceChar) +
179
+ quoteChar
180
+ );
181
+ }
182
+
183
+ /** Strip a single layer of `quoteChar` from the start and end of `text` if present. */
184
+ export function unquote(text: string, quoteChar = '"'): string {
185
+ const start = text.startsWith(quoteChar) ? 1 : undefined;
186
+ const end = text.endsWith(quoteChar) ? -1 : undefined;
187
+ return text.slice(start, end);
188
+ }
189
+
190
+ /** Capitalise the first character of `input`. */
191
+ export function toPascal(input: string): string {
192
+ return input.charAt(0).toLocaleUpperCase() + input.slice(1);
193
+ }
194
+
195
+ /**
196
+ * Format a [codicon](https://github.com/microsoft/vscode-codicons) name as the
197
+ * CSS class string consumed by VS Code- and Theia-style icon hosts.
198
+ *
199
+ * Presentation, not wire contract, and a duplicate of `@eclipse-glsp/client`'s
200
+ * export of the same name — a file importing both barrels gets an ambiguity, so
201
+ * client code that already depends on GLSP takes the upstream one. It stays on
202
+ * this barrel because adopter protocol packages re-export these helpers
203
+ * wholesale, which makes relocating it a breaking change rather than a move.
204
+ */
205
+ export function codiconCSSString(icon: string): string {
206
+ return `codicon codicon-${icon}`;
207
+ }
208
+
209
+ /** The identity function. Useful as a default mapping callback. */
210
+ export function identity<T>(value: T): T {
211
+ return value;
212
+ }
213
+
214
+ /** Find a name based on `suggestion` that is not already in `existing`, appending an
215
+ * incrementing numeric suffix until a free slot is found. */
216
+ export function findNextUnique(suggestion: string, existing: string[]): string;
217
+ export function findNextUnique<T>(suggestion: string, existing: T[], nameGetter: (element: T) => string): string;
218
+ export function findNextUnique<T>(suggestion: string, existing: T[], nameGetter?: (element: T) => string): string {
219
+ const names = nameGetter ? existing.map(nameGetter) : (existing as string[]);
220
+ let name = suggestion;
221
+ let index = 1;
222
+ while (names.includes(name)) {
223
+ name = suggestion + index++;
224
+ }
225
+ return name;
226
+ }
227
+
228
+ /** Throw if reached. Helper for exhaustiveness checks at the end of a discriminated-union switch. */
229
+ export function unreachable(input: never): never {
230
+ throw new Error('Value detected in unreachable assertion: ' + `${input}`);
231
+ }
232
+
233
+ /**
234
+ * Resolve a nested value from a plain object using a path of property names.
235
+ * Returns `undefined` for any segment that is not an object or is null.
236
+ *
237
+ * The generic `T` is a caller-provided assertion about the value's type — it
238
+ * is not validated at runtime. Use `unknown` (the default) if the type is
239
+ * uncertain, then narrow with a type-guard.
240
+ */
241
+ export function getAt<T = unknown>(obj: unknown, path: string[]): T | undefined {
242
+ let cur: unknown = obj;
243
+ for (const seg of path) {
244
+ if (cur == null || typeof cur !== 'object') {
245
+ return undefined;
246
+ }
247
+ cur = (cur as Record<string, unknown>)[seg];
248
+ }
249
+ return cur as T;
250
+ }