@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,48 @@
|
|
|
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
|
+
* Validates that a wire-name prefix — the `methodNamespace` option shared
|
|
12
|
+
* by `bindRpcMethods` and
|
|
13
|
+
* `createRpcProxy` — carries a
|
|
14
|
+
* trailing `/`.
|
|
15
|
+
*
|
|
16
|
+
* **Why this exists.** Adopters reading the LSP-namespace analogy
|
|
17
|
+
* (`textDocument/*`, `workspace/*`) naturally pass `'mylang'` expecting
|
|
18
|
+
* `'mylang/getModelDocument'` on the wire. Concatenating verbatim instead
|
|
19
|
+
* yields `'mylanggetModelDocument'`, and because the proxy and binding sites
|
|
20
|
+
* pick the wrong prefix in lockstep the mismatch is silent on a single-process
|
|
21
|
+
* adopter, audible only as a per-method "Unhandled method" on the wire log.
|
|
22
|
+
*
|
|
23
|
+
* Throwing is preferred over coercion: coercion would disguise the case where
|
|
24
|
+
* the adopter intended a different separator (`'.'`, `':'`), and throwing at
|
|
25
|
+
* construction time surfaces the bug at the adopter's own callsite rather than
|
|
26
|
+
* at the first wire response.
|
|
27
|
+
*
|
|
28
|
+
* Accepts the empty string (explicit no-prefix mode) or any non-empty string
|
|
29
|
+
* ending with `/`; anything else throws {@link TypeError} synchronously.
|
|
30
|
+
*
|
|
31
|
+
* @param methodNamespace The configured prefix (e.g. `'mylang/'`,
|
|
32
|
+
* `'data-server/'`, `''`).
|
|
33
|
+
* @param caller Name of the validating function, embedded in the thrown
|
|
34
|
+
* message so the error reads as if it came from the adopter-facing
|
|
35
|
+
* function rather than this helper.
|
|
36
|
+
*/
|
|
37
|
+
export function assertValidMethodNamespace(methodNamespace: string, caller: string): void {
|
|
38
|
+
if (methodNamespace === '' || methodNamespace.endsWith('/')) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
`${caller}: methodNamespace '${methodNamespace}' must end with '/' (or be empty). ` +
|
|
43
|
+
"Adopters reading the LSP-namespace analogy ('textDocument/*', 'workspace/*') often " +
|
|
44
|
+
`forget the trailing slash, producing wire names like '${methodNamespace}someMethod' ` +
|
|
45
|
+
`that do not match handler bindings under '${methodNamespace}/someMethod'. ` +
|
|
46
|
+
`Either pass '${methodNamespace}/' or use the empty string for no prefix.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
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 two CLIENT-side doubles of the data head: a {@link DataPort} whose
|
|
12
|
+
* transport the test supplies, and a {@link DataClientProtocol} that records
|
|
13
|
+
* what arrived.
|
|
14
|
+
*
|
|
15
|
+
* # Why standalone, when a harness already builds one
|
|
16
|
+
*
|
|
17
|
+
* `makeDataServerHarness` builds a capturing client too, but fused into a
|
|
18
|
+
* server-bearing harness: taking it means taking a duplex pair, a `DataServer`
|
|
19
|
+
* and a proxy. That is the right shape for testing the server. It is the wrong
|
|
20
|
+
* shape — and not available at all — for testing the CLIENT half: a
|
|
21
|
+
* `DataSession`, a widget model, a host's port adapter. Those need to stand a
|
|
22
|
+
* double on one side of the boundary and something else entirely on the other,
|
|
23
|
+
* which is why both halves get hand-rolled per suite instead.
|
|
24
|
+
*
|
|
25
|
+
* # Runner-agnostic, deliberately
|
|
26
|
+
*
|
|
27
|
+
* No `jest.fn`, no `vi.fn`, no `expect`. The recorded arrays ARE the assertion
|
|
28
|
+
* surface, so the same double works under vitest, jest and a bare script. A
|
|
29
|
+
* mock-framework double would also make "was it called" the observable, when
|
|
30
|
+
* what a data-head test actually asserts is the CONTENT of what arrived —
|
|
31
|
+
* which `sourceClientId`, which version, which reason.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { Emitter, type MessageConnection } from 'vscode-jsonrpc';
|
|
35
|
+
import type { DataPort } from '../client/data-port';
|
|
36
|
+
import type { DataClientProtocol } from '../data/data-server-protocol';
|
|
37
|
+
import type { ProjectsChangedEvent, TransferDocumentSavedEvent, TransferDocumentUpdatedEvent } from '../data/events';
|
|
38
|
+
import type { Project } from '../project';
|
|
39
|
+
import type { TransferDiagnostic } from '../transfer-diagnostic';
|
|
40
|
+
import type { TransferElement } from '../transfer-element';
|
|
41
|
+
|
|
42
|
+
/** Inputs to {@link makeFakeDataPort}. */
|
|
43
|
+
export interface FakeDataPortOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Establish one transport generation and hand back a LISTENING connection —
|
|
46
|
+
* the port's whole contract. Called once per generation, so a test that
|
|
47
|
+
* asserts on reconnection must return a FRESH connection each time rather
|
|
48
|
+
* than closing over one.
|
|
49
|
+
*
|
|
50
|
+
* Throwing (or rejecting) here is the transport-construction failure path,
|
|
51
|
+
* which the consumer surfaces through {@link FakeDataPort.reported}.
|
|
52
|
+
*/
|
|
53
|
+
connect(): MessageConnection | Promise<MessageConnection>;
|
|
54
|
+
/**
|
|
55
|
+
* Stable client identity. Defaults to `'fake-data-port'`, which avoids the
|
|
56
|
+
* three sentinels the framework reserves (`'language-client'`, `'unknown'`,
|
|
57
|
+
* `'revert-on-close'`); override it when a test needs two distinguishable
|
|
58
|
+
* clients on one server.
|
|
59
|
+
*/
|
|
60
|
+
clientId?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A {@link DataPort} that records what passed through it. */
|
|
64
|
+
export interface FakeDataPort extends DataPort {
|
|
65
|
+
/**
|
|
66
|
+
* Every connection {@link FakeDataPortOptions.connect} handed back, in
|
|
67
|
+
* order. Read from outside: "one connection shared across concurrent
|
|
68
|
+
* callers" and "a fresh connection after a teardown" are assertions about
|
|
69
|
+
* this length and about nothing else observable — a session that memoised a
|
|
70
|
+
* rejected promise, for instance, differs from one that retries ONLY here.
|
|
71
|
+
*/
|
|
72
|
+
readonly connections: readonly MessageConnection[];
|
|
73
|
+
/**
|
|
74
|
+
* Every {@link DataPort.reportError} call, in order. Read from outside: this
|
|
75
|
+
* is the only place a transport failure surfaces, so a test for the failure
|
|
76
|
+
* path asserts on the `context` string here rather than on a rejection that
|
|
77
|
+
* the consumer may legitimately swallow.
|
|
78
|
+
*/
|
|
79
|
+
readonly reported: readonly { readonly error: unknown; readonly context: string }[];
|
|
80
|
+
/**
|
|
81
|
+
* Fire {@link DataPort.onDispose} — the host tearing the transport down, a
|
|
82
|
+
* language-server restart being the case that forces the event to exist.
|
|
83
|
+
* Drives the consumer's reconnection path, which has no other trigger.
|
|
84
|
+
*/
|
|
85
|
+
fireDispose(): void;
|
|
86
|
+
/** Release the emitter. Idempotent; does not close the connections, which the test owns. */
|
|
87
|
+
dispose(): void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A {@link DataPort} over a transport the caller supplies.
|
|
92
|
+
*
|
|
93
|
+
* It deliberately does not create the transport itself. What varies between the
|
|
94
|
+
* cases worth testing is exactly that: an in-process duplex pair, a socket, a
|
|
95
|
+
* connection with no server behind it, a different one per generation.
|
|
96
|
+
*/
|
|
97
|
+
export function makeFakeDataPort(options: FakeDataPortOptions): FakeDataPort {
|
|
98
|
+
const connections: MessageConnection[] = [];
|
|
99
|
+
const reported: { error: unknown; context: string }[] = [];
|
|
100
|
+
const disposeEmitter = new Emitter<void>();
|
|
101
|
+
return {
|
|
102
|
+
clientId: options.clientId ?? 'fake-data-port',
|
|
103
|
+
connections,
|
|
104
|
+
reported,
|
|
105
|
+
onDispose: disposeEmitter.event,
|
|
106
|
+
async connect(): Promise<MessageConnection> {
|
|
107
|
+
const connection = await options.connect();
|
|
108
|
+
connections.push(connection);
|
|
109
|
+
return connection;
|
|
110
|
+
},
|
|
111
|
+
reportError(error: unknown, context: string): void {
|
|
112
|
+
reported.push({ error, context });
|
|
113
|
+
},
|
|
114
|
+
fireDispose(): void {
|
|
115
|
+
disposeEmitter.fire(undefined);
|
|
116
|
+
},
|
|
117
|
+
dispose(): void {
|
|
118
|
+
disposeEmitter.dispose();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A {@link DataClientProtocol} plus the arrays it records into. */
|
|
124
|
+
export interface CapturingDataClient<
|
|
125
|
+
TTransfer extends TransferElement,
|
|
126
|
+
TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
|
|
127
|
+
TProject extends Project = Project
|
|
128
|
+
> {
|
|
129
|
+
/** The client to hand to a `DataSession` or bind as an RPC `localTarget`. */
|
|
130
|
+
readonly client: DataClientProtocol<TTransfer, TDiagnostic, TProject>;
|
|
131
|
+
/** Every `onDocumentUpdated` event, in arrival order. */
|
|
132
|
+
readonly updates: TransferDocumentUpdatedEvent<TTransfer, TDiagnostic>[];
|
|
133
|
+
/** Every `onDocumentSaved` event, in arrival order. */
|
|
134
|
+
readonly saves: TransferDocumentSavedEvent<TTransfer, TDiagnostic>[];
|
|
135
|
+
/** Every `onProjectsChanged` event, in arrival order. */
|
|
136
|
+
readonly projectsChanges: ProjectsChangedEvent<TProject>[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A {@link DataClientProtocol} that records every notification it receives.
|
|
141
|
+
*
|
|
142
|
+
* Recording ALL THREE channels even when a suite reads one is deliberate: an
|
|
143
|
+
* event delivered on the wrong channel is a real defect of the data head, and a
|
|
144
|
+
* double that drops the other two turns it into silence on the one being
|
|
145
|
+
* watched.
|
|
146
|
+
*
|
|
147
|
+
* An override replaces a channel's recording rather than supplementing it, so
|
|
148
|
+
* the corresponding array stays empty — that is what makes an override usable
|
|
149
|
+
* as a barrier (rejecting, counting differently, throwing) rather than only as
|
|
150
|
+
* a spy.
|
|
151
|
+
*/
|
|
152
|
+
export function makeCapturingDataClient<
|
|
153
|
+
TTransfer extends TransferElement,
|
|
154
|
+
TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
|
|
155
|
+
TProject extends Project = Project
|
|
156
|
+
>(overrides: Partial<DataClientProtocol<TTransfer, TDiagnostic, TProject>> = {}): CapturingDataClient<TTransfer, TDiagnostic, TProject> {
|
|
157
|
+
const updates: TransferDocumentUpdatedEvent<TTransfer, TDiagnostic>[] = [];
|
|
158
|
+
const saves: TransferDocumentSavedEvent<TTransfer, TDiagnostic>[] = [];
|
|
159
|
+
const projectsChanges: ProjectsChangedEvent<TProject>[] = [];
|
|
160
|
+
const client: DataClientProtocol<TTransfer, TDiagnostic, TProject> = {
|
|
161
|
+
onDocumentUpdated:
|
|
162
|
+
overrides.onDocumentUpdated ??
|
|
163
|
+
(event => {
|
|
164
|
+
updates.push(event);
|
|
165
|
+
}),
|
|
166
|
+
onDocumentSaved:
|
|
167
|
+
overrides.onDocumentSaved ??
|
|
168
|
+
(event => {
|
|
169
|
+
saves.push(event);
|
|
170
|
+
}),
|
|
171
|
+
onProjectsChanged:
|
|
172
|
+
overrides.onProjectsChanged ??
|
|
173
|
+
(event => {
|
|
174
|
+
projectsChanges.push(event);
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
return { client, updates, saves, projectsChanges };
|
|
178
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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, type Timed } from '../clock';
|
|
11
|
+
import { Disposable } from '../util';
|
|
12
|
+
|
|
13
|
+
/** A {@link Clock} whose time only moves when the test calls {@link FakeClock.advance}. */
|
|
14
|
+
export interface FakeClock extends Clock {
|
|
15
|
+
/**
|
|
16
|
+
* Move virtual time forward by `ms`, firing every {@link Clock.setTimer}
|
|
17
|
+
* whose deadline falls within the window — in chronological order, each at
|
|
18
|
+
* its own deadline (so `now()` inside a callback reads the fire instant, not
|
|
19
|
+
* the advance target). Timers scheduled by a callback fire too if they fall
|
|
20
|
+
* within the same window.
|
|
21
|
+
*/
|
|
22
|
+
advance(ms: number): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface FakeTimer {
|
|
26
|
+
readonly at: number;
|
|
27
|
+
readonly callback: () => void;
|
|
28
|
+
disposed: boolean;
|
|
29
|
+
fired: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* In-process {@link Clock} double for deterministic time. The real
|
|
34
|
+
* `SystemClock` has two underlying sources
|
|
35
|
+
* (`Date.now` for `now()`, `performance.now()` for the stopwatch); the fake
|
|
36
|
+
* collapses them onto ONE virtual time axis, so a single
|
|
37
|
+
* {@link FakeClock.advance} drives `now()`, every live stopwatch's elapsed, and
|
|
38
|
+
* any due timer together.
|
|
39
|
+
*
|
|
40
|
+
* Server-free and DI-free — lives in `@hydranium/protocol/testing` so every
|
|
41
|
+
* head can bind it without pulling in a server package.
|
|
42
|
+
*/
|
|
43
|
+
export function makeFakeClock(options: { now?: number } = {}): FakeClock {
|
|
44
|
+
let current = options.now ?? 0;
|
|
45
|
+
const timers: FakeTimer[] = [];
|
|
46
|
+
|
|
47
|
+
const makeStopwatch = (): Stopwatch => {
|
|
48
|
+
const start = current;
|
|
49
|
+
let lastLap = current;
|
|
50
|
+
let stopped: number | undefined;
|
|
51
|
+
return {
|
|
52
|
+
get elapsedMs(): number {
|
|
53
|
+
return (stopped ?? current) - start;
|
|
54
|
+
},
|
|
55
|
+
lap(): number {
|
|
56
|
+
const at = stopped ?? current;
|
|
57
|
+
const split = at - lastLap;
|
|
58
|
+
lastLap = at;
|
|
59
|
+
return split;
|
|
60
|
+
},
|
|
61
|
+
stop(): number {
|
|
62
|
+
if (stopped === undefined) {
|
|
63
|
+
stopped = current;
|
|
64
|
+
}
|
|
65
|
+
return stopped - start;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
now: () => current,
|
|
72
|
+
setTimer(callback: () => void, ms: number): Disposable {
|
|
73
|
+
const timer: FakeTimer = { at: current + ms, callback, disposed: false, fired: false };
|
|
74
|
+
timers.push(timer);
|
|
75
|
+
return Disposable.create(() => {
|
|
76
|
+
timer.disposed = true;
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
stopwatch: makeStopwatch,
|
|
80
|
+
measure: (<T>(callback: () => T | Promise<T>): Timed<T> | Promise<Timed<T>> => {
|
|
81
|
+
const stopwatch = makeStopwatch();
|
|
82
|
+
const result = callback();
|
|
83
|
+
if (result instanceof Promise) {
|
|
84
|
+
return (result as Promise<T>).then(value => ({ result: value, elapsedMs: stopwatch.elapsedMs }));
|
|
85
|
+
}
|
|
86
|
+
return { result: result as T, elapsedMs: stopwatch.elapsedMs };
|
|
87
|
+
}) as Clock['measure'],
|
|
88
|
+
advance(ms: number): void {
|
|
89
|
+
const target = current + ms;
|
|
90
|
+
// Fire due timers one at a time, re-scanning after each so a callback
|
|
91
|
+
// that schedules a nearer timer still fires within this window.
|
|
92
|
+
for (;;) {
|
|
93
|
+
const next = timers
|
|
94
|
+
.filter(timer => !timer.disposed && !timer.fired && timer.at <= target)
|
|
95
|
+
.sort((left, right) => left.at - right.at)[0];
|
|
96
|
+
if (!next) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
current = next.at;
|
|
100
|
+
next.fired = true;
|
|
101
|
+
next.callback();
|
|
102
|
+
}
|
|
103
|
+
current = target;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 uniform contract every framework test harness satisfies. A harness is
|
|
12
|
+
* a wired bundle around a system-under-test, driven through a seam; its full
|
|
13
|
+
* shape is `{ <subject>, <seam>, …captureArrays, dispose() }`. This marker
|
|
14
|
+
* pins the one member that is identical across every harness — `dispose()` —
|
|
15
|
+
* so teardown is always `harness.dispose()` regardless of which harness, and
|
|
16
|
+
* a `Harness` reference can release any of them without knowing its concrete
|
|
17
|
+
* type. Each concrete harness interface — per head, and per conformance driver
|
|
18
|
+
* port — `extends Harness` and adds its subject / seam / capture members on
|
|
19
|
+
* top.
|
|
20
|
+
*/
|
|
21
|
+
export interface Harness {
|
|
22
|
+
/** Release every resource the harness holds. Idempotent; the uniform teardown hook. */
|
|
23
|
+
dispose(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
// Subpath barrel for `@hydranium/protocol/testing` — the BROWSER-NEUTRAL shared
|
|
11
|
+
// primitives: `makeFakeClock` (deterministic `Clock` double on one virtual time
|
|
12
|
+
// axis), `waitFor` / `tick`, the client-side data-head doubles
|
|
13
|
+
// (`makeFakeDataPort` / `makeCapturingDataClient`), and the `Harness` marker
|
|
14
|
+
// interface every framework harness extends. Kept out of the main barrel so production bundles don't pull
|
|
15
|
+
// the test scaffolding in by default; adopters opt in by importing from
|
|
16
|
+
// `@hydranium/protocol/testing`.
|
|
17
|
+
//
|
|
18
|
+
// Neutrality is gated (`scripts/check-neutral-bundles.mjs`), which is why the
|
|
19
|
+
// duplex transports are NOT here: a `PassThrough` pair is in their exported
|
|
20
|
+
// type, so they cannot be made portable and live at `./testing/node` instead.
|
|
21
|
+
// The same rule the package surface uses — the portable name is the short one.
|
|
22
|
+
|
|
23
|
+
export * from './data-doubles';
|
|
24
|
+
export * from './fake-clock';
|
|
25
|
+
export * from './harness';
|
|
26
|
+
export * from './wait-for';
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { StreamMessageReader, StreamMessageWriter, createMessageConnection, type MessageConnection } from 'vscode-jsonrpc/node';
|
|
11
|
+
import { makeDuplexStreamPair } from './duplex-stream';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Bidirectional in-process vscode-jsonrpc bridge — two MessageConnections
|
|
15
|
+
* crossed over a PassThrough pair, both already listening. The framework's
|
|
16
|
+
* standard in-process test transport.
|
|
17
|
+
*
|
|
18
|
+
* Typical use: wire a `DataServer` to `pair.left` and exercise it from a
|
|
19
|
+
* client proxy on `pair.right` (or vice versa). Call `dispose` at test
|
|
20
|
+
* teardown to release the underlying streams.
|
|
21
|
+
*/
|
|
22
|
+
export interface DuplexConnectionPair {
|
|
23
|
+
readonly left: MessageConnection;
|
|
24
|
+
readonly right: MessageConnection;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function makeDuplexConnectionPair(): DuplexConnectionPair {
|
|
29
|
+
// `left` is the client side, `right` the server side: `clientToServer` is
|
|
30
|
+
// what `left` writes and `right` reads; `serverToClient` is what `right`
|
|
31
|
+
// writes and `left` reads.
|
|
32
|
+
const pair = makeDuplexStreamPair();
|
|
33
|
+
const { clientToServer, serverToClient } = pair;
|
|
34
|
+
|
|
35
|
+
const left = createMessageConnection(new StreamMessageReader(serverToClient), new StreamMessageWriter(clientToServer));
|
|
36
|
+
const right = createMessageConnection(new StreamMessageReader(clientToServer), new StreamMessageWriter(serverToClient));
|
|
37
|
+
|
|
38
|
+
left.listen();
|
|
39
|
+
right.listen();
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
left,
|
|
43
|
+
right,
|
|
44
|
+
dispose(): void {
|
|
45
|
+
left.dispose();
|
|
46
|
+
right.dispose();
|
|
47
|
+
pair.dispose();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { PassThrough } from 'node:stream';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A crossed pair of in-process `PassThrough` streams modelling one
|
|
14
|
+
* bidirectional client/server transport. Each `PassThrough` is itself both
|
|
15
|
+
* readable and writable: the side named first writes, the side named second
|
|
16
|
+
* reads off the same object. The shared substrate under the framework's
|
|
17
|
+
* in-process test transports — `makeDuplexConnectionPair` layers a
|
|
18
|
+
* `MessageConnection` pair on top.
|
|
19
|
+
*/
|
|
20
|
+
export interface DuplexStreamPair {
|
|
21
|
+
/** Client writes here; server reads here. */
|
|
22
|
+
readonly clientToServer: PassThrough;
|
|
23
|
+
/** Server writes here; client reads here. */
|
|
24
|
+
readonly serverToClient: PassThrough;
|
|
25
|
+
/** Destroy both streams. Idempotent. */
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a fresh {@link DuplexStreamPair}. The two streams are independent;
|
|
31
|
+
* data written to one never appears on the other. Call `dispose` at test
|
|
32
|
+
* teardown to release them.
|
|
33
|
+
*/
|
|
34
|
+
export function makeDuplexStreamPair(): DuplexStreamPair {
|
|
35
|
+
const clientToServer = new PassThrough();
|
|
36
|
+
const serverToClient = new PassThrough();
|
|
37
|
+
|
|
38
|
+
let disposed = false;
|
|
39
|
+
return {
|
|
40
|
+
clientToServer,
|
|
41
|
+
serverToClient,
|
|
42
|
+
dispose(): void {
|
|
43
|
+
if (disposed) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
disposed = true;
|
|
47
|
+
clientToServer.destroy();
|
|
48
|
+
serverToClient.destroy();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
// Subpath barrel for `@hydranium/protocol/testing/node` — the test transports
|
|
11
|
+
// that need a Node runtime: a crossed `PassThrough` pair and the in-process
|
|
12
|
+
// `MessageConnection` bridge over it.
|
|
13
|
+
//
|
|
14
|
+
// Separate from `./testing` because a `PassThrough` is in `DuplexStreamPair`'s
|
|
15
|
+
// exported type and `StreamMessageReader` comes from `vscode-jsonrpc/node`, so
|
|
16
|
+
// neither can be made portable — and keeping them in the neutral barrel taints
|
|
17
|
+
// every subpath that re-exports it, which is what made three of this repo's
|
|
18
|
+
// `./testing` entries unbundleable for a browser while having no Node code of
|
|
19
|
+
// their own.
|
|
20
|
+
|
|
21
|
+
export * from './duplex-connection';
|
|
22
|
+
export * from './duplex-stream';
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
* Two test primitives for awaiting in-process asynchrony (RPC notifications
|
|
12
|
+
* crossing the duplex wire, `DocumentBuilder` phase events, etc.) WITHOUT a
|
|
13
|
+
* flaky fixed sleep.
|
|
14
|
+
*
|
|
15
|
+
* Decision rule:
|
|
16
|
+
* - Asserting an effect **will** happen ("eventually N events / a build ran /
|
|
17
|
+
* the doc reached a phase")? → {@link waitFor} with a predicate over the
|
|
18
|
+
* observable. Non-racy: resolves the instant it is true, tolerates a slow CI.
|
|
19
|
+
* - Asserting an effect **did not / has not yet** happened ("no event for an
|
|
20
|
+
* unsubscribed URI", "the gated call has not resolved")? → {@link tick}: a
|
|
21
|
+
* negative cannot be polled, so yield the loop a bounded moment to give the
|
|
22
|
+
* (unwanted) effect a chance, then assert its absence.
|
|
23
|
+
*
|
|
24
|
+
* Default to `waitFor`; reach for `tick` only when the assertion is an absence.
|
|
25
|
+
* A microtask flush (`await Promise.resolve()` / `queue-microtask`) is never the
|
|
26
|
+
* answer here — stream-delivered JSON-RPC notifications arrive on a macrotask/IO
|
|
27
|
+
* turn, after microtasks drain.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Options for {@link waitFor}. */
|
|
31
|
+
export interface WaitForOptions {
|
|
32
|
+
/** Reject after this many milliseconds if the predicate is still false. Default 2000. */
|
|
33
|
+
readonly timeoutMs?: number;
|
|
34
|
+
/** Poll the predicate every this-many milliseconds. Default 5. */
|
|
35
|
+
readonly intervalMs?: number;
|
|
36
|
+
/** Error message on timeout. Default a generic phrasing. */
|
|
37
|
+
readonly message?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve once `predicate()` returns true, polling at `intervalMs`; reject with
|
|
42
|
+
* `message` once `timeoutMs` elapses. The **non-racy** alternative to a fixed
|
|
43
|
+
* sleep for awaiting an asynchronous side effect that lands on an observable
|
|
44
|
+
* value.
|
|
45
|
+
*
|
|
46
|
+
* It resolves the instant the condition holds, so it is *faster* than a fixed
|
|
47
|
+
* delay on the happy path, and tolerates a slow event loop up to `timeoutMs`, so
|
|
48
|
+
* it does not flake under parallel CI where a fixed `setTimeout` can be starved
|
|
49
|
+
* past the test timeout.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* await proxy.updateModelDocument({ uri, clientId, model });
|
|
53
|
+
* await waitFor(() => harness.events.length === 1); // the notification crossed
|
|
54
|
+
* expect(harness.events[0].document.uri).toBe(uri);
|
|
55
|
+
*/
|
|
56
|
+
export function waitFor(predicate: () => boolean, options: WaitForOptions = {}): Promise<void> {
|
|
57
|
+
const timeoutMs = options.timeoutMs ?? 2000;
|
|
58
|
+
const intervalMs = options.intervalMs ?? 5;
|
|
59
|
+
const message = options.message ?? `Timed out after ${timeoutMs}ms waiting for a condition`;
|
|
60
|
+
if (predicate()) {
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
return new Promise<void>((resolve, reject) => {
|
|
64
|
+
const deadline = setTimeout(() => {
|
|
65
|
+
clearInterval(poll);
|
|
66
|
+
reject(new Error(message));
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
const poll = setInterval(() => {
|
|
69
|
+
if (predicate()) {
|
|
70
|
+
clearInterval(poll);
|
|
71
|
+
clearTimeout(deadline);
|
|
72
|
+
resolve();
|
|
73
|
+
}
|
|
74
|
+
}, intervalMs);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Yield the event loop for `ms` (default 10). The bounded counterpart to
|
|
80
|
+
* {@link waitFor}, for the rare assertion that something has NOT happened — a
|
|
81
|
+
* *negative* you cannot poll positively. Give async delivery a real chance to
|
|
82
|
+
* (wrongly) occur, then assert it did not.
|
|
83
|
+
*
|
|
84
|
+
* A fixed yield is unavoidable here, but the failure mode is a false-green, not
|
|
85
|
+
* the flaky-red a fixed delay causes on a positive wait.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* await proxy.saveModelDocument({ uri, clientId, model }); // no subscriber yet
|
|
89
|
+
* await tick(); // give a (wrongly) fired event a chance to land
|
|
90
|
+
* expect(savedEvents).toHaveLength(0); // assert the ABSENCE
|
|
91
|
+
*/
|
|
92
|
+
export function tick(ms = 10): Promise<void> {
|
|
93
|
+
return new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
94
|
+
}
|