@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.
- package/README.md +44 -83
- package/lib/abstract-logger.d.ts +11 -0
- package/lib/abstract-logger.d.ts.map +1 -1
- package/lib/abstract-logger.js +16 -0
- package/lib/abstract-logger.js.map +1 -1
- package/lib/client/data-connection.d.ts +303 -0
- package/lib/client/data-connection.d.ts.map +1 -0
- package/lib/client/data-connection.js +534 -0
- package/lib/client/data-connection.js.map +1 -0
- package/lib/client/data-events.d.ts +18 -1
- package/lib/client/data-events.d.ts.map +1 -1
- package/lib/client/data-events.js +26 -0
- package/lib/client/data-events.js.map +1 -1
- package/lib/client/data-port.d.ts +34 -24
- package/lib/client/data-port.d.ts.map +1 -1
- package/lib/client/data-session.d.ts +691 -89
- package/lib/client/data-session.d.ts.map +1 -1
- package/lib/client/data-session.js +1155 -115
- package/lib/client/data-session.js.map +1 -1
- package/lib/client/index.d.ts +15 -9
- package/lib/client/index.d.ts.map +1 -1
- package/lib/client/index.js +15 -9
- package/lib/client/index.js.map +1 -1
- package/lib/client/message-relay.d.ts +6 -4
- package/lib/client/message-relay.d.ts.map +1 -1
- package/lib/client/message-relay.js +4 -4
- package/lib/client/message-relay.js.map +1 -1
- package/lib/client/post-message-transport.d.ts +69 -3
- package/lib/client/post-message-transport.d.ts.map +1 -1
- package/lib/client/post-message-transport.js +175 -1
- package/lib/client/post-message-transport.js.map +1 -1
- package/lib/client/rpc-connection.d.ts +173 -0
- package/lib/client/rpc-connection.d.ts.map +1 -0
- package/lib/client/rpc-connection.js +225 -0
- package/lib/client/rpc-connection.js.map +1 -0
- package/lib/client/worker-handshake.d.ts +94 -0
- package/lib/client/worker-handshake.d.ts.map +1 -0
- package/lib/client/worker-handshake.js +51 -0
- package/lib/client/worker-handshake.js.map +1 -0
- package/lib/client-ids.d.ts +45 -0
- package/lib/client-ids.d.ts.map +1 -0
- package/lib/client-ids.js +48 -0
- package/lib/client-ids.js.map +1 -0
- package/lib/clock.d.ts +50 -10
- package/lib/clock.d.ts.map +1 -1
- package/lib/clock.js +36 -1
- package/lib/clock.js.map +1 -1
- package/lib/console-logger.d.ts +23 -0
- package/lib/console-logger.d.ts.map +1 -0
- package/lib/console-logger.js +39 -0
- package/lib/console-logger.js.map +1 -0
- package/lib/data/data-protocol-methods.d.ts +4 -4
- package/lib/data/data-protocol-methods.d.ts.map +1 -1
- package/lib/data/data-protocol-methods.js +13 -1
- package/lib/data/data-protocol-methods.js.map +1 -1
- package/lib/data/data-server-protocol.d.ts +171 -48
- package/lib/data/data-server-protocol.d.ts.map +1 -1
- package/lib/data/events.d.ts +144 -33
- package/lib/data/events.d.ts.map +1 -1
- package/lib/data/requests.d.ts +76 -11
- package/lib/data/requests.d.ts.map +1 -1
- package/lib/debouncer.d.ts +7 -4
- package/lib/debouncer.d.ts.map +1 -1
- package/lib/debouncer.js +10 -10
- package/lib/debouncer.js.map +1 -1
- package/lib/errors.d.ts +218 -41
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +364 -55
- package/lib/errors.js.map +1 -1
- package/lib/format-log-arg.d.ts +33 -0
- package/lib/format-log-arg.d.ts.map +1 -0
- package/lib/format-log-arg.js +158 -0
- package/lib/format-log-arg.js.map +1 -0
- package/lib/glsp-edits-settled-actions.d.ts +54 -0
- package/lib/glsp-edits-settled-actions.d.ts.map +1 -0
- package/lib/glsp-edits-settled-actions.js +28 -0
- package/lib/glsp-edits-settled-actions.js.map +1 -0
- package/lib/glsp-request-model-args.d.ts +16 -0
- package/lib/glsp-request-model-args.d.ts.map +1 -0
- package/lib/glsp-request-model-args.js +19 -0
- package/lib/glsp-request-model-args.js.map +1 -0
- package/lib/glsp-save-model-actions.d.ts +55 -0
- package/lib/glsp-save-model-actions.d.ts.map +1 -0
- package/lib/glsp-save-model-actions.js +28 -0
- package/lib/glsp-save-model-actions.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -1
- package/lib/latency-collector.d.ts +10 -4
- package/lib/latency-collector.d.ts.map +1 -1
- package/lib/latency-collector.js +2 -0
- package/lib/latency-collector.js.map +1 -1
- package/lib/logger.d.ts +38 -7
- package/lib/logger.d.ts.map +1 -1
- package/lib/logger.js +42 -4
- package/lib/logger.js.map +1 -1
- package/lib/messages/index.d.ts +4 -2
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +16 -4
- package/lib/messages/index.js.map +1 -1
- package/lib/messages/primitives.d.ts +59 -4
- package/lib/messages/primitives.d.ts.map +1 -1
- package/lib/messages/primitives.js +97 -4
- package/lib/messages/primitives.js.map +1 -1
- package/lib/model-server.d.ts +60 -13
- package/lib/model-server.d.ts.map +1 -1
- package/lib/model-server.js +4 -2
- package/lib/model-server.js.map +1 -1
- package/lib/model-service/base-version.d.ts +67 -0
- package/lib/model-service/base-version.d.ts.map +1 -0
- package/lib/model-service/base-version.js +44 -0
- package/lib/model-service/base-version.js.map +1 -0
- package/lib/model-service/index.d.ts +1 -1
- package/lib/model-service/index.d.ts.map +1 -1
- package/lib/model-service/index.js +4 -5
- package/lib/model-service/index.js.map +1 -1
- package/lib/model-service/reference-candidate.d.ts +5 -3
- package/lib/model-service/reference-candidate.d.ts.map +1 -1
- package/lib/node/index.d.ts +10 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +29 -0
- package/lib/node/index.js.map +1 -0
- package/lib/node/process-memory.d.ts +66 -0
- package/lib/node/process-memory.d.ts.map +1 -0
- package/lib/node/process-memory.js +291 -0
- package/lib/node/process-memory.js.map +1 -0
- package/lib/noop-logger.d.ts.map +1 -1
- package/lib/noop-logger.js.map +1 -1
- package/lib/observable-value.js.map +1 -1
- package/lib/patch-merge.d.ts +63 -35
- package/lib/patch-merge.d.ts.map +1 -1
- package/lib/patch-merge.js +171 -25
- package/lib/patch-merge.js.map +1 -1
- package/lib/pending-work-tracker.d.ts +30 -0
- package/lib/pending-work-tracker.d.ts.map +1 -0
- package/lib/pending-work-tracker.js +47 -0
- package/lib/pending-work-tracker.js.map +1 -0
- package/lib/profile-session.d.ts +24 -8
- package/lib/profile-session.d.ts.map +1 -1
- package/lib/profile-session.js +13 -4
- package/lib/profile-session.js.map +1 -1
- package/lib/profiling.d.ts +15 -2
- package/lib/profiling.d.ts.map +1 -1
- package/lib/profiling.js.map +1 -1
- package/lib/project.d.ts +4 -1
- package/lib/project.d.ts.map +1 -1
- package/lib/project.js.map +1 -1
- package/lib/random-uuid.d.ts +14 -0
- package/lib/random-uuid.d.ts.map +1 -0
- package/lib/random-uuid.js +24 -0
- package/lib/random-uuid.js.map +1 -0
- package/lib/reconcile-write.d.ts +70 -0
- package/lib/reconcile-write.d.ts.map +1 -0
- package/lib/reconcile-write.js +69 -0
- package/lib/reconcile-write.js.map +1 -0
- package/lib/rpc/bind-rpc-methods.d.ts +6 -2
- package/lib/rpc/bind-rpc-methods.d.ts.map +1 -1
- package/lib/rpc/bind-rpc-methods.js +17 -7
- package/lib/rpc/bind-rpc-methods.js.map +1 -1
- package/lib/rpc/create-rpc-proxy.d.ts +3 -0
- package/lib/rpc/create-rpc-proxy.d.ts.map +1 -1
- package/lib/rpc/create-rpc-proxy.js +6 -1
- package/lib/rpc/create-rpc-proxy.js.map +1 -1
- package/lib/rpc/index.d.ts +1 -0
- package/lib/rpc/index.d.ts.map +1 -1
- package/lib/rpc/index.js +1 -0
- package/lib/rpc/index.js.map +1 -1
- package/lib/rpc/send-by-method-name.d.ts +76 -0
- package/lib/rpc/send-by-method-name.d.ts.map +1 -0
- package/lib/rpc/send-by-method-name.js +120 -0
- package/lib/rpc/send-by-method-name.js.map +1 -0
- package/lib/rpc/wire-prefix.js.map +1 -1
- package/lib/save-text-document.d.ts +40 -0
- package/lib/save-text-document.d.ts.map +1 -0
- package/lib/save-text-document.js +34 -0
- package/lib/save-text-document.js.map +1 -0
- package/lib/testing/catalogue-audit.js.map +1 -1
- package/lib/testing/data-doubles.d.ts +41 -12
- package/lib/testing/data-doubles.d.ts.map +1 -1
- package/lib/testing/data-doubles.js +56 -8
- package/lib/testing/data-doubles.js.map +1 -1
- package/lib/testing/fake-clock.d.ts +17 -2
- package/lib/testing/fake-clock.d.ts.map +1 -1
- package/lib/testing/fake-clock.js +61 -46
- package/lib/testing/fake-clock.js.map +1 -1
- package/lib/testing/index.d.ts +1 -0
- package/lib/testing/index.d.ts.map +1 -1
- package/lib/testing/index.js +4 -3
- package/lib/testing/index.js.map +1 -1
- package/lib/testing/node/duplex-connection.d.ts.map +1 -1
- package/lib/testing/node/duplex-connection.js +3 -2
- package/lib/testing/node/duplex-connection.js.map +1 -1
- package/lib/testing/node/duplex-stream.js.map +1 -1
- package/lib/testing/node/index.d.ts +1 -0
- package/lib/testing/node/index.d.ts.map +1 -1
- package/lib/testing/node/index.js +2 -2
- package/lib/testing/node/index.js.map +1 -1
- package/lib/testing/node/message-port-pair.d.ts +25 -0
- package/lib/testing/node/message-port-pair.d.ts.map +1 -0
- package/lib/testing/node/message-port-pair.js +26 -0
- package/lib/testing/node/message-port-pair.js.map +1 -0
- package/lib/testing/transfer-node-builder.d.ts +81 -0
- package/lib/testing/transfer-node-builder.d.ts.map +1 -0
- package/lib/testing/transfer-node-builder.js +47 -0
- package/lib/testing/transfer-node-builder.js.map +1 -0
- package/lib/testing/wait-for.d.ts +9 -3
- package/lib/testing/wait-for.d.ts.map +1 -1
- package/lib/testing/wait-for.js +57 -11
- package/lib/testing/wait-for.js.map +1 -1
- package/lib/tracer.d.ts.map +1 -1
- package/lib/tracer.js +5 -9
- package/lib/tracer.js.map +1 -1
- package/lib/transfer-diagnostic.js.map +1 -1
- package/lib/transfer-document.d.ts +96 -32
- package/lib/transfer-document.d.ts.map +1 -1
- package/lib/transfer-document.js +17 -9
- package/lib/transfer-document.js.map +1 -1
- package/lib/transfer-element.d.ts +4 -4
- package/lib/types.d.ts +32 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/{model-service/args.js → types.js} +2 -2
- package/lib/types.js.map +1 -0
- package/lib/uri.d.ts.map +1 -1
- package/lib/uri.js.map +1 -1
- package/lib/util.d.ts +8 -2
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +32 -4
- package/lib/util.js.map +1 -1
- package/package.json +24 -40
- package/src/abstract-logger.ts +18 -0
- package/src/client/data-connection.ts +654 -0
- package/src/client/data-events.ts +38 -1
- package/src/client/data-port.ts +36 -25
- package/src/client/data-session.ts +1476 -137
- package/src/client/index.ts +15 -9
- package/src/client/message-relay.ts +6 -4
- package/src/client/post-message-transport.ts +224 -4
- package/src/client/rpc-connection.ts +302 -0
- package/src/client/worker-handshake.ts +121 -0
- package/src/client-ids.ts +49 -0
- package/src/clock.ts +68 -10
- package/src/console-logger.ts +39 -0
- package/src/data/data-protocol-methods.ts +14 -4
- package/src/data/data-server-protocol.ts +198 -48
- package/src/data/events.ts +150 -33
- package/src/data/requests.ts +82 -11
- package/src/debouncer.ts +11 -11
- package/src/errors.ts +453 -65
- package/src/format-log-arg.ts +176 -0
- package/src/glsp-edits-settled-actions.ts +62 -0
- package/src/glsp-request-model-args.ts +16 -0
- package/src/glsp-save-model-actions.ts +64 -0
- package/src/index.ts +11 -0
- package/src/latency-collector.ts +10 -3
- package/src/logger.ts +44 -8
- package/src/messages/index.ts +11 -2
- package/src/messages/primitives.ts +129 -5
- package/src/model-server.ts +64 -19
- package/src/model-service/base-version.ts +75 -0
- package/src/model-service/index.ts +4 -5
- package/src/model-service/reference-candidate.ts +5 -3
- package/src/node/index.ts +14 -0
- package/src/node/process-memory.ts +299 -0
- package/src/patch-merge.ts +230 -45
- package/src/pending-work-tracker.ts +47 -0
- package/src/profile-session.ts +31 -11
- package/src/profiling.ts +15 -2
- package/src/project.ts +4 -1
- package/src/random-uuid.ts +21 -0
- package/src/reconcile-write.ts +131 -0
- package/src/rpc/README.md +6 -6
- package/src/rpc/bind-rpc-methods.ts +18 -8
- package/src/rpc/create-rpc-proxy.ts +6 -1
- package/src/rpc/index.ts +1 -0
- package/src/rpc/send-by-method-name.ts +140 -0
- package/src/save-text-document.ts +39 -0
- package/src/testing/data-doubles.ts +141 -19
- package/src/testing/fake-clock.ts +76 -48
- package/src/testing/index.ts +4 -3
- package/src/testing/node/duplex-connection.ts +3 -2
- package/src/testing/node/index.ts +2 -2
- package/src/testing/node/message-port-pair.ts +40 -0
- package/src/testing/transfer-node-builder.ts +107 -0
- package/src/testing/wait-for.ts +61 -13
- package/src/tracer.ts +5 -9
- package/src/transfer-document.ts +116 -34
- package/src/transfer-element.ts +4 -4
- package/src/types.ts +30 -0
- package/src/util.ts +33 -6
- package/lib/model-service/args.d.ts +0 -64
- package/lib/model-service/args.d.ts.map +0 -1
- package/lib/model-service/args.js.map +0 -1
- 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';
|
package/src/latency-collector.ts
CHANGED
|
@@ -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
|
-
/**
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
46
|
+
/** A milestone the person running the server would recognise without knowing the internals. */
|
|
46
47
|
info(message?: string, ...args: unknown[]): this;
|
|
47
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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.
|
package/src/messages/index.ts
CHANGED
|
@@ -25,8 +25,17 @@
|
|
|
25
25
|
|
|
26
26
|
export * from './primitives';
|
|
27
27
|
|
|
28
|
-
export {
|
|
29
|
-
|
|
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
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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
|
|
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 :
|
|
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
|
/**
|