@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
|
@@ -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
|
+
import { type Disposable, type Event } from 'vscode-jsonrpc';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A value that may change over the connection lifetime. `value` is the
|
|
14
|
+
* current snapshot; `onChange` fires when the snapshot is replaced.
|
|
15
|
+
*
|
|
16
|
+
* The dynamic source in practice is always a user-configurable setting (see
|
|
17
|
+
* the `Settings` producers, which bind one to a Langium configuration
|
|
18
|
+
* section), but the type is source-agnostic — a constant wrapped via
|
|
19
|
+
* {@link ObservableValue.of} is still an `ObservableValue`; its `onChange`
|
|
20
|
+
* simply never fires. Recognised structurally by {@link isObservableValue}.
|
|
21
|
+
*/
|
|
22
|
+
export interface ObservableValue<T> {
|
|
23
|
+
/**
|
|
24
|
+
* `readonly` means the holder cannot assign it, NOT that it is stable —
|
|
25
|
+
* a live cell backs this with a getter, so it must be read at the point of
|
|
26
|
+
* use. Copying it into a field at construction pins the snapshot and
|
|
27
|
+
* silently defeats the whole type. A setting-bound cell answers its declared
|
|
28
|
+
* default until the client's configuration fetch resolves, so an early read
|
|
29
|
+
* is a legitimate value rather than an error.
|
|
30
|
+
*/
|
|
31
|
+
readonly value: T;
|
|
32
|
+
/**
|
|
33
|
+
* Fires when {@link value} is replaced, deduplicated by `Object.is`, so an
|
|
34
|
+
* unchanged push is not re-announced. Subscribing is OPTIONAL and usually
|
|
35
|
+
* unnecessary — reading `.value` per use already sees every change; the
|
|
36
|
+
* event is for consumers that must re-arm something a later read cannot fix
|
|
37
|
+
* (a timer already scheduled, a listener already registered).
|
|
38
|
+
*
|
|
39
|
+
* It never fires at all for a constant cell, so logic that lives only in an
|
|
40
|
+
* `onChange` handler does nothing when an adopter passes a plain value.
|
|
41
|
+
* Dispose what subscribing returns.
|
|
42
|
+
*/
|
|
43
|
+
readonly onChange: Event<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What a service option accepts: either a plain constant (one-shot, never
|
|
48
|
+
* changes) or an {@link ObservableValue}. Consumers normalise to a cell once
|
|
49
|
+
* via {@link ObservableValue.from} and read `.value` at the point of use.
|
|
50
|
+
*/
|
|
51
|
+
export type MaybeObservableValue<T> = T | ObservableValue<T>;
|
|
52
|
+
|
|
53
|
+
const NOOP_DISPOSABLE: Disposable = Object.freeze({ dispose: () => undefined });
|
|
54
|
+
|
|
55
|
+
/** An {@link Event} that never fires — the `onChange` of a constant cell. */
|
|
56
|
+
const NEVER_EVENT: Event<never> = () => NOOP_DISPOSABLE;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Type-guard discriminating a {@link MaybeObservableValue} into its {@link ObservableValue}
|
|
60
|
+
* arm. Returns `false` for primitives, `null`, and any non-object payload; for
|
|
61
|
+
* objects, requires both a `value` property and a callable `onChange` accessor
|
|
62
|
+
* (vscode-jsonrpc events are exposed as callable subscribers).
|
|
63
|
+
*/
|
|
64
|
+
export function isObservableValue<T>(input: MaybeObservableValue<T>): input is ObservableValue<T> {
|
|
65
|
+
if (input === null || typeof input !== 'object') {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const candidate = input as Partial<ObservableValue<T>>;
|
|
69
|
+
return 'value' in candidate && typeof candidate.onChange === 'function';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export namespace ObservableValue {
|
|
73
|
+
/**
|
|
74
|
+
* Wrap a constant as an {@link ObservableValue} whose `onChange` never
|
|
75
|
+
* fires. Adopters rarely need this — the constant arm of {@link MaybeObservableValue}
|
|
76
|
+
* already accepts a bare value; it exists for {@link from} and for tests
|
|
77
|
+
* that want an explicit cell.
|
|
78
|
+
*/
|
|
79
|
+
export function of<T>(value: T): ObservableValue<T> {
|
|
80
|
+
return { value, onChange: NEVER_EVENT as Event<T> };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalise a {@link MaybeObservableValue} to an {@link ObservableValue}. Identity for a
|
|
85
|
+
* cell (preserving its live `value` getter + `onChange`); wraps a constant
|
|
86
|
+
* via {@link of}. The combinator a consumer calls once in its constructor
|
|
87
|
+
* so the rest of the class reads `.value` uniformly.
|
|
88
|
+
*/
|
|
89
|
+
export function from<T>(input: MaybeObservableValue<T>): ObservableValue<T> {
|
|
90
|
+
return isObservableValue(input) ? input : of(input);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
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 { applyPatch, compare, deepClone, getValueByPointer, type Operation as JsonPatchOperation } from 'fast-json-patch';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Augment a user-intent JSON patch (computed `baseline → attempted`) with
|
|
14
|
+
* `test` ops so a strict `applyPatch` against freshly-fetched server state
|
|
15
|
+
* fails loudly when a foreign writer changed a path the user also changed.
|
|
16
|
+
*
|
|
17
|
+
* `fast-json-patch`'s `validateOperation: true` validates op structure and
|
|
18
|
+
* path resolvability but NOT the pre-existing value — a plain `replace` /
|
|
19
|
+
* `remove` silently overwrites whatever the foreign writer put there. For each
|
|
20
|
+
* `replace` / `remove` op we prepend a `test` op carrying the baseline value at
|
|
21
|
+
* that path, so a same-path divergence becomes a `TEST_OPERATION_FAILED` throw.
|
|
22
|
+
* The caller treats that as a real field-level conflict (drop + refetch) rather
|
|
23
|
+
* than silently clobbering the foreign edit.
|
|
24
|
+
*
|
|
25
|
+
* `add` ops are left unguarded: the path is new, so there is no baseline value
|
|
26
|
+
* to test against, and add-vs-add overlaps are out of scope for this floor.
|
|
27
|
+
*
|
|
28
|
+
* Used behind the framework's `ConflictError` contract by every reconcile
|
|
29
|
+
* path — the GLSP recording command's undo/redo and forward-write, via
|
|
30
|
+
* {@link ReconcilingConflictResolver}, and an adopter's form-widget save — so
|
|
31
|
+
* they share one collision-detection rule.
|
|
32
|
+
*/
|
|
33
|
+
export function augmentWithTestOps(baseline: object, userPatch: ReadonlyArray<JsonPatchOperation>): JsonPatchOperation[] {
|
|
34
|
+
const augmented: JsonPatchOperation[] = [];
|
|
35
|
+
for (const op of userPatch) {
|
|
36
|
+
if (op.op === 'replace' || op.op === 'remove') {
|
|
37
|
+
augmented.push({ op: 'test', path: op.path, value: getValueByPointer(baseline, op.path) });
|
|
38
|
+
}
|
|
39
|
+
augmented.push(op);
|
|
40
|
+
}
|
|
41
|
+
return augmented;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Outcome of {@link reconcileByPatchReplay}. The caller persists / re-baselines
|
|
46
|
+
* on `merged`, drops + surfaces the `fresh` root on `conflict`, and decides its
|
|
47
|
+
* own fallback (e.g. force-retry) on `no-op` / `unavailable`.
|
|
48
|
+
*/
|
|
49
|
+
export type ReconcileOutcome<T> =
|
|
50
|
+
| {
|
|
51
|
+
/** The replay succeeded: the foreign writer touched no path the user did. */
|
|
52
|
+
status: 'merged';
|
|
53
|
+
/**
|
|
54
|
+
* A fresh root built on the refetched server state, carrying both
|
|
55
|
+
* intents. It is NOT the caller's `attempted` root — re-baseline on
|
|
56
|
+
* this value, or the next write diffs against state the server never
|
|
57
|
+
* had.
|
|
58
|
+
*/
|
|
59
|
+
merged: T;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
/**
|
|
63
|
+
* The user's root already equalled the baseline, so the version gate
|
|
64
|
+
* fired on drift that changed nothing. No refetch was performed and
|
|
65
|
+
* there is nothing to persist — retrying the same write reproduces it.
|
|
66
|
+
*/
|
|
67
|
+
status: 'no-op';
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
/** A guarded `test` op tripped: user and foreign writer touched one path. */
|
|
71
|
+
status: 'conflict';
|
|
72
|
+
/**
|
|
73
|
+
* The server's current root, refetched and unmodified — the user's
|
|
74
|
+
* intent was NOT applied to it. Surface it and drop the write; treating
|
|
75
|
+
* it as a merge result silently discards what the user typed.
|
|
76
|
+
*/
|
|
77
|
+
fresh: T;
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
/**
|
|
81
|
+
* The refetch produced nothing, so reconciliation could not be
|
|
82
|
+
* attempted at all. This says nothing about whether a conflict exists;
|
|
83
|
+
* the caller picks its own fallback (force-write, retry, surface).
|
|
84
|
+
*/
|
|
85
|
+
status: 'unavailable';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Shared three-way reconcile for a `ConflictError`: diff the user's intent
|
|
90
|
+
* (`baseline → attempted`), refetch the server's current root, and replay the
|
|
91
|
+
* intent on top under strict, {@link augmentWithTestOps}-guarded `applyPatch`.
|
|
92
|
+
*
|
|
93
|
+
* - `no-op` — the user's root equals the baseline, so the gate fired on a
|
|
94
|
+
* benign version drift; nothing to replay (refetch is skipped).
|
|
95
|
+
* - `unavailable` — the refetch produced nothing; caller falls back.
|
|
96
|
+
* - `merged` — the foreign writer touched only paths the user did not; the
|
|
97
|
+
* merged root carries both intents.
|
|
98
|
+
* - `conflict` — a same-path divergence tripped a `test` op; caller drops the
|
|
99
|
+
* write and surfaces `fresh`.
|
|
100
|
+
*
|
|
101
|
+
* I/O is the caller's: `refetch` supplies the current root, and applying the
|
|
102
|
+
* `merged` result (update vs save, re-baseline, UI refresh) stays at the call
|
|
103
|
+
* site so form and GLSP paths keep their own persistence semantics.
|
|
104
|
+
*/
|
|
105
|
+
export async function reconcileByPatchReplay<T extends object>(
|
|
106
|
+
baseline: T,
|
|
107
|
+
attempted: T,
|
|
108
|
+
refetch: () => Promise<T | undefined>
|
|
109
|
+
): Promise<ReconcileOutcome<T>> {
|
|
110
|
+
const userPatch = compare(baseline, attempted);
|
|
111
|
+
if (userPatch.length === 0) {
|
|
112
|
+
return { status: 'no-op' };
|
|
113
|
+
}
|
|
114
|
+
const fresh = await refetch();
|
|
115
|
+
if (!fresh) {
|
|
116
|
+
return { status: 'unavailable' };
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const merged = applyPatch(deepClone(fresh), augmentWithTestOps(baseline, userPatch), true).newDocument;
|
|
120
|
+
return { status: 'merged', merged };
|
|
121
|
+
} catch {
|
|
122
|
+
return { status: 'conflict', fresh };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Strategy for resolving a write that raced a foreign edit. One declarative
|
|
128
|
+
* choice covers every conflict site — `execute` forward-write `(before,
|
|
129
|
+
* after)`, `undo` `(after, before)`, `redo` `(before, after)`, and form / GLSP
|
|
130
|
+
* save `(lastSynced, newRoot)` — so adopters pick reconcile-vs-force once
|
|
131
|
+
* (by binding a resolver) rather than per call site.
|
|
132
|
+
*/
|
|
133
|
+
export interface ConflictResolver {
|
|
134
|
+
/**
|
|
135
|
+
* Reconcile the user's `baseline → attempted` intent against the current
|
|
136
|
+
* server state (`refetch`), returning a {@link ReconcileOutcome} the caller
|
|
137
|
+
* acts on (persist `merged`, drop on `conflict`, fall back on `no-op` /
|
|
138
|
+
* `unavailable`).
|
|
139
|
+
*/
|
|
140
|
+
resolve<T extends object>(baseline: T, attempted: T, refetch: () => Promise<T | undefined>): Promise<ReconcileOutcome<T>>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Default {@link ConflictResolver}: field-level three-way merge via
|
|
145
|
+
* {@link reconcileByPatchReplay}. A foreign edit to a different field is
|
|
146
|
+
* merged; a same-field collision is reported as a conflict rather than
|
|
147
|
+
* clobbered.
|
|
148
|
+
*/
|
|
149
|
+
export class ReconcilingConflictResolver implements ConflictResolver {
|
|
150
|
+
resolve<T extends object>(baseline: T, attempted: T, refetch: () => Promise<T | undefined>): Promise<ReconcileOutcome<T>> {
|
|
151
|
+
return reconcileByPatchReplay(baseline, attempted, refetch);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Last-writer-wins {@link ConflictResolver}: always reports the `attempted`
|
|
157
|
+
* state as merged, without refetching or guarding. Suitable for single-client
|
|
158
|
+
* tools or always-regenerated artifacts where a concurrent foreign edit may be
|
|
159
|
+
* overwritten. A foreign edit to any field is clobbered.
|
|
160
|
+
*/
|
|
161
|
+
export class ForceConflictResolver implements ConflictResolver {
|
|
162
|
+
async resolve<T extends object>(_baseline: T, attempted: T, _refetch: () => Promise<T | undefined>): Promise<ReconcileOutcome<T>> {
|
|
163
|
+
return { status: 'merged', merged: attempted };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
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, type Stopwatch } from './clock';
|
|
11
|
+
import { Format, Logger, type LogThreshold } from './logger';
|
|
12
|
+
|
|
13
|
+
/** One aggregated row of a {@link ProfileSession}: a scope id and its totals. */
|
|
14
|
+
export interface ProfileRecord {
|
|
15
|
+
/** The scope id passed to {@link ProfileSession.scope}. */
|
|
16
|
+
id: string;
|
|
17
|
+
/** How many times a scope with this id ran. */
|
|
18
|
+
count: number;
|
|
19
|
+
/** Summed self-time across those runs (wall-clock minus nested scopes). */
|
|
20
|
+
selfMs: number;
|
|
21
|
+
/** Self-time as a percentage of the session's total wall-clock. */
|
|
22
|
+
selfPct: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An aggregating, parent-exclusive self-time profiler scoped to one operation.
|
|
27
|
+
*
|
|
28
|
+
* Unlike `Tracer.time`, which emits one log line per
|
|
29
|
+
* call and keeps the timeline, a {@link ProfileSession} collapses many
|
|
30
|
+
* {@link scope} calls into a compact per-id aggregate (count + self-time +
|
|
31
|
+
* self-%), then dumps it as line-based output through a {@link Logger}. It is
|
|
32
|
+
* the in-house analogue of Langium's `ProfilingTask` (per-rule / per-node-type
|
|
33
|
+
* self-time) but for the framework's own passes — integrity rules, GModel
|
|
34
|
+
* construction, the save/reconcile chain — that Langium's profiler does not
|
|
35
|
+
* cover.
|
|
36
|
+
*
|
|
37
|
+
* Spawned per operation via `tracer.profile(id)`, used, reported, discarded —
|
|
38
|
+
* it is stateful and op-scoped, which is why it is not a method on the
|
|
39
|
+
* long-lived {@link Logger}/`Tracer`.
|
|
40
|
+
*/
|
|
41
|
+
export interface ProfileSession {
|
|
42
|
+
/**
|
|
43
|
+
* Run `fn` as a measured task `id`, accumulating its self-time = wall-clock
|
|
44
|
+
* minus any nested {@link scope} calls. Re-entrant: an inner scope nests and
|
|
45
|
+
* its full duration is excluded from the enclosing id's self-time
|
|
46
|
+
* (parent-exclusive). Async `fn` is awaited so timing covers the full
|
|
47
|
+
* settle. Returns `fn`'s result unchanged; rethrows after recording the
|
|
48
|
+
* elapsed time of a throwing scope.
|
|
49
|
+
*/
|
|
50
|
+
scope<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
51
|
+
scope<T>(id: string, fn: () => T): T;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Emit the aggregate through the bound {@link Logger}: one line per id,
|
|
55
|
+
* sorted by self-time descending, plus an "unaccounted" remainder (the
|
|
56
|
+
* session wall-clock not attributed to any top-level scope). No-op when
|
|
57
|
+
* `logLevel` is suppressed by the global threshold (incl. `'off'`).
|
|
58
|
+
*/
|
|
59
|
+
report(logLevel?: LogThreshold): void;
|
|
60
|
+
|
|
61
|
+
/** The same aggregate as structured records, for tests / programmatic use. */
|
|
62
|
+
records(): readonly ProfileRecord[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ScopeFrame {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
/** Session-stopwatch reading when this scope began. */
|
|
68
|
+
readonly start: number;
|
|
69
|
+
/** Wall-time consumed by nested scopes, excluded from this scope's self-time. */
|
|
70
|
+
childMs: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Default {@link ProfileSession}. Times against an injected {@link Clock} (so
|
|
75
|
+
* it is fake-clock deterministic in tests, unlike a raw `performance.now`) and
|
|
76
|
+
* dumps through an injected {@link Logger}.
|
|
77
|
+
*/
|
|
78
|
+
export class DefaultProfileSession implements ProfileSession {
|
|
79
|
+
/** Single monotonic timeline for the whole session; per-scope readings are deltas off it. */
|
|
80
|
+
protected readonly sessionStopwatch: Stopwatch;
|
|
81
|
+
protected readonly stack: ScopeFrame[] = [];
|
|
82
|
+
protected readonly entries = new Map<string, number[]>();
|
|
83
|
+
|
|
84
|
+
constructor(
|
|
85
|
+
protected readonly logger: Logger,
|
|
86
|
+
clock: Clock,
|
|
87
|
+
protected readonly identifier: string
|
|
88
|
+
) {
|
|
89
|
+
this.sessionStopwatch = clock.stopwatch();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
scope<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
93
|
+
scope<T>(id: string, fn: () => T): T;
|
|
94
|
+
scope<T>(id: string, fn: () => T | Promise<T>): T | Promise<T> {
|
|
95
|
+
const frame: ScopeFrame = { id, start: this.sessionStopwatch.elapsedMs, childMs: 0 };
|
|
96
|
+
this.stack.push(frame);
|
|
97
|
+
let result: T | Promise<T>;
|
|
98
|
+
try {
|
|
99
|
+
result = fn();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.closeFrame(frame);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (result instanceof Promise) {
|
|
105
|
+
return result.then(
|
|
106
|
+
value => {
|
|
107
|
+
this.closeFrame(frame);
|
|
108
|
+
return value;
|
|
109
|
+
},
|
|
110
|
+
error => {
|
|
111
|
+
this.closeFrame(frame);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
this.closeFrame(frame);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Pop `frame`, charge its full duration to the parent, and record its self-time. */
|
|
121
|
+
protected closeFrame(frame: ScopeFrame): void {
|
|
122
|
+
this.stack.pop();
|
|
123
|
+
const duration = this.sessionStopwatch.elapsedMs - frame.start;
|
|
124
|
+
const parent = this.stack[this.stack.length - 1];
|
|
125
|
+
if (parent) {
|
|
126
|
+
parent.childMs += duration;
|
|
127
|
+
}
|
|
128
|
+
const selfMs = duration - frame.childMs;
|
|
129
|
+
const samples = this.entries.get(frame.id);
|
|
130
|
+
if (samples) {
|
|
131
|
+
samples.push(selfMs);
|
|
132
|
+
} else {
|
|
133
|
+
this.entries.set(frame.id, [selfMs]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
records(): readonly ProfileRecord[] {
|
|
138
|
+
const totalMs = this.sessionStopwatch.elapsedMs;
|
|
139
|
+
const records: ProfileRecord[] = [];
|
|
140
|
+
for (const [id, samples] of this.entries) {
|
|
141
|
+
const selfMs = samples.reduce((sum, value) => sum + value, 0);
|
|
142
|
+
records.push({ id, count: samples.length, selfMs, selfPct: totalMs > 0 ? (100 * selfMs) / totalMs : 0 });
|
|
143
|
+
}
|
|
144
|
+
return records.sort((left, right) => right.selfMs - left.selfMs);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
report(logLevel: LogThreshold = 'debug'): void {
|
|
148
|
+
if (logLevel === 'off' || !Logger.isLevelEnabled(logLevel)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const totalMs = this.sessionStopwatch.elapsedMs;
|
|
152
|
+
const records = this.records();
|
|
153
|
+
const emit = this.logger[logLevel].bind(this.logger);
|
|
154
|
+
for (const record of records) {
|
|
155
|
+
emit(`[profile ${this.identifier}] ${record.id} ×${record.count} ${Math.round(record.selfPct)}% ${Format.elapsed(record.selfMs)}`);
|
|
156
|
+
}
|
|
157
|
+
const accountedMs = records.reduce((sum, record) => sum + record.selfMs, 0);
|
|
158
|
+
const unaccountedMs = totalMs - accountedMs;
|
|
159
|
+
if (unaccountedMs >= 0.5) {
|
|
160
|
+
const pct = totalMs > 0 ? Math.round((100 * unaccountedMs) / totalMs) : 0;
|
|
161
|
+
emit(`[profile ${this.identifier}] (unaccounted) ${pct}% ${Format.elapsed(unaccountedMs)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/profiling.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
* Neutral shape of a sampled-profile capture request. It lives in `protocol`
|
|
12
|
+
* (not the Node-only `@hydranium/core/node` capture implementation) because it
|
|
13
|
+
* crosses the wire — the data-server diagnostics `startProfiling` args extend it,
|
|
14
|
+
* and a frontend/CLI names the dimensions to capture. The implementation
|
|
15
|
+
* (`ProfileCapture`, the `node:inspector` session) stays in `@hydranium/core/node`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Which dimensions to capture, and the two sampling knobs. All off by default. */
|
|
19
|
+
export interface ProfileCaptureOptions {
|
|
20
|
+
cpu?: boolean;
|
|
21
|
+
allocation?: boolean;
|
|
22
|
+
gc?: boolean;
|
|
23
|
+
eventLoopDelay?: boolean;
|
|
24
|
+
/** Also drop a retained `.heapsnapshot` at stop (for the memory analyzer). */
|
|
25
|
+
heapSnapshot?: boolean;
|
|
26
|
+
/** CPU sampling interval in µs (implementation default 1000). */
|
|
27
|
+
cpuIntervalMicros?: number;
|
|
28
|
+
/** Allocation sampling interval in bytes (implementation default 32768). */
|
|
29
|
+
allocationIntervalBytes?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* The manifest schema — the self-describing index over one profiling run's
|
|
34
|
+
* artefacts. It lives in `protocol` (not the Node-only `ProfilingRun` that writes
|
|
35
|
+
* it) so both the server-side assembler AND a Playwright-side assembler can share
|
|
36
|
+
* the shape without a `node`-only dependency chain. `ProfilingRun` re-exports
|
|
37
|
+
* these from `@hydranium/core/node`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export const PROFILING_SCHEMA_VERSION = 1;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The locked, origin-first artefact kinds (the kind equals the artefact's filename stem).
|
|
44
|
+
* A closed union so a producer cannot emit a stale or typo'd kind and still
|
|
45
|
+
* compile — the drift the naming scheme forbids.
|
|
46
|
+
*/
|
|
47
|
+
export type ProfilingArtifactKind =
|
|
48
|
+
| 'server-cpu'
|
|
49
|
+
| 'server-alloc'
|
|
50
|
+
| 'server-heap'
|
|
51
|
+
| 'server-summary'
|
|
52
|
+
| 'server-latency'
|
|
53
|
+
| 'server-ast'
|
|
54
|
+
| 'server-memory-report'
|
|
55
|
+
| 'server-log'
|
|
56
|
+
| 'browser-runtime'
|
|
57
|
+
| 'browser-timeline'
|
|
58
|
+
| 'browser-heap'
|
|
59
|
+
| 'browser-console';
|
|
60
|
+
|
|
61
|
+
/** One entry in the manifest — a captured file plus enough context for a skill to know what it is. */
|
|
62
|
+
export interface ProfilingArtifact {
|
|
63
|
+
/** The artefact's origin-first kind; equals its filename stem. */
|
|
64
|
+
kind: ProfilingArtifactKind;
|
|
65
|
+
/** Path relative to the manifest (the artefact lives beside it in the session directory). */
|
|
66
|
+
path: string;
|
|
67
|
+
/** The window the artefact was captured in, when it is window-scoped. */
|
|
68
|
+
window?: string;
|
|
69
|
+
/** Pointer to a compact digest file for a heavy artefact (added by the digest emitters). */
|
|
70
|
+
digest?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** What a skill reads to know what to expect from a run (browser entries only in `app` mode, etc.). */
|
|
74
|
+
export interface ProfilingEnvironment {
|
|
75
|
+
mode: 'harness' | 'app';
|
|
76
|
+
/** Whether a cgroup memory controller is present (running in a pod) vs local. */
|
|
77
|
+
container: boolean;
|
|
78
|
+
commit?: string;
|
|
79
|
+
workspace?: string;
|
|
80
|
+
node: string;
|
|
81
|
+
windows: { label: string; ms: number }[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The `manifest.json` contents — the index over one run's artefacts. */
|
|
85
|
+
export interface ProfilingManifest {
|
|
86
|
+
/**
|
|
87
|
+
* Always {@link PROFILING_SCHEMA_VERSION} as written; declared as `number`
|
|
88
|
+
* because a READER may be newer than the run it is opening. Check it before
|
|
89
|
+
* trusting any field — an older manifest can be missing fields this
|
|
90
|
+
* declaration says are required.
|
|
91
|
+
*/
|
|
92
|
+
schemaVersion: number;
|
|
93
|
+
/**
|
|
94
|
+
* Names the session directory the manifest sits in, so it identifies a run
|
|
95
|
+
* without carrying an absolute path. Unique per run by construction (a
|
|
96
|
+
* timestamp when the caller supplies none), but nothing enforces that a
|
|
97
|
+
* caller-supplied id is not reused.
|
|
98
|
+
*/
|
|
99
|
+
sessionId: string;
|
|
100
|
+
/**
|
|
101
|
+
* What the run was captured against — needed to know which artefacts to
|
|
102
|
+
* expect at all, since the browser kinds only occur in `app` mode, and to
|
|
103
|
+
* know whether two runs are comparable.
|
|
104
|
+
*/
|
|
105
|
+
environment: ProfilingEnvironment;
|
|
106
|
+
/**
|
|
107
|
+
* Every file written beside the manifest. Presence is the only signal that a
|
|
108
|
+
* dimension was captured: an artefact for a dimension that was not requested
|
|
109
|
+
* is absent rather than empty, and a kind may occur more than once when the
|
|
110
|
+
* run has several windows.
|
|
111
|
+
*/
|
|
112
|
+
artifacts: ProfilingArtifact[];
|
|
113
|
+
}
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
* Cross-cutting domain type for the project tier — a descriptor-discovered,
|
|
12
|
+
* optionally versioned and dependency-aware sub-grouping of model files
|
|
13
|
+
* inside an LSP workspace folder.
|
|
14
|
+
*
|
|
15
|
+
* Owned by `@hydranium/protocol` (rather than `@hydranium/core`) so
|
|
16
|
+
* protocol consumers — including browser-bundle frontend code that
|
|
17
|
+
* imports the typed data-server proxy — pull this type without
|
|
18
|
+
* transitively depending on Langium. The `Project` shape is the structural
|
|
19
|
+
* baseline both the framework's runtime project tier (`ProjectManager` in
|
|
20
|
+
* `@hydranium/core`) and its wire-level surface (`DataServerProtocol`
|
|
21
|
+
* methods returning projects) agree on; collapsing them into one
|
|
22
|
+
* type eliminates a duplicate definition that would otherwise drift.
|
|
23
|
+
*
|
|
24
|
+
* **Project tier rationale.** Sits at a tier Langium itself deliberately
|
|
25
|
+
* does not provide. Langium's `WorkspaceManager` indexes all files in all
|
|
26
|
+
* workspace folders into one flat global scope; the maintainers have
|
|
27
|
+
* on-record stated they intend to keep it that way — see
|
|
28
|
+
* [Discussion #1375 — *Multiple Languages from different Projects*](https://github.com/eclipse-langium/langium/discussions/1375).
|
|
29
|
+
* This framework fills that gap so consumers don't have to reinvent it.
|
|
30
|
+
* Consumers that don't need projects bind `SingleProjectManager` and pay
|
|
31
|
+
* no cost — one synthetic project at runtime, no descriptor tracking,
|
|
32
|
+
* no events.
|
|
33
|
+
*
|
|
34
|
+
* **Extension model.** Adopters with richer per-project metadata declare a
|
|
35
|
+
* subtype `interface MyProject extends Project { ... }` and parameterise
|
|
36
|
+
* `ProjectManager<MyProject>` / `DataServer<TRoot, TDiagnostic, MyProject>`.
|
|
37
|
+
* The framework's structural read from a project is `id` (registry key,
|
|
38
|
+
* dependency-graph identity) plus optionally `dependencies` (visibility-
|
|
39
|
+
* closure walk); every other field is either wire metadata (`version`) or
|
|
40
|
+
* adopter-specific extension carried transparently through the typed
|
|
41
|
+
* surface. The framework imposes no format on extension fields — they
|
|
42
|
+
* ride on the JSON-RPC envelope as additional properties when crossing
|
|
43
|
+
* the wire.
|
|
44
|
+
*/
|
|
45
|
+
export interface Project {
|
|
46
|
+
/**
|
|
47
|
+
* Stable, unique identifier. Used as the lookup key in
|
|
48
|
+
* `ProjectManager.getProjectById` and as the value in
|
|
49
|
+
* {@link dependencies} entries that point at this project.
|
|
50
|
+
* Conventionally a name-plus-version string (e.g.
|
|
51
|
+
* `"example-dwh@1.0.0"`), but the framework imposes no format — any
|
|
52
|
+
* non-empty string works.
|
|
53
|
+
*
|
|
54
|
+
* The framework's internal identifier — map keys, visibility-set
|
|
55
|
+
* membership, dependency-declaration matching. Distinct from the
|
|
56
|
+
* user-facing {@link referenceName}; see there for why they are separate.
|
|
57
|
+
*/
|
|
58
|
+
readonly id: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* User-facing prefix used in qualified-name references that cross
|
|
62
|
+
* project boundaries. Composed by `NameProvider.getProjectQualifiedName`
|
|
63
|
+
* to produce the workspace-unique form that the linker resolves
|
|
64
|
+
* cross-project references against; emitted as the name of the
|
|
65
|
+
* `TieredAstNodeDescription` produced by
|
|
66
|
+
* `HydraniumScopeComputation.addExportedSymbol` when the project-
|
|
67
|
+
* qualified form differs from the document-qualified form.
|
|
68
|
+
*
|
|
69
|
+
* Set to {@link UNQUALIFIED_PROJECT_REFERENCE} (`''`) to contribute no
|
|
70
|
+
* prefix at all — see that constant for the semantics.
|
|
71
|
+
*
|
|
72
|
+
* **Why this is a separate field from {@link id}.** Two reasons:
|
|
73
|
+
* 1. Versioned ids (e.g. `"example-dwh@1.0.0"`) need un-versioned
|
|
74
|
+
* names in reference syntax (`example-dwh.User.email`) — the
|
|
75
|
+
* version pinning only matters for dependency declarations.
|
|
76
|
+
* 2. Names with grammar-illegal characters (dots, spaces) need
|
|
77
|
+
* sanitisation. The framework cannot safely sanitise {@link id}
|
|
78
|
+
* (it would break dependency matching); a separate field lets
|
|
79
|
+
* adopters sanitise once at parse time.
|
|
80
|
+
*/
|
|
81
|
+
readonly referenceName: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Optional semantic version of the project. Distinct from {@link id}
|
|
85
|
+
* because two versions of the same logical project are different
|
|
86
|
+
* projects (different {@link id}s) — `version` is metadata for tooling /
|
|
87
|
+
* display / version-matched dependency declarations, not part of
|
|
88
|
+
* identity. The framework does not read this field internally; it
|
|
89
|
+
* passes through to clients as-is.
|
|
90
|
+
*/
|
|
91
|
+
readonly version?: string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Ids of other projects this project explicitly depends on. Concrete
|
|
95
|
+
* `ProjectManager` implementations decide what "depends on" means
|
|
96
|
+
* and combine these into the visibility closure via
|
|
97
|
+
* `ProjectManager.getVisibleProjects` — the framework default in
|
|
98
|
+
* `AbstractProjectManager` walks them transitively; adopters can
|
|
99
|
+
* override.
|
|
100
|
+
*/
|
|
101
|
+
readonly dependencies?: readonly string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sentinel value for {@link Project.referenceName} indicating the
|
|
106
|
+
* project does not prefix its names with a reference segment. Names
|
|
107
|
+
* from such projects contribute to the workspace-wide namespace as-is —
|
|
108
|
+
* `NameProvider.getProjectQualifiedName` collapses to
|
|
109
|
+
* `getDocumentQualifiedName`, and `HydraniumScopeComputation` emits a
|
|
110
|
+
* single description per node (no separate `tier: 'public'` entry,
|
|
111
|
+
* since it would carry the same name).
|
|
112
|
+
*
|
|
113
|
+
* Typical use:
|
|
114
|
+
* - `SingleProjectManager` workspaces (no inter-project disambiguation
|
|
115
|
+
* needed — there is only one project, the synthetic workspace project).
|
|
116
|
+
* - Adopters whose grammar has no qualified-name reference syntax (e.g.
|
|
117
|
+
* class references written as bare `[Class:ID]` tokens at the framework
|
|
118
|
+
* boundary; per-package projects all bind this sentinel).
|
|
119
|
+
*
|
|
120
|
+
* The empty string is chosen so that `referenceName + nameSeparator + name`
|
|
121
|
+
* compositions detect the unqualified case without an explicit null
|
|
122
|
+
* check on the consumer side — but for predicate readability prefer
|
|
123
|
+
* `ProjectManager.isUnqualifiedProjectReference(uri)`.
|
|
124
|
+
*/
|
|
125
|
+
export const UNQUALIFIED_PROJECT_REFERENCE = '';
|