@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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/lib/abstract-logger.d.ts +66 -0
- package/lib/abstract-logger.d.ts.map +1 -0
- package/lib/abstract-logger.js +110 -0
- package/lib/abstract-logger.js.map +1 -0
- package/lib/browser-runtime.d.ts +124 -0
- package/lib/browser-runtime.d.ts.map +1 -0
- package/lib/browser-runtime.js +11 -0
- package/lib/browser-runtime.js.map +1 -0
- package/lib/client/data-events.d.ts +49 -0
- package/lib/client/data-events.d.ts.map +1 -0
- package/lib/client/data-events.js +60 -0
- package/lib/client/data-events.js.map +1 -0
- package/lib/client/data-port.d.ts +95 -0
- package/lib/client/data-port.d.ts.map +1 -0
- package/lib/client/data-port.js +11 -0
- package/lib/client/data-port.js.map +1 -0
- package/lib/client/data-session.d.ts +129 -0
- package/lib/client/data-session.d.ts.map +1 -0
- package/lib/client/data-session.js +179 -0
- package/lib/client/data-session.js.map +1 -0
- package/lib/client/index.d.ts +39 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +55 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/message-relay.d.ts +111 -0
- package/lib/client/message-relay.d.ts.map +1 -0
- package/lib/client/message-relay.js +139 -0
- package/lib/client/message-relay.js.map +1 -0
- package/lib/client/post-message-transport.d.ts +77 -0
- package/lib/client/post-message-transport.d.ts.map +1 -0
- package/lib/client/post-message-transport.js +95 -0
- package/lib/client/post-message-transport.js.map +1 -0
- package/lib/clock.d.ts +111 -0
- package/lib/clock.d.ts.map +1 -0
- package/lib/clock.js +74 -0
- package/lib/clock.js.map +1 -0
- package/lib/data/data-protocol-methods.d.ts +39 -0
- package/lib/data/data-protocol-methods.d.ts.map +1 -0
- package/lib/data/data-protocol-methods.js +61 -0
- package/lib/data/data-protocol-methods.js.map +1 -0
- package/lib/data/data-server-protocol.d.ts +264 -0
- package/lib/data/data-server-protocol.d.ts.map +1 -0
- package/lib/data/data-server-protocol.js +11 -0
- package/lib/data/data-server-protocol.js.map +1 -0
- package/lib/data/diagnostics.d.ts +97 -0
- package/lib/data/diagnostics.d.ts.map +1 -0
- package/lib/data/diagnostics.js +25 -0
- package/lib/data/diagnostics.js.map +1 -0
- package/lib/data/events.d.ts +107 -0
- package/lib/data/events.d.ts.map +1 -0
- package/lib/data/events.js +11 -0
- package/lib/data/events.js.map +1 -0
- package/lib/data/index.d.ts +15 -0
- package/lib/data/index.d.ts.map +1 -0
- package/lib/data/index.js +38 -0
- package/lib/data/index.js.map +1 -0
- package/lib/data/methods.d.ts +57 -0
- package/lib/data/methods.d.ts.map +1 -0
- package/lib/data/methods.js +60 -0
- package/lib/data/methods.js.map +1 -0
- package/lib/data/requests.d.ts +65 -0
- package/lib/data/requests.d.ts.map +1 -0
- package/lib/data/requests.js +11 -0
- package/lib/data/requests.js.map +1 -0
- package/lib/debouncer.d.ts +75 -0
- package/lib/debouncer.d.ts.map +1 -0
- package/lib/debouncer.js +91 -0
- package/lib/debouncer.js.map +1 -0
- package/lib/errors.d.ts +84 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +113 -0
- package/lib/errors.js.map +1 -0
- package/lib/host-diagnostics.d.ts +58 -0
- package/lib/host-diagnostics.d.ts.map +1 -0
- package/lib/host-diagnostics.js +18 -0
- package/lib/host-diagnostics.js.map +1 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/latency-collector.d.ts +126 -0
- package/lib/latency-collector.d.ts.map +1 -0
- package/lib/latency-collector.js +137 -0
- package/lib/latency-collector.js.map +1 -0
- package/lib/logger.d.ts +119 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +126 -0
- package/lib/logger.js.map +1 -0
- package/lib/model-server.d.ts +352 -0
- package/lib/model-server.d.ts.map +1 -0
- package/lib/model-server.js +160 -0
- package/lib/model-server.js.map +1 -0
- package/lib/model-service/args.d.ts +64 -0
- package/lib/model-service/args.d.ts.map +1 -0
- package/lib/model-service/args.js +11 -0
- package/lib/model-service/args.js.map +1 -0
- package/lib/model-service/index.d.ts +11 -0
- package/lib/model-service/index.d.ts.map +1 -0
- package/lib/model-service/index.js +31 -0
- package/lib/model-service/index.js.map +1 -0
- package/lib/model-service/reference-candidate.d.ts +56 -0
- package/lib/model-service/reference-candidate.d.ts.map +1 -0
- package/lib/model-service/reference-candidate.js +11 -0
- package/lib/model-service/reference-candidate.js.map +1 -0
- package/lib/noop-logger.d.ts +25 -0
- package/lib/noop-logger.d.ts.map +1 -0
- package/lib/noop-logger.js +36 -0
- package/lib/noop-logger.js.map +1 -0
- package/lib/observable-value.d.ts +72 -0
- package/lib/observable-value.d.ts.map +1 -0
- package/lib/observable-value.js +52 -0
- package/lib/observable-value.js.map +1 -0
- package/lib/patch-merge.d.ts +123 -0
- package/lib/patch-merge.d.ts.map +1 -0
- package/lib/patch-merge.js +104 -0
- package/lib/patch-merge.js.map +1 -0
- package/lib/profile-session.d.ts +87 -0
- package/lib/profile-session.d.ts.map +1 -0
- package/lib/profile-session.js +98 -0
- package/lib/profile-session.js.map +1 -0
- package/lib/profiling.d.ts +83 -0
- package/lib/profiling.d.ts.map +1 -0
- package/lib/profiling.js +20 -0
- package/lib/profiling.js.map +1 -0
- package/lib/project.d.ts +121 -0
- package/lib/project.d.ts.map +1 -0
- package/lib/project.js +34 -0
- package/lib/project.js.map +1 -0
- package/lib/rpc/bind-rpc-methods.d.ts +100 -0
- package/lib/rpc/bind-rpc-methods.d.ts.map +1 -0
- package/lib/rpc/bind-rpc-methods.js +110 -0
- package/lib/rpc/bind-rpc-methods.js.map +1 -0
- package/lib/rpc/create-rpc-proxy.d.ts +188 -0
- package/lib/rpc/create-rpc-proxy.d.ts.map +1 -0
- package/lib/rpc/create-rpc-proxy.js +158 -0
- package/lib/rpc/create-rpc-proxy.js.map +1 -0
- package/lib/rpc/index.d.ts +12 -0
- package/lib/rpc/index.d.ts.map +1 -0
- package/lib/rpc/index.js +31 -0
- package/lib/rpc/index.js.map +1 -0
- package/lib/rpc/wire-prefix.d.ts +37 -0
- package/lib/rpc/wire-prefix.d.ts.map +1 -0
- package/lib/rpc/wire-prefix.js +49 -0
- package/lib/rpc/wire-prefix.js.map +1 -0
- package/lib/testing/data-doubles.d.ts +121 -0
- package/lib/testing/data-doubles.d.ts.map +1 -0
- package/lib/testing/data-doubles.js +102 -0
- package/lib/testing/data-doubles.js.map +1 -0
- package/lib/testing/fake-clock.d.ts +35 -0
- package/lib/testing/fake-clock.d.ts.map +1 -0
- package/lib/testing/fake-clock.js +86 -0
- package/lib/testing/fake-clock.js.map +1 -0
- package/lib/testing/harness.d.ts +24 -0
- package/lib/testing/harness.d.ts.map +1 -0
- package/lib/testing/harness.js +11 -0
- package/lib/testing/harness.js.map +1 -0
- package/lib/testing/index.d.ts +13 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +41 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/node/duplex-connection.d.ts +25 -0
- package/lib/testing/node/duplex-connection.d.ts.map +1 -0
- package/lib/testing/node/duplex-connection.js +34 -0
- package/lib/testing/node/duplex-connection.js.map +1 -0
- package/lib/testing/node/duplex-stream.d.ts +32 -0
- package/lib/testing/node/duplex-stream.d.ts.map +1 -0
- package/lib/testing/node/duplex-stream.js +35 -0
- package/lib/testing/node/duplex-stream.js.map +1 -0
- package/lib/testing/node/index.d.ts +11 -0
- package/lib/testing/node/index.d.ts.map +1 -0
- package/lib/testing/node/index.js +37 -0
- package/lib/testing/node/index.js.map +1 -0
- package/lib/testing/wait-for.d.ts +69 -0
- package/lib/testing/wait-for.d.ts.map +1 -0
- package/lib/testing/wait-for.js +67 -0
- package/lib/testing/wait-for.js.map +1 -0
- package/lib/tracer.d.ts +115 -0
- package/lib/tracer.d.ts.map +1 -0
- package/lib/tracer.js +205 -0
- package/lib/tracer.js.map +1 -0
- package/lib/transfer-diagnostic.d.ts +73 -0
- package/lib/transfer-diagnostic.d.ts.map +1 -0
- package/lib/transfer-diagnostic.js +45 -0
- package/lib/transfer-diagnostic.js.map +1 -0
- package/lib/transfer-document.d.ts +73 -0
- package/lib/transfer-document.d.ts.map +1 -0
- package/lib/transfer-document.js +46 -0
- package/lib/transfer-document.js.map +1 -0
- package/lib/transfer-element.d.ts +39 -0
- package/lib/transfer-element.d.ts.map +1 -0
- package/lib/transfer-element.js +11 -0
- package/lib/transfer-element.js.map +1 -0
- package/lib/uri.d.ts +62 -0
- package/lib/uri.d.ts.map +1 -0
- package/lib/uri.js +28 -0
- package/lib/uri.js.map +1 -0
- package/lib/util.d.ts +147 -0
- package/lib/util.d.ts.map +1 -0
- package/lib/util.js +211 -0
- package/lib/util.js.map +1 -0
- package/package.json +100 -0
- package/src/abstract-logger.ts +120 -0
- package/src/browser-runtime.ts +135 -0
- package/src/client/data-events.ts +71 -0
- package/src/client/data-port.ts +99 -0
- package/src/client/data-session.ts +209 -0
- package/src/client/index.ts +40 -0
- package/src/client/message-relay.ts +216 -0
- package/src/client/post-message-transport.ts +167 -0
- package/src/clock.ts +173 -0
- package/src/data/data-protocol-methods.ts +79 -0
- package/src/data/data-server-protocol.ts +296 -0
- package/src/data/diagnostics.ts +116 -0
- package/src/data/events.ts +127 -0
- package/src/data/index.ts +23 -0
- package/src/data/methods.ts +59 -0
- package/src/data/requests.ts +70 -0
- package/src/debouncer.ts +118 -0
- package/src/errors.ts +127 -0
- package/src/host-diagnostics.ts +63 -0
- package/src/index.ts +41 -0
- package/src/latency-collector.ts +231 -0
- package/src/logger.ts +172 -0
- package/src/model-server.ts +479 -0
- package/src/model-service/args.ts +67 -0
- package/src/model-service/index.ts +16 -0
- package/src/model-service/reference-candidate.ts +58 -0
- package/src/noop-logger.ts +36 -0
- package/src/observable-value.ts +92 -0
- package/src/patch-merge.ts +165 -0
- package/src/profile-session.ts +164 -0
- package/src/profiling.ts +113 -0
- package/src/project.ts +125 -0
- package/src/rpc/README.md +163 -0
- package/src/rpc/bind-rpc-methods.ts +180 -0
- package/src/rpc/create-rpc-proxy.ts +292 -0
- package/src/rpc/index.ts +16 -0
- package/src/rpc/wire-prefix.ts +48 -0
- package/src/testing/data-doubles.ts +178 -0
- package/src/testing/fake-clock.ts +106 -0
- package/src/testing/harness.ts +24 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/node/duplex-connection.ts +50 -0
- package/src/testing/node/duplex-stream.ts +51 -0
- package/src/testing/node/index.ts +22 -0
- package/src/testing/wait-for.ts +94 -0
- package/src/tracer.ts +277 -0
- package/src/transfer-diagnostic.ts +92 -0
- package/src/transfer-document.ts +99 -0
- package/src/transfer-element.ts +43 -0
- package/src/uri.ts +61 -0
- 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;
|