@vscode/component-explorer-cli 0.2.1-18 → 0.2.1-181
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 +130 -12
- package/dist/WorktreePool.d.ts +5 -4
- package/dist/WorktreePool.d.ts.map +1 -1
- package/dist/WorktreePool.js +3 -3
- package/dist/WorktreePool.js.map +1 -1
- package/dist/_virtual/_build-info.js +1 -1
- package/dist/browserPage.d.ts +55 -1
- package/dist/browserPage.d.ts.map +1 -1
- package/dist/browserPage.js +103 -7
- package/dist/browserPage.js.map +1 -1
- package/dist/browserPage.test.d.ts +2 -0
- package/dist/browserPage.test.d.ts.map +1 -0
- package/dist/commands/acceptCommand.d.ts +1 -1
- package/dist/commands/acceptCommand.d.ts.map +1 -1
- package/dist/commands/acceptCommand.js +21 -14
- package/dist/commands/acceptCommand.js.map +1 -1
- package/dist/commands/checkStabilityCommand.d.ts +2 -0
- package/dist/commands/checkStabilityCommand.d.ts.map +1 -1
- package/dist/commands/checkStabilityCommand.js +25 -8
- package/dist/commands/checkStabilityCommand.js.map +1 -1
- package/dist/commands/compareCommand.d.ts +1 -1
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +31 -22
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/inputArg.d.ts +33 -0
- package/dist/commands/inputArg.d.ts.map +1 -0
- package/dist/commands/inputArg.js +103 -0
- package/dist/commands/inputArg.js.map +1 -0
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +18 -12
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/renderCommand.d.ts +10 -1
- package/dist/commands/renderCommand.d.ts.map +1 -1
- package/dist/commands/renderCommand.js +390 -61
- package/dist/commands/renderCommand.js.map +1 -1
- package/dist/commands/renderCommand.test.d.ts +2 -0
- package/dist/commands/renderCommand.test.d.ts.map +1 -0
- package/dist/commands/serveCommand.d.ts +4 -1
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +105 -36
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/serviceDiffCommitsCommand.d.ts.map +1 -1
- package/dist/commands/serviceDiffCommitsCommand.js +10 -9
- package/dist/commands/serviceDiffCommitsCommand.js.map +1 -1
- package/dist/commands/watchCommand.d.ts +1 -0
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +31 -20
- package/dist/commands/watchCommand.js.map +1 -1
- package/dist/comparison.d.ts +4 -3
- package/dist/comparison.d.ts.map +1 -1
- package/dist/comparison.js +17 -14
- package/dist/comparison.js.map +1 -1
- package/dist/component-explorer-config.schema.json +55 -2
- package/dist/componentExplorer.d.ts +110 -12
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +177 -24
- package/dist/componentExplorer.js.map +1 -1
- package/dist/config.d.ts +33 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +64 -27
- package/dist/config.js.map +1 -1
- package/dist/coverage.d.ts +64 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +212 -0
- package/dist/coverage.js.map +1 -0
- package/dist/daemon/DaemonService.d.ts +5 -199
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +248 -300
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/approvalStore.d.ts +2 -1
- package/dist/daemon/approvalStore.d.ts.map +1 -1
- package/dist/daemon/approvalStore.js +4 -3
- package/dist/daemon/approvalStore.js.map +1 -1
- package/dist/daemon/client.d.ts +76 -5
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/daemonRpc.test.d.ts +2 -0
- package/dist/daemon/daemonRpc.test.d.ts.map +1 -0
- package/dist/daemon/endpointServer.d.ts +4 -0
- package/dist/daemon/endpointServer.d.ts.map +1 -0
- package/dist/daemon/endpointServer.js +24 -0
- package/dist/daemon/endpointServer.js.map +1 -0
- package/dist/daemon/inProcessClient.d.ts +3 -9
- package/dist/daemon/inProcessClient.d.ts.map +1 -1
- package/dist/daemon/inProcessClient.js +29 -30
- package/dist/daemon/inProcessClient.js.map +1 -1
- package/dist/daemon/lifecycle.d.ts +5 -5
- package/dist/daemon/lifecycle.d.ts.map +1 -1
- package/dist/daemon/lifecycle.js +4 -3
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/pipeClient.d.ts +2 -3
- package/dist/daemon/pipeClient.d.ts.map +1 -1
- package/dist/daemon/pipeClient.js +93 -194
- package/dist/daemon/pipeClient.js.map +1 -1
- package/dist/daemon/pipeName.d.ts +3 -3
- package/dist/daemon/pipeName.d.ts.map +1 -1
- package/dist/daemon/pipeName.js +6 -5
- package/dist/daemon/pipeName.js.map +1 -1
- package/dist/daemon/pipeServer.d.ts +2 -2
- package/dist/daemon/pipeServer.d.ts.map +1 -1
- package/dist/daemon/pipeServer.js +79 -70
- package/dist/daemon/pipeServer.js.map +1 -1
- package/dist/daemon/registerDaemonConnection.d.ts +7 -0
- package/dist/daemon/registerDaemonConnection.d.ts.map +1 -0
- package/dist/daemon/registerDaemonConnection.js +37 -0
- package/dist/daemon/registerDaemonConnection.js.map +1 -0
- package/dist/daemon/transports.test.d.ts +2 -0
- package/dist/daemon/transports.test.d.ts.map +1 -0
- package/dist/daemon/types.d.ts +1655 -8
- package/dist/daemon/types.d.ts.map +1 -1
- package/dist/daemon/types.js +4 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/daemon/version.d.ts +1 -5
- package/dist/daemon/version.d.ts.map +1 -1
- package/dist/daemon/version.js +3 -7
- package/dist/daemon/version.js.map +1 -1
- package/dist/dependencyInstaller.d.ts +2 -1
- package/dist/dependencyInstaller.d.ts.map +1 -1
- package/dist/dependencyInstaller.js +4 -4
- package/dist/dependencyInstaller.js.map +1 -1
- package/dist/explorerSession.d.ts +5 -1
- package/dist/explorerSession.d.ts.map +1 -1
- package/dist/explorerSession.js +24 -7
- package/dist/explorerSession.js.map +1 -1
- package/dist/external/linkrpc/typescript/packages/linkrpc/dist/chunks/inspectionHost-BA6ZmC_P.js +2523 -0
- package/dist/external/linkrpc/typescript/packages/linkrpc/dist/chunks/inspectionHost-BA6ZmC_P.js.map +1 -0
- package/dist/external/linkrpc/typescript/packages/linkrpc/dist/chunks/src-DYVSK_tz.js +1871 -0
- package/dist/external/linkrpc/typescript/packages/linkrpc/dist/chunks/src-DYVSK_tz.js.map +1 -0
- package/dist/external/linkrpc/typescript/packages/linkrpc/dist/chunks/webSocketTransport-Ct4OIdG9.js +220 -0
- package/dist/external/linkrpc/typescript/packages/linkrpc/dist/chunks/webSocketTransport-Ct4OIdG9.js.map +1 -0
- package/dist/external/linkrpc/typescript/packages/linkrpc/dist/node.js +721 -0
- package/dist/external/linkrpc/typescript/packages/linkrpc/dist/node.js.map +1 -0
- package/dist/git/gitCommitResolver.d.ts +2 -1
- package/dist/git/gitCommitResolver.d.ts.map +1 -1
- package/dist/git/gitCommitResolver.js +8 -8
- package/dist/git/gitCommitResolver.js.map +1 -1
- package/dist/git/gitIndexResolver.d.ts +2 -1
- package/dist/git/gitIndexResolver.d.ts.map +1 -1
- package/dist/git/gitIndexResolver.js +7 -7
- package/dist/git/gitIndexResolver.js.map +1 -1
- package/dist/git/gitService.d.ts +5 -4
- package/dist/git/gitService.d.ts.map +1 -1
- package/dist/git/gitService.js.map +1 -1
- package/dist/git/gitUtils.d.ts +6 -5
- package/dist/git/gitUtils.d.ts.map +1 -1
- package/dist/git/gitUtils.js +5 -4
- package/dist/git/gitUtils.js.map +1 -1
- package/dist/git/gitWorktreeManager.d.ts +15 -14
- package/dist/git/gitWorktreeManager.d.ts.map +1 -1
- package/dist/git/gitWorktreeManager.js +10 -11
- package/dist/git/gitWorktreeManager.js.map +1 -1
- package/dist/git/testUtils.d.ts +2 -1
- package/dist/git/testUtils.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/js-component-explorer/packages/common/dist/daemonRpc.js +165 -0
- package/dist/js-component-explorer/packages/common/dist/daemonRpc.js.map +1 -0
- package/dist/js-component-explorer/packages/common/dist/explorerUrl.js +21 -0
- package/dist/js-component-explorer/packages/common/dist/explorerUrl.js.map +1 -0
- package/dist/js-component-explorer/packages/common/dist/renderManifest.js +68 -0
- package/dist/js-component-explorer/packages/common/dist/renderManifest.js.map +1 -0
- package/dist/js-component-explorer/packages/semver/dist/index.js.map +1 -0
- package/dist/manifest.schema.json +44 -16
- package/dist/mcp/DaemonAccessor.d.ts +2 -3
- package/dist/mcp/DaemonAccessor.d.ts.map +1 -1
- package/dist/mcp/McpServer.d.ts +1 -1
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +252 -159
- package/dist/mcp/McpServer.js.map +1 -1
- package/dist/path.d.ts +30 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +78 -0
- package/dist/path.js.map +1 -0
- package/dist/processTree.d.ts +6 -1
- package/dist/processTree.d.ts.map +1 -1
- package/dist/processTree.js +48 -5
- package/dist/processTree.js.map +1 -1
- package/dist/renderManifest.d.ts +70 -5
- package/dist/renderManifest.js +1 -1
- package/dist/server/httpServer.d.ts +1 -1
- package/dist/server/httpServer.d.ts.map +1 -1
- package/dist/server/httpServer.js +4 -8
- package/dist/server/httpServer.js.map +1 -1
- package/dist/server/serverConfig.d.ts +23 -8
- package/dist/server/serverConfig.d.ts.map +1 -1
- package/dist/server/serverConfig.js +11 -18
- package/dist/server/serverConfig.js.map +1 -1
- package/dist/server/viteServer.d.ts +16 -3
- package/dist/server/viteServer.d.ts.map +1 -1
- package/dist/server/viteServer.js +86 -5
- package/dist/server/viteServer.js.map +1 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1 -1
- package/dist/storage.js.map +1 -1
- package/dist/utils.d.ts +3 -30
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -24
- package/dist/utils.js.map +1 -1
- package/dist/visualCache.d.ts +2 -1
- package/dist/visualCache.d.ts.map +1 -1
- package/dist/visualCache.js +4 -3
- package/dist/visualCache.js.map +1 -1
- package/dist/viteProjectRef.d.ts +4 -12
- package/dist/viteProjectRef.d.ts.map +1 -1
- package/dist/viteProjectRef.js +9 -17
- package/dist/viteProjectRef.js.map +1 -1
- package/dist/vscode-observables/observables/dist/disposables.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/base.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/debugLocation.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/debugName.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/index.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js.map +1 -0
- package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/logging/debugGetDependencyGraph.js +1 -1
- package/dist/vscode-observables/observables/dist/observableInternal/logging/debugGetDependencyGraph.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/logging/logging.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/derived.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/derivedImpl.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/observableValue.js.map +1 -0
- package/dist/{external/vscode-observables/observables/dist/observableInternal/reactions/autorun.js → vscode-observables/observables/dist/observableInternal/reactions/createEffect.js} +9 -5
- package/dist/vscode-observables/observables/dist/observableInternal/reactions/createEffect.js.map +1 -0
- package/dist/{external/vscode-observables/observables/dist/observableInternal/reactions/autorunImpl.js → vscode-observables/observables/dist/observableInternal/reactions/createEffectImpl.js} +1 -1
- package/dist/vscode-observables/observables/dist/observableInternal/reactions/createEffectImpl.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/transaction.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/utils/utils.js.map +1 -0
- package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/utils/utilsCancellation.js +2 -2
- package/dist/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js.map +1 -0
- package/package.json +18 -15
- package/dist/commands/artifactCommand.d.ts +0 -14
- package/dist/commands/artifactCommand.d.ts.map +0 -1
- package/dist/external/vscode-observables/observables/dist/disposables.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/base.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/debugLocation.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/debugName.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/index.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugGetDependencyGraph.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/logging.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derived.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derivedImpl.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/observableValue.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/reactions/autorun.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/reactions/autorunImpl.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/transaction.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utils.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js.map +0 -1
- package/dist/formatValue.js +0 -96
- package/dist/formatValue.js.map +0 -1
- package/dist/packages/common/dist/renderManifest.js +0 -46
- package/dist/packages/common/dist/renderManifest.js.map +0 -1
- package/dist/packages/semver/dist/index.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-FJ7AVNQE.js +0 -138
- package/dist/packages/simple-api/dist/chunk-FJ7AVNQE.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-TTRCY65Z.js +0 -27
- package/dist/packages/simple-api/dist/chunk-TTRCY65Z.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-WNXMRXWV.js +0 -28
- package/dist/packages/simple-api/dist/chunk-WNXMRXWV.js.map +0 -1
- package/dist/packages/simple-api/dist/express.js +0 -104
- package/dist/packages/simple-api/dist/express.js.map +0 -1
- /package/dist/{packages → js-component-explorer/packages}/semver/dist/index.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/disposables.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/base.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/commonFacade/deps.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/debugLocation.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/debugName.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/index.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/logging/consoleObservableLogger.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/logging/logging.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/baseObservable.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/derived.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/derivedImpl.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/observableFromEvent.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/observableValue.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/transaction.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/utils/utils.js +0 -0
package/dist/external/linkrpc/typescript/packages/linkrpc/dist/chunks/inspectionHost-BA6ZmC_P.js
ADDED
|
@@ -0,0 +1,2523 @@
|
|
|
1
|
+
import { void as _void, object, optional, unknown, string, enum as _enum, union, number, array, int, nonnegative, strictObject, literal, record, boolean } from 'zod/mini';
|
|
2
|
+
import { toJSONSchema, safeParse } from 'zod/v4/core';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/*---------------------------------------------------------------------------------------------
|
|
6
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
7
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
8
|
+
*--------------------------------------------------------------------------------------------*/
|
|
9
|
+
//#region src/schema/normalize.ts
|
|
10
|
+
/**
|
|
11
|
+
* Keys that describe structural shape and are part of the decidable subset.
|
|
12
|
+
* Everything else gets stripped during normalization.
|
|
13
|
+
*
|
|
14
|
+
* Note: `not` is intentionally NOT in this list. The special case `{"not":{}}`
|
|
15
|
+
* (i.e. matches-nothing) is mapped to the literal `false` bottom; any other
|
|
16
|
+
* `not` is dropped.
|
|
17
|
+
*/
|
|
18
|
+
const KEPT_KEYS = /* @__PURE__ */ new Set([
|
|
19
|
+
"type",
|
|
20
|
+
"format",
|
|
21
|
+
"properties",
|
|
22
|
+
"required",
|
|
23
|
+
"additionalProperties",
|
|
24
|
+
"items",
|
|
25
|
+
"prefixItems",
|
|
26
|
+
"const",
|
|
27
|
+
"enum",
|
|
28
|
+
"anyOf",
|
|
29
|
+
"oneOf",
|
|
30
|
+
"discriminator",
|
|
31
|
+
"$ref",
|
|
32
|
+
"title",
|
|
33
|
+
"description"
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Normalize raw JSON Schema output (e.g. from `z.toJSONSchema`) into the
|
|
37
|
+
* SvcJsonSchema subset:
|
|
38
|
+
*
|
|
39
|
+
* - drop annotation keys (`examples`, `default`, `$comment`, `readOnly`, ...)
|
|
40
|
+
* - drop out-of-subset refinements (`pattern`, `minimum`, `multipleOf`,
|
|
41
|
+
* `allOf`, `oneOf`, `if/then/else`, `patternProperties`, ...)
|
|
42
|
+
* - empty schema `{}` ⇒ `true` (top)
|
|
43
|
+
* - `{"not":{}}` ⇒ `false` (bottom); any other `not` is stripped
|
|
44
|
+
* - `type:"object"` without `additionalProperties` ⇒ closed (`false`),
|
|
45
|
+
* matching linkrpc's stricter contract
|
|
46
|
+
* - producer `type` arrays ⇒ `anyOf` branches, preserving their order
|
|
47
|
+
*
|
|
48
|
+
* The output is canonical: structurally equal inputs produce structurally
|
|
49
|
+
* equal outputs, which is what `computeInterfaceHash` relies on.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeJsonSchema(raw) {
|
|
52
|
+
if (raw === true || raw === false) return raw;
|
|
53
|
+
if (raw === null || typeof raw !== "object") throw new Error(`normalizeJsonSchema: expected object, got ${typeof raw}`);
|
|
54
|
+
if (Array.isArray(raw)) throw new Error("normalizeJsonSchema: expected object, got array");
|
|
55
|
+
const r = raw;
|
|
56
|
+
if ("not" in r && isEmptyObject(r["not"])) return false;
|
|
57
|
+
if (Array.isArray(r["type"])) {
|
|
58
|
+
const types = r["type"];
|
|
59
|
+
if (types.length === 0 || new Set(types).size !== types.length || types.some((type) => ![
|
|
60
|
+
"null",
|
|
61
|
+
"boolean",
|
|
62
|
+
"number",
|
|
63
|
+
"integer",
|
|
64
|
+
"string",
|
|
65
|
+
"array",
|
|
66
|
+
"object"
|
|
67
|
+
].includes(type))) throw new Error("normalizeJsonSchema: type array must contain distinct JSON Schema type names");
|
|
68
|
+
const { title, description, ...branch } = r;
|
|
69
|
+
return normalizeJsonSchema({
|
|
70
|
+
title,
|
|
71
|
+
description,
|
|
72
|
+
anyOf: types.map((type) => {
|
|
73
|
+
const schema = {
|
|
74
|
+
...branch,
|
|
75
|
+
type
|
|
76
|
+
};
|
|
77
|
+
if (type !== "object") {
|
|
78
|
+
delete schema.properties;
|
|
79
|
+
delete schema.required;
|
|
80
|
+
delete schema.additionalProperties;
|
|
81
|
+
}
|
|
82
|
+
if (type !== "array") {
|
|
83
|
+
delete schema.items;
|
|
84
|
+
delete schema.prefixItems;
|
|
85
|
+
}
|
|
86
|
+
if (type !== "string" && type !== "number" && type !== "integer") delete schema.format;
|
|
87
|
+
return schema;
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [k, v] of Object.entries(r)) {
|
|
93
|
+
if (!KEPT_KEYS.has(k)) continue;
|
|
94
|
+
const normalized = normalizeChild(k, v);
|
|
95
|
+
if (normalized === void 0) continue;
|
|
96
|
+
out[k] = normalized;
|
|
97
|
+
}
|
|
98
|
+
if (out["type"] === "object" && !("additionalProperties" in out)) out["additionalProperties"] = false;
|
|
99
|
+
if (Array.isArray(out["oneOf"]) && !("discriminator" in out)) {
|
|
100
|
+
const detected = _detectDiscriminator(out["oneOf"]);
|
|
101
|
+
if (detected !== void 0) out["discriminator"] = detected;
|
|
102
|
+
}
|
|
103
|
+
if ("discriminator" in out && !Array.isArray(out["oneOf"])) delete out["discriminator"];
|
|
104
|
+
if (Object.keys(out).length === 0) return true;
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function normalizeChild(key, v) {
|
|
108
|
+
switch (key) {
|
|
109
|
+
case "properties": {
|
|
110
|
+
if (!isPlainObject(v)) return {};
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [pk, pv] of Object.entries(v)) out[pk] = normalizeJsonSchema(pv);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
case "items":
|
|
116
|
+
case "additionalProperties":
|
|
117
|
+
if (v === false) return false;
|
|
118
|
+
return normalizeJsonSchema(v);
|
|
119
|
+
case "prefixItems":
|
|
120
|
+
case "anyOf":
|
|
121
|
+
case "oneOf":
|
|
122
|
+
if (!Array.isArray(v)) return [];
|
|
123
|
+
return v.map((s) => normalizeJsonSchema(s));
|
|
124
|
+
case "discriminator": {
|
|
125
|
+
if (!isPlainObject(v)) return void 0;
|
|
126
|
+
const name = v["propertyName"];
|
|
127
|
+
if (typeof name !== "string" || name.length === 0) return void 0;
|
|
128
|
+
return { propertyName: name };
|
|
129
|
+
}
|
|
130
|
+
case "enum":
|
|
131
|
+
if (!Array.isArray(v)) return [];
|
|
132
|
+
return v.slice();
|
|
133
|
+
case "required":
|
|
134
|
+
if (!Array.isArray(v)) return [];
|
|
135
|
+
return v.slice().sort();
|
|
136
|
+
default: return v;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function isEmptyObject(v) {
|
|
140
|
+
return isPlainObject(v) && Object.keys(v).length === 0;
|
|
141
|
+
}
|
|
142
|
+
function isPlainObject(v) {
|
|
143
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Inspect a `oneOf` branch list and return a `discriminator` hint when
|
|
147
|
+
* every branch is an object schema and there is exactly one property name
|
|
148
|
+
* that appears in every branch with a distinct `const`-valued schema.
|
|
149
|
+
*
|
|
150
|
+
* This is the canonical JSON-Schema-only encoding of a tagged union, and
|
|
151
|
+
* the shape `z.toJSONSchema(z.discriminatedUnion(...))` emits.
|
|
152
|
+
*/
|
|
153
|
+
function _detectDiscriminator(branches) {
|
|
154
|
+
if (branches.length < 2) return void 0;
|
|
155
|
+
const branchPropsList = [];
|
|
156
|
+
for (const b of branches) {
|
|
157
|
+
if (b === true || b === false) return void 0;
|
|
158
|
+
if (b.type !== "object") return void 0;
|
|
159
|
+
const props = b.properties;
|
|
160
|
+
if (!props) return void 0;
|
|
161
|
+
branchPropsList.push(props);
|
|
162
|
+
}
|
|
163
|
+
let candidates = _constPropNames(branchPropsList[0]);
|
|
164
|
+
for (let i = 1; i < branchPropsList.length; i++) {
|
|
165
|
+
candidates = candidates.filter((n) => _constPropNames(branchPropsList[i]).includes(n));
|
|
166
|
+
if (candidates.length === 0) return void 0;
|
|
167
|
+
}
|
|
168
|
+
for (const name of candidates) {
|
|
169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
170
|
+
let allDistinct = true;
|
|
171
|
+
for (const props of branchPropsList) {
|
|
172
|
+
const s = props[name];
|
|
173
|
+
const key = JSON.stringify(s.const);
|
|
174
|
+
if (seen.has(key)) {
|
|
175
|
+
allDistinct = false;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
seen.add(key);
|
|
179
|
+
}
|
|
180
|
+
if (allDistinct) return { propertyName: name };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function _constPropNames(props) {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const [k, v] of Object.entries(props)) {
|
|
186
|
+
if (v === true || v === false) continue;
|
|
187
|
+
if ("const" in v) out.push(k);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/connection/rpcFailure.ts
|
|
193
|
+
const rpcFailureBrand = Symbol("linkrpc.rpcFailure");
|
|
194
|
+
/** A nominal failure wrapper: successful application objects can never collide with it. */
|
|
195
|
+
var RpcFailure = class {
|
|
196
|
+
error;
|
|
197
|
+
[rpcFailureBrand] = true;
|
|
198
|
+
constructor(error) {
|
|
199
|
+
this.error = error;
|
|
200
|
+
Object.freeze(this);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/protocol/jsonRpc.ts
|
|
205
|
+
const ErrorCode = {
|
|
206
|
+
/** Default code for named LinkRPC application errors. */
|
|
207
|
+
applicationError: 1,
|
|
208
|
+
parseError: -32700,
|
|
209
|
+
invalidRequest: -32600,
|
|
210
|
+
methodNotFound: -32601,
|
|
211
|
+
invalidParams: -32602,
|
|
212
|
+
internalError: -32603,
|
|
213
|
+
/** Caller is authenticated but lacks a capability covering the call. */
|
|
214
|
+
permissionRequired: -32401,
|
|
215
|
+
/** The peer a request was routed to detached before it could respond. */
|
|
216
|
+
peerDisconnected: -32402,
|
|
217
|
+
/** The request exceeded the hub's idle timeout with no stream activity. */
|
|
218
|
+
requestTimeout: -32403,
|
|
219
|
+
/** The request was cancelled (by the caller, or by the hub on disconnect). */
|
|
220
|
+
cancelled: -32800
|
|
221
|
+
};
|
|
222
|
+
function isRequest(m) {
|
|
223
|
+
return typeof m.method === "string" && m.id !== void 0;
|
|
224
|
+
}
|
|
225
|
+
function isNotification(m) {
|
|
226
|
+
return typeof m.method === "string" && m.id === void 0;
|
|
227
|
+
}
|
|
228
|
+
function isResponse(m) {
|
|
229
|
+
return typeof m.method !== "string";
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/schema/applicationErrorBrand.ts
|
|
233
|
+
const applicationErrorBrand = Symbol("linkrpc.applicationError");
|
|
234
|
+
function brandApplicationError(value) {
|
|
235
|
+
return Object.freeze(Object.defineProperty(value, applicationErrorBrand, { value: true }));
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/schema/schemaSource.ts
|
|
239
|
+
const schemaSources = /* @__PURE__ */ new WeakMap();
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/schema/validationIssue.ts
|
|
242
|
+
function jsonPointerSegment(key) {
|
|
243
|
+
return `/${String(key).replace(/~/g, "~0").replace(/\//g, "~1")}`;
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/schema/errorValidation.ts
|
|
247
|
+
const bodySchemas = /* @__PURE__ */ new WeakMap();
|
|
248
|
+
function parseDeclaredErrorBody(descriptor, body) {
|
|
249
|
+
return parseErrorBody(descriptor.bodySchema ?? errorBodySchema(descriptor), body);
|
|
250
|
+
}
|
|
251
|
+
function errorBodySchema(descriptor) {
|
|
252
|
+
let schema = bodySchemas.get(descriptor);
|
|
253
|
+
if (schema === void 0) {
|
|
254
|
+
const payload = descriptor.dataSchema === void 0 ? {} : { data: descriptor.dataSchema };
|
|
255
|
+
schema = descriptor.type === void 0 ? strictObject({
|
|
256
|
+
message: literal(descriptor.message),
|
|
257
|
+
...payload
|
|
258
|
+
}) : strictObject({
|
|
259
|
+
message: string(),
|
|
260
|
+
data: strictObject({
|
|
261
|
+
type: literal(descriptor.type),
|
|
262
|
+
...payload
|
|
263
|
+
})
|
|
264
|
+
});
|
|
265
|
+
bodySchemas.set(descriptor, schema);
|
|
266
|
+
}
|
|
267
|
+
return schema;
|
|
268
|
+
}
|
|
269
|
+
function parseErrorBody(schema, body) {
|
|
270
|
+
const parsed = safeParse(schema, body);
|
|
271
|
+
if (!parsed.success) return {
|
|
272
|
+
success: false,
|
|
273
|
+
issues: validationIssues(parsed.error.issues)
|
|
274
|
+
};
|
|
275
|
+
if (typeof parsed.data !== "object" || parsed.data === null || !("message" in parsed.data) || typeof parsed.data.message !== "string") return {
|
|
276
|
+
success: false,
|
|
277
|
+
issues: [{
|
|
278
|
+
path: "/message",
|
|
279
|
+
message: "Expected a string message"
|
|
280
|
+
}]
|
|
281
|
+
};
|
|
282
|
+
return {
|
|
283
|
+
success: true,
|
|
284
|
+
data: {
|
|
285
|
+
...parsed.data,
|
|
286
|
+
message: parsed.data.message
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function validationIssues(issues, prefix = []) {
|
|
291
|
+
return issues.flatMap((issue) => {
|
|
292
|
+
const path = [...prefix, ...issue.path];
|
|
293
|
+
if (issue.code === "invalid_union") return issue.errors.flatMap((branch) => validationIssues(branch, path));
|
|
294
|
+
if (issue.code === "unrecognized_keys") return issue.keys.map((key) => ({
|
|
295
|
+
path: pointer([...path, key]),
|
|
296
|
+
message: "Unexpected property"
|
|
297
|
+
}));
|
|
298
|
+
return [{
|
|
299
|
+
path: pointer(path),
|
|
300
|
+
message: issue.message
|
|
301
|
+
}];
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function pointer(path) {
|
|
305
|
+
return path.map(jsonPointerSegment).join("");
|
|
306
|
+
}
|
|
307
|
+
function jsonIssues(value, path = [], ancestors = /* @__PURE__ */ new Set()) {
|
|
308
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return [];
|
|
309
|
+
if (typeof value !== "object" || ancestors.has(value) || !Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return [{
|
|
310
|
+
path: pointer(path),
|
|
311
|
+
message: "Expected a JSON value"
|
|
312
|
+
}];
|
|
313
|
+
ancestors.add(value);
|
|
314
|
+
const issues = (Array.isArray(value) ? Array.from(value.entries()) : Object.entries(value)).flatMap(([key, child]) => jsonIssues(child, [...path, key], ancestors));
|
|
315
|
+
ancestors.delete(value);
|
|
316
|
+
return issues;
|
|
317
|
+
}
|
|
318
|
+
/** Runtime nominal check for values produced by an application-error descriptor. */
|
|
319
|
+
function isApplicationErrorValue(value) {
|
|
320
|
+
return typeof value === "object" && value !== null && value[applicationErrorBrand] === true;
|
|
321
|
+
}
|
|
322
|
+
function assertApplicationErrorCode(code) {
|
|
323
|
+
assertErrorCode(code);
|
|
324
|
+
if (code >= -32768 && code <= -32e3 || code === -32800) throw new Error(`Application error code ${code} is reserved by JSON-RPC or LinkRPC.`);
|
|
325
|
+
}
|
|
326
|
+
function assertErrorCode(code) {
|
|
327
|
+
if (!Number.isInteger(code) || code < -2147483648 || code > 2147483647) throw new Error(`Application error code ${code} must be a signed 32-bit integer.`);
|
|
328
|
+
}
|
|
329
|
+
var RequestType = class RequestType {
|
|
330
|
+
paramsSchema;
|
|
331
|
+
resultSchema;
|
|
332
|
+
errorSchema;
|
|
333
|
+
docs;
|
|
334
|
+
clientStreamSchema;
|
|
335
|
+
serverStreamSchema;
|
|
336
|
+
applicationErrors;
|
|
337
|
+
kind = "request";
|
|
338
|
+
constructor(paramsSchema, resultSchema, errorSchema, docs = {}, clientStreamSchema, serverStreamSchema, applicationErrors = []) {
|
|
339
|
+
this.paramsSchema = paramsSchema;
|
|
340
|
+
this.resultSchema = resultSchema;
|
|
341
|
+
this.errorSchema = errorSchema;
|
|
342
|
+
this.docs = docs;
|
|
343
|
+
this.clientStreamSchema = clientStreamSchema;
|
|
344
|
+
this.serverStreamSchema = serverStreamSchema;
|
|
345
|
+
this.applicationErrors = applicationErrors;
|
|
346
|
+
const seen = /* @__PURE__ */ new Set();
|
|
347
|
+
for (const error of applicationErrors) {
|
|
348
|
+
if (applicationErrors.some((other) => other !== error && other.code === error.code && (other.bodySchema !== void 0 || error.bodySchema !== void 0))) throw new Error(`Raw error code ${error.code} cannot be shared.`);
|
|
349
|
+
if (error.bodySchema !== void 0) {
|
|
350
|
+
assertErrorCode(error.code);
|
|
351
|
+
const key = `code:${error.code}`;
|
|
352
|
+
if (seen.has(key)) throw new Error(`Duplicate application error ${key}.`);
|
|
353
|
+
seen.add(key);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
assertApplicationErrorCode(error.code);
|
|
357
|
+
if (typeof error.message !== "string") throw new Error("Application error message must be a string.");
|
|
358
|
+
if (error.type !== void 0 && (typeof error.type !== "string" || error.type.length === 0)) throw new Error("Application error type must be a nonempty string.");
|
|
359
|
+
const key = error.type === void 0 ? `code:${error.code}` : `type:${error.type}`;
|
|
360
|
+
if (seen.has(key)) throw new Error(`Duplicate application error ${key}.`);
|
|
361
|
+
seen.add(key);
|
|
362
|
+
}
|
|
363
|
+
this.applicationErrors = Object.freeze([...applicationErrors]);
|
|
364
|
+
this.errors = this.applicationErrors;
|
|
365
|
+
}
|
|
366
|
+
/** Declared checked errors, retained as a tuple for generated-contract consumers. */
|
|
367
|
+
errors;
|
|
368
|
+
/**
|
|
369
|
+
* Return a copy of this request type with stream payload schemas
|
|
370
|
+
* attached. Pass `undefined` for either direction to leave it
|
|
371
|
+
* closed.
|
|
372
|
+
* `NoInfer` keeps a contextual MemberMap's `any` stream slots from
|
|
373
|
+
* widening undeclared directions; only the supplied schemas infer payloads.
|
|
374
|
+
*/
|
|
375
|
+
withStream(opts) {
|
|
376
|
+
return new RequestType(this.paramsSchema, this.resultSchema, this.errorSchema, this.docs, opts.client, opts.server, this.applicationErrors);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Return a copy with an explicit, typed set of checked application errors.
|
|
380
|
+
* The legacy third `requestType` schema remains accepted for compatibility,
|
|
381
|
+
* but does not opt a method into checked errors.
|
|
382
|
+
*/
|
|
383
|
+
withErrors(errors) {
|
|
384
|
+
return new RequestType(this.paramsSchema, this.resultSchema, this.errorSchema, this.docs, this.clientStreamSchema, this.serverStreamSchema, errors);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
var NotificationType = class {
|
|
388
|
+
paramsSchema;
|
|
389
|
+
docs;
|
|
390
|
+
kind = "notification";
|
|
391
|
+
constructor(paramsSchema, docs = {}) {
|
|
392
|
+
this.paramsSchema = paramsSchema;
|
|
393
|
+
this.docs = docs;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Define a request method.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* bar: requestType(z.object({ to: z.string() }), z.string(), {
|
|
401
|
+
* description: "MUST resolve before the next call from the same caller.",
|
|
402
|
+
* })
|
|
403
|
+
*/
|
|
404
|
+
function requestType(params, result, docsOrError, maybeDocs) {
|
|
405
|
+
const docs = {};
|
|
406
|
+
return new RequestType(params, result ?? _void(), _void(), docs);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Define a notification method.
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* foo: notificationType(z.object({ message: z.string() }))
|
|
413
|
+
*/
|
|
414
|
+
function notificationType(params, docs = {}) {
|
|
415
|
+
return new NotificationType(params, docs);
|
|
416
|
+
}
|
|
417
|
+
/** Convert a zod schema to our restricted SvcJsonSchema subset. */
|
|
418
|
+
function zodToSvcJsonSchema(schema, options) {
|
|
419
|
+
const source = schemaSources.get(schema);
|
|
420
|
+
if (source !== void 0) {
|
|
421
|
+
for (const [name, component] of Object.entries(source.components)) {
|
|
422
|
+
if (options === void 0) throw new Error("Imported schema references require a component destination");
|
|
423
|
+
addComponent(options.components, name, normalizeJsonSchema(component));
|
|
424
|
+
}
|
|
425
|
+
return normalizeJsonSchema(source.schema);
|
|
426
|
+
}
|
|
427
|
+
const type = schema._zod?.def?.type;
|
|
428
|
+
if (type === "void" || type === "undefined") return true;
|
|
429
|
+
return normalizeZodJsonSchema(toJSONSchema(schema, { override: (context) => {
|
|
430
|
+
const name = options?.parameterNames?.get(context.zodSchema);
|
|
431
|
+
if (name !== void 0) {
|
|
432
|
+
for (const key of Object.keys(context.jsonSchema)) delete context.jsonSchema[key];
|
|
433
|
+
context.jsonSchema.$ref = `linkrpc-parameter:${encodeURIComponent(name)}`;
|
|
434
|
+
} else if (options?.parameterNames !== void 0) {
|
|
435
|
+
for (const key of [
|
|
436
|
+
"allOf",
|
|
437
|
+
"patternProperties",
|
|
438
|
+
"unevaluatedProperties",
|
|
439
|
+
"if",
|
|
440
|
+
"then",
|
|
441
|
+
"else",
|
|
442
|
+
"contains"
|
|
443
|
+
]) if (key in context.jsonSchema) throw new Error(`Unsupported template JSON Schema keyword "${key}"`);
|
|
444
|
+
}
|
|
445
|
+
} }), options);
|
|
446
|
+
}
|
|
447
|
+
function normalizeZodJsonSchema(raw, options) {
|
|
448
|
+
if (!isRecord(raw)) return normalizeJsonSchema(raw);
|
|
449
|
+
const definitions = isRecord(raw.$defs) ? raw.$defs : {};
|
|
450
|
+
const localRefs = collectLocalRefs(raw);
|
|
451
|
+
if (Object.keys(definitions).length === 0 && localRefs.size === 0) return normalizeJsonSchema(raw);
|
|
452
|
+
if (options === void 0) throw new Error("zodToSvcJsonSchema: local references require a component destination");
|
|
453
|
+
const componentNamePrefix = `method=${encodeComponentNamePart(options.methodName)}&schema=${encodeComponentNamePart(options.schemaPosition)}`;
|
|
454
|
+
const definitionComponents = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const definitionName of Object.keys(definitions)) definitionComponents.set(definitionName, `${componentNamePrefix}&def=${encodeComponentNamePart(definitionName)}`);
|
|
456
|
+
const rootComponent = localRefs.has("#") ? `${componentNamePrefix}&root` : void 0;
|
|
457
|
+
const rewrite = (value) => rewriteLocalRefs(value, definitionComponents, rootComponent);
|
|
458
|
+
for (const [definitionName, definition] of Object.entries(definitions)) {
|
|
459
|
+
const componentName = definitionComponents.get(definitionName);
|
|
460
|
+
if (componentName === void 0) throw new Error(`zodToSvcJsonSchema: missing component for "${definitionName}"`);
|
|
461
|
+
addComponent(options.components, componentName, normalizeJsonSchema(rewrite(definition)));
|
|
462
|
+
}
|
|
463
|
+
const root = rewrite(raw);
|
|
464
|
+
if (rootComponent !== void 0) {
|
|
465
|
+
addComponent(options.components, rootComponent, normalizeJsonSchema(root));
|
|
466
|
+
return { $ref: componentRef(rootComponent) };
|
|
467
|
+
}
|
|
468
|
+
return normalizeJsonSchema(root);
|
|
469
|
+
}
|
|
470
|
+
function collectLocalRefs(value, refs = /* @__PURE__ */ new Set()) {
|
|
471
|
+
if (Array.isArray(value)) {
|
|
472
|
+
for (const child of value) collectLocalRefs(child, refs);
|
|
473
|
+
return refs;
|
|
474
|
+
}
|
|
475
|
+
if (!isRecord(value)) return refs;
|
|
476
|
+
if (typeof value.$ref === "string" && value.$ref.startsWith("#")) refs.add(value.$ref);
|
|
477
|
+
for (const child of Object.values(value)) collectLocalRefs(child, refs);
|
|
478
|
+
return refs;
|
|
479
|
+
}
|
|
480
|
+
function rewriteLocalRefs(value, definitionComponents, rootComponent) {
|
|
481
|
+
if (Array.isArray(value)) return value.map((child) => rewriteLocalRefs(child, definitionComponents, rootComponent));
|
|
482
|
+
if (!isRecord(value)) return value;
|
|
483
|
+
const result = {};
|
|
484
|
+
for (const [key, child] of Object.entries(value)) {
|
|
485
|
+
if (key === "$defs") continue;
|
|
486
|
+
if (key === "$ref" && typeof child === "string") result[key] = rewriteLocalRef(child, definitionComponents, rootComponent);
|
|
487
|
+
else result[key] = rewriteLocalRefs(child, definitionComponents, rootComponent);
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
function rewriteLocalRef(ref, definitionComponents, rootComponent) {
|
|
492
|
+
if (ref === "#") {
|
|
493
|
+
if (rootComponent === void 0) throw new Error("zodToSvcJsonSchema: root reference has no component destination");
|
|
494
|
+
return componentRef(rootComponent);
|
|
495
|
+
}
|
|
496
|
+
if (ref.startsWith("#/$defs/")) {
|
|
497
|
+
const encodedName = ref.slice(8);
|
|
498
|
+
const definitionName = [...definitionComponents.keys()].find((name) => encodeJsonPointerSegment(name) === encodedName);
|
|
499
|
+
const componentName = definitionName === void 0 ? void 0 : definitionComponents.get(definitionName);
|
|
500
|
+
if (componentName === void 0) throw new Error(`zodToSvcJsonSchema: dangling local reference "${ref}"`);
|
|
501
|
+
return componentRef(componentName);
|
|
502
|
+
}
|
|
503
|
+
if (ref.startsWith("#") && !ref.startsWith("#/components/schemas/")) throw new Error(`zodToSvcJsonSchema: unsupported local reference "${ref}"`);
|
|
504
|
+
return ref;
|
|
505
|
+
}
|
|
506
|
+
function addComponent(components, name, schema) {
|
|
507
|
+
if (Object.hasOwn(components, name)) throw new Error(`zodToSvcJsonSchema: duplicate component name "${name}"`);
|
|
508
|
+
components[name] = schema;
|
|
509
|
+
}
|
|
510
|
+
function componentRef(name) {
|
|
511
|
+
return `#/components/schemas/${encodeJsonPointerSegment(name)}`;
|
|
512
|
+
}
|
|
513
|
+
function encodeJsonPointerSegment(value) {
|
|
514
|
+
return value.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
515
|
+
}
|
|
516
|
+
function encodeComponentNamePart(value) {
|
|
517
|
+
return encodeURIComponent(value).replace(/~/g, "%7E");
|
|
518
|
+
}
|
|
519
|
+
function isRecord(value) {
|
|
520
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/schema/schemaValidationError.ts
|
|
524
|
+
/** Invalid reflected schema data, as distinct from an unexpected implementation failure. */
|
|
525
|
+
var SchemaValidationError = class extends Error {
|
|
526
|
+
name = "SchemaValidationError";
|
|
527
|
+
};
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/schema/assertSchemaReferences.ts
|
|
530
|
+
const componentPrefix = "#/components/schemas/";
|
|
531
|
+
function componentSchemaRef(name) {
|
|
532
|
+
return componentPrefix + name.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
533
|
+
}
|
|
534
|
+
function componentSchemaName(ref) {
|
|
535
|
+
if (!ref.startsWith(componentPrefix)) throw new SchemaValidationError(`Invalid component reference: ${ref}`);
|
|
536
|
+
const token = ref.slice(21);
|
|
537
|
+
if (token.includes("/") || /~(?:[^01]|$)/.test(token)) throw new SchemaValidationError(`Invalid component reference token: ${ref}`);
|
|
538
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/protocol/jcs.ts
|
|
542
|
+
/**
|
|
543
|
+
* RFC 8785 JSON Canonicalization Scheme (JCS).
|
|
544
|
+
*
|
|
545
|
+
* Used as the byte-deterministic encoding under every linkrpc signature
|
|
546
|
+
* (RPC calls, capabilities, hub-signed previews). Both signer and verifier
|
|
547
|
+
* canonicalize the same value object and obtain byte-identical UTF-8 bytes.
|
|
548
|
+
*
|
|
549
|
+
* Implementation inlined from the `canonicalize` npm package (Apache-2.0,
|
|
550
|
+
* https://github.com/erdtman/canonicalize) so this module stays
|
|
551
|
+
* zero-dependency for browser bundlers that don't resolve transitive
|
|
552
|
+
* npm specifiers (e.g. the in-house app-bundler).
|
|
553
|
+
*/
|
|
554
|
+
/** RFC 8785 canonical JSON string for `value`. */
|
|
555
|
+
function jcsCanonicalize(value) {
|
|
556
|
+
const out = _canonicalize(value);
|
|
557
|
+
if (out === void 0) throw new Error("jcsCanonicalize: value is not JSON-representable");
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
new TextEncoder();
|
|
561
|
+
function _canonicalize(object, seen = /* @__PURE__ */ new Set()) {
|
|
562
|
+
if (typeof object === "number" && Number.isNaN(object)) throw new Error("NaN is not allowed");
|
|
563
|
+
if (typeof object === "number" && !Number.isFinite(object)) throw new Error("Infinity is not allowed");
|
|
564
|
+
if (object === null || typeof object !== "object") return JSON.stringify(object);
|
|
565
|
+
const obj = object;
|
|
566
|
+
if (typeof obj.toJSON === "function") {
|
|
567
|
+
if (seen.has(obj)) throw new Error("Circular reference detected");
|
|
568
|
+
seen.add(obj);
|
|
569
|
+
const result = _canonicalize(obj.toJSON(), seen);
|
|
570
|
+
seen.delete(obj);
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
if (seen.has(obj)) throw new Error("Circular reference detected");
|
|
574
|
+
seen.add(obj);
|
|
575
|
+
let result;
|
|
576
|
+
if (Array.isArray(obj)) result = `[${obj.map((cv) => {
|
|
577
|
+
return _canonicalize(cv === void 0 || typeof cv === "symbol" ? null : cv, seen);
|
|
578
|
+
}).join(",")}]`;
|
|
579
|
+
else {
|
|
580
|
+
const parts = [];
|
|
581
|
+
for (const key of Object.keys(obj).sort()) {
|
|
582
|
+
const v = obj[key];
|
|
583
|
+
if (v === void 0 || typeof v === "symbol") continue;
|
|
584
|
+
parts.push(`${JSON.stringify(key)}:${_canonicalize(v, seen)}`);
|
|
585
|
+
}
|
|
586
|
+
result = `{${parts.join(",")}}`;
|
|
587
|
+
}
|
|
588
|
+
seen.delete(obj);
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/crypto/sha256.ts
|
|
593
|
+
/**
|
|
594
|
+
* Pure-JS SHA-256 (RFC 6234 / FIPS 180-4). Synchronous, dependency-free,
|
|
595
|
+
* isomorphic. Used internally by linkrpc for content-hashing interface
|
|
596
|
+
* schemas and deriving identity slot names; also surfaced through the
|
|
597
|
+
* `crypto` API as `crypto.sha256`. For signature work we go through the
|
|
598
|
+
* `crypto` module (`./identity/crypto`).
|
|
599
|
+
*
|
|
600
|
+
* Replaces the previous `@noble/hashes/sha2` import so the bundle has
|
|
601
|
+
* no runtime npm dependencies. Trades the well-vetted dep for ~80 lines
|
|
602
|
+
* of straight-line code that has no branching on input contents and is
|
|
603
|
+
* exercised by the existing test vectors plus `sha256.test.ts`.
|
|
604
|
+
*/
|
|
605
|
+
const K = new Uint32Array([
|
|
606
|
+
1116352408,
|
|
607
|
+
1899447441,
|
|
608
|
+
3049323471,
|
|
609
|
+
3921009573,
|
|
610
|
+
961987163,
|
|
611
|
+
1508970993,
|
|
612
|
+
2453635748,
|
|
613
|
+
2870763221,
|
|
614
|
+
3624381080,
|
|
615
|
+
310598401,
|
|
616
|
+
607225278,
|
|
617
|
+
1426881987,
|
|
618
|
+
1925078388,
|
|
619
|
+
2162078206,
|
|
620
|
+
2614888103,
|
|
621
|
+
3248222580,
|
|
622
|
+
3835390401,
|
|
623
|
+
4022224774,
|
|
624
|
+
264347078,
|
|
625
|
+
604807628,
|
|
626
|
+
770255983,
|
|
627
|
+
1249150122,
|
|
628
|
+
1555081692,
|
|
629
|
+
1996064986,
|
|
630
|
+
2554220882,
|
|
631
|
+
2821834349,
|
|
632
|
+
2952996808,
|
|
633
|
+
3210313671,
|
|
634
|
+
3336571891,
|
|
635
|
+
3584528711,
|
|
636
|
+
113926993,
|
|
637
|
+
338241895,
|
|
638
|
+
666307205,
|
|
639
|
+
773529912,
|
|
640
|
+
1294757372,
|
|
641
|
+
1396182291,
|
|
642
|
+
1695183700,
|
|
643
|
+
1986661051,
|
|
644
|
+
2177026350,
|
|
645
|
+
2456956037,
|
|
646
|
+
2730485921,
|
|
647
|
+
2820302411,
|
|
648
|
+
3259730800,
|
|
649
|
+
3345764771,
|
|
650
|
+
3516065817,
|
|
651
|
+
3600352804,
|
|
652
|
+
4094571909,
|
|
653
|
+
275423344,
|
|
654
|
+
430227734,
|
|
655
|
+
506948616,
|
|
656
|
+
659060556,
|
|
657
|
+
883997877,
|
|
658
|
+
958139571,
|
|
659
|
+
1322822218,
|
|
660
|
+
1537002063,
|
|
661
|
+
1747873779,
|
|
662
|
+
1955562222,
|
|
663
|
+
2024104815,
|
|
664
|
+
2227730452,
|
|
665
|
+
2361852424,
|
|
666
|
+
2428436474,
|
|
667
|
+
2756734187,
|
|
668
|
+
3204031479,
|
|
669
|
+
3329325298
|
|
670
|
+
]);
|
|
671
|
+
function _rotr(x, n) {
|
|
672
|
+
return x >>> n | x << 32 - n;
|
|
673
|
+
}
|
|
674
|
+
function sha256(data) {
|
|
675
|
+
const inputLen = data.length;
|
|
676
|
+
const bitLen = inputLen * 8;
|
|
677
|
+
const paddedLen = inputLen + 9 + 63 & -64;
|
|
678
|
+
const padded = new Uint8Array(paddedLen);
|
|
679
|
+
padded.set(data);
|
|
680
|
+
padded[inputLen] = 128;
|
|
681
|
+
const view = new DataView(padded.buffer);
|
|
682
|
+
view.setUint32(paddedLen - 8, Math.floor(bitLen / 4294967296), false);
|
|
683
|
+
view.setUint32(paddedLen - 4, bitLen >>> 0, false);
|
|
684
|
+
const H = new Uint32Array([
|
|
685
|
+
1779033703,
|
|
686
|
+
3144134277,
|
|
687
|
+
1013904242,
|
|
688
|
+
2773480762,
|
|
689
|
+
1359893119,
|
|
690
|
+
2600822924,
|
|
691
|
+
528734635,
|
|
692
|
+
1541459225
|
|
693
|
+
]);
|
|
694
|
+
const W = /* @__PURE__ */ new Uint32Array(64);
|
|
695
|
+
for (let block = 0; block < paddedLen; block += 64) {
|
|
696
|
+
for (let t = 0; t < 16; t++) W[t] = view.getUint32(block + t * 4, false);
|
|
697
|
+
for (let t = 16; t < 64; t++) {
|
|
698
|
+
const w15 = W[t - 15];
|
|
699
|
+
const w2 = W[t - 2];
|
|
700
|
+
const s0 = _rotr(w15, 7) ^ _rotr(w15, 18) ^ w15 >>> 3;
|
|
701
|
+
const s1 = _rotr(w2, 17) ^ _rotr(w2, 19) ^ w2 >>> 10;
|
|
702
|
+
W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
|
|
703
|
+
}
|
|
704
|
+
let a = H[0], b = H[1], c = H[2], d = H[3];
|
|
705
|
+
let e = H[4], f = H[5], g = H[6], h = H[7];
|
|
706
|
+
for (let t = 0; t < 64; t++) {
|
|
707
|
+
const S1 = _rotr(e, 6) ^ _rotr(e, 11) ^ _rotr(e, 25);
|
|
708
|
+
const ch = e & f ^ ~e & g;
|
|
709
|
+
const temp1 = h + S1 + ch + K[t] + W[t] >>> 0;
|
|
710
|
+
const temp2 = (_rotr(a, 2) ^ _rotr(a, 13) ^ _rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
711
|
+
h = g;
|
|
712
|
+
g = f;
|
|
713
|
+
f = e;
|
|
714
|
+
e = d + temp1 >>> 0;
|
|
715
|
+
d = c;
|
|
716
|
+
c = b;
|
|
717
|
+
b = a;
|
|
718
|
+
a = temp1 + temp2 >>> 0;
|
|
719
|
+
}
|
|
720
|
+
H[0] = H[0] + a >>> 0;
|
|
721
|
+
H[1] = H[1] + b >>> 0;
|
|
722
|
+
H[2] = H[2] + c >>> 0;
|
|
723
|
+
H[3] = H[3] + d >>> 0;
|
|
724
|
+
H[4] = H[4] + e >>> 0;
|
|
725
|
+
H[5] = H[5] + f >>> 0;
|
|
726
|
+
H[6] = H[6] + g >>> 0;
|
|
727
|
+
H[7] = H[7] + h >>> 0;
|
|
728
|
+
}
|
|
729
|
+
const out = /* @__PURE__ */ new Uint8Array(32);
|
|
730
|
+
const outView = new DataView(out.buffer);
|
|
731
|
+
for (let i = 0; i < 8; i++) outView.setUint32(i * 4, H[i], false);
|
|
732
|
+
return out;
|
|
733
|
+
}
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/schema/hash.ts
|
|
736
|
+
const methodSchemaFields = [
|
|
737
|
+
"params",
|
|
738
|
+
"result",
|
|
739
|
+
"clientStream",
|
|
740
|
+
"serverStream"
|
|
741
|
+
];
|
|
742
|
+
/**
|
|
743
|
+
* Compute the interface hash: SHA-256 of the canonicalized schema, truncated
|
|
744
|
+
* to 16 hex chars (64 bits of collision budget per id).
|
|
745
|
+
*
|
|
746
|
+
* Canonicalization strips the schema down to its **normative wire-contract
|
|
747
|
+
* projection** before hashing, so a single interface document can carry richer
|
|
748
|
+
* non-normative material (codegen hints, safety expressions, notes) without
|
|
749
|
+
* changing identity:
|
|
750
|
+
*
|
|
751
|
+
* 1. Normalize every JSON-Schema position (`params`, `result`, streams, and
|
|
752
|
+
* `components.schemas`) onto the decidable linkrpc subset.
|
|
753
|
+
* 2. Strip every `comment` field (non-normative — must not affect identity).
|
|
754
|
+
* `description` is NORMATIVE and kept in the hash.
|
|
755
|
+
* 3. Strip every **specification-extension** field — any object key whose name
|
|
756
|
+
* begins with `x-` — at every level of the document (see
|
|
757
|
+
* {@link EXTENSION_PREFIX}). This is the minimal, explicit "one document,
|
|
758
|
+
* two views" mechanism: the stored document keeps the rich `x-…`
|
|
759
|
+
* expressions; identity hashes only the simple contract. It mirrors
|
|
760
|
+
* OpenRPC/OpenAPI specification extensions (this schema format is a subset
|
|
761
|
+
* of OpenRPC 1.x). Editing an `x-…` value never changes the hash; changing
|
|
762
|
+
* a wire field (`params`, `result`, member names, `type`, `required`,
|
|
763
|
+
* `description`, `annotations`, …) does.
|
|
764
|
+
* 4. Omit the top-level `hash` field itself.
|
|
765
|
+
* 5. RFC 8785 JCS encode (recursive key sort, no whitespace) via {@link jcsCanonicalize}.
|
|
766
|
+
*
|
|
767
|
+
* Because no interface schema uses `x-…` keys today, this preserves every
|
|
768
|
+
* existing hash: stripping a set of keys that are always absent is a no-op.
|
|
769
|
+
*
|
|
770
|
+
* > Reservation. `x-…` is reserved for non-normative extensions at every level.
|
|
771
|
+
* > Member names cannot collide (they are alphanumeric per chapter 01 §2), and
|
|
772
|
+
* > object property names in the JSON Schema subset MUST NOT begin with `x-`.
|
|
773
|
+
*/
|
|
774
|
+
function computeInterfaceHash(schema) {
|
|
775
|
+
const json = jcsCanonicalize(stripNonNormative(normalizeSchemaPositions(schema), true));
|
|
776
|
+
const digest = sha256(new TextEncoder().encode(json));
|
|
777
|
+
return Array.from(digest.subarray(0, 8), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
778
|
+
}
|
|
779
|
+
function normalizeSchemaPositions(schema) {
|
|
780
|
+
const methods = Object.fromEntries(Object.entries(schema.methods).map(([name, method]) => {
|
|
781
|
+
const normalized = { ...method };
|
|
782
|
+
for (const field of methodSchemaFields) {
|
|
783
|
+
const value = normalized[field];
|
|
784
|
+
if (value !== void 0) normalized[field] = normalizeJsonSchema(value);
|
|
785
|
+
}
|
|
786
|
+
if (method.errors !== void 0) normalized.errors = method.errors.map((error) => error.schema !== void 0 ? {
|
|
787
|
+
...error,
|
|
788
|
+
schema: normalizeJsonSchema(error.schema)
|
|
789
|
+
} : {
|
|
790
|
+
...error,
|
|
791
|
+
...error.data === void 0 ? {} : { data: normalizeJsonSchema(error.data) }
|
|
792
|
+
});
|
|
793
|
+
return [name, normalized];
|
|
794
|
+
}));
|
|
795
|
+
const componentSchemas = schema.components?.schemas;
|
|
796
|
+
const components = componentSchemas === void 0 ? schema.components : {
|
|
797
|
+
...schema.components,
|
|
798
|
+
schemas: Object.fromEntries(Object.entries(componentSchemas).map(([name, value]) => [name, normalizeJsonSchema(value)]))
|
|
799
|
+
};
|
|
800
|
+
return {
|
|
801
|
+
...schema,
|
|
802
|
+
methods,
|
|
803
|
+
...components === void 0 ? {} : { components }
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/** True for a key that must not contribute to the interface hash. */
|
|
807
|
+
function isNonNormativeKey(key, isRoot) {
|
|
808
|
+
return key === "comment" || key.startsWith("x-") || isRoot && (key === "hash" || key === "tags");
|
|
809
|
+
}
|
|
810
|
+
function stripNonNormative(value, isRoot = false) {
|
|
811
|
+
if (value === null || typeof value !== "object") return value;
|
|
812
|
+
if (Array.isArray(value)) return value.map((v) => stripNonNormative(v));
|
|
813
|
+
const out = {};
|
|
814
|
+
for (const k of Object.keys(value)) {
|
|
815
|
+
if (isNonNormativeKey(k, isRoot)) continue;
|
|
816
|
+
const v = value[k];
|
|
817
|
+
if (v === void 0) continue;
|
|
818
|
+
out[k] = stripNonNormative(v);
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
823
|
+
//#region src/schema/interfaceTemplates.ts
|
|
824
|
+
const INTERFACE_TEMPLATES_EXTENSION = "x-interface-templates";
|
|
825
|
+
/** Attach structural metadata to an already lowered schema, without rebuilding a definition. */
|
|
826
|
+
function attachInterfaceTemplates(schema, mapped) {
|
|
827
|
+
const templates = Object.create(null);
|
|
828
|
+
const instances = [];
|
|
829
|
+
for (const [name, instance] of Object.entries(mapped)) {
|
|
830
|
+
const priorTemplate = templates[instance.template.id];
|
|
831
|
+
if (priorTemplate !== void 0 && !interfaceTemplatesEqual(priorTemplate, instance.template)) throw new Error(`Conflicting definitions for interface template "${instance.template.id}"`);
|
|
832
|
+
templates[instance.template.id] = priorTemplate === void 0 ? instance.template : {
|
|
833
|
+
...priorTemplate,
|
|
834
|
+
tags: normalizeInterfaceTags([...priorTemplate.tags ?? [], ...instance.template.tags ?? []])
|
|
835
|
+
};
|
|
836
|
+
instances.push({
|
|
837
|
+
...instance.schema,
|
|
838
|
+
name
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
const tags = normalizeInterfaceTags([...schema.tags ?? [], ...instances.flatMap((instance) => templates[instance.template].tags ?? [])]);
|
|
842
|
+
return {
|
|
843
|
+
...schema,
|
|
844
|
+
...schema.tags !== void 0 || tags.length > 0 ? { tags } : {},
|
|
845
|
+
[INTERFACE_TEMPLATES_EXTENSION]: {
|
|
846
|
+
templates,
|
|
847
|
+
instances
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
/** Stable, deduplicated discovery labels for a schema and its instantiated templates. */
|
|
852
|
+
function getInterfaceTags(schema) {
|
|
853
|
+
validateInterfaceTemplates(schema);
|
|
854
|
+
const metadata = schema[INTERFACE_TEMPLATES_EXTENSION];
|
|
855
|
+
return normalizeInterfaceTags([...schema.tags ?? [], ...(metadata?.instances ?? []).flatMap((instance) => metadata?.templates[instance.template]?.tags ?? [])]);
|
|
856
|
+
}
|
|
857
|
+
function normalizeInterfaceTags(tags) {
|
|
858
|
+
return [...new Set(tags)].sort();
|
|
859
|
+
}
|
|
860
|
+
function validateInterfaceTemplates(schema) {
|
|
861
|
+
validateTags(schema.tags, `Interface "${schema.id}"`);
|
|
862
|
+
const raw = schema[INTERFACE_TEMPLATES_EXTENSION];
|
|
863
|
+
if (raw === void 0) return;
|
|
864
|
+
if (!_isRecord(raw) || !_isRecord(raw.templates) || !Array.isArray(raw.instances)) throw new SchemaValidationError(`${INTERFACE_TEMPLATES_EXTENSION} must contain templates and instances`);
|
|
865
|
+
const templates = raw.templates;
|
|
866
|
+
for (const [id, template] of Object.entries(templates)) {
|
|
867
|
+
if (!_isRecord(template) || template.id !== id) throw new SchemaValidationError(`Interface template key "${id}" must match its id`);
|
|
868
|
+
validateInterfaceTemplate(template);
|
|
869
|
+
}
|
|
870
|
+
const occupied = /* @__PURE__ */ new Map();
|
|
871
|
+
const names = /* @__PURE__ */ new Set();
|
|
872
|
+
for (const value of raw.instances) {
|
|
873
|
+
if (!_isRecord(value)) throw new SchemaValidationError("Interface template instance must be an object");
|
|
874
|
+
if (typeof value.name !== "string") throw new SchemaValidationError("Interface template instance name must be a string");
|
|
875
|
+
if (typeof value.template !== "string") throw new SchemaValidationError("Interface template reference must be a string");
|
|
876
|
+
if (value.prefix !== void 0) throw new SchemaValidationError("Interface template instances require explicit member mappings, not prefixes");
|
|
877
|
+
if (!_isRecord(value.members)) throw new SchemaValidationError("Interface template members must be an object");
|
|
878
|
+
if (!_isRecord(value.arguments)) throw new SchemaValidationError("Interface template arguments must be an object");
|
|
879
|
+
const instance = {
|
|
880
|
+
name: value.name,
|
|
881
|
+
template: value.template,
|
|
882
|
+
members: value.members,
|
|
883
|
+
arguments: value.arguments
|
|
884
|
+
};
|
|
885
|
+
_assertSegment(instance.name, "interface template instance name");
|
|
886
|
+
if (names.has(instance.name)) throw new SchemaValidationError(`Interface template instance name collision at "${instance.name}"`);
|
|
887
|
+
names.add(instance.name);
|
|
888
|
+
const template = templates[instance.template];
|
|
889
|
+
if (!Object.hasOwn(templates, instance.template)) throw new SchemaValidationError(`Interface template instance "${instance.name}" references unknown template "${instance.template}"`);
|
|
890
|
+
_validateArguments(instance, template);
|
|
891
|
+
if (instance.members !== void 0) {
|
|
892
|
+
for (const name of Object.keys(instance.members)) if (!Object.hasOwn(template.methods, name)) throw new SchemaValidationError(`Unknown mapped member "${name}"`);
|
|
893
|
+
for (const name of Object.keys(template.methods)) {
|
|
894
|
+
const target = instance.members[name];
|
|
895
|
+
if (!Object.hasOwn(instance.members, name) || typeof target !== "string") throw new SchemaValidationError(`Missing mapped member "${name}"`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
_validateInstanceMethods(schema, instance, template, occupied);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Best-effort reflection selection, not template consumption or RPC validation.
|
|
903
|
+
* Keep a whole document; incompatible explanations and ties retain the first.
|
|
904
|
+
* Malformed optional metadata is unranked, while template-aware consumers still
|
|
905
|
+
* use validateInterfaceTemplates and report its errors normally.
|
|
906
|
+
*/
|
|
907
|
+
function preferRicherInterfaceSchema(existing, candidate) {
|
|
908
|
+
if (existing.id !== candidate.id || existing.hash !== candidate.hash) return existing;
|
|
909
|
+
if (computeInterfaceHash(existing) !== existing.hash || computeInterfaceHash(candidate) !== candidate.hash) return existing;
|
|
910
|
+
validateTags(existing.tags, `Interface "${existing.id}"`);
|
|
911
|
+
validateTags(candidate.tags, `Interface "${candidate.id}"`);
|
|
912
|
+
const next = _rankingMetadata(candidate);
|
|
913
|
+
const prior = _rankingMetadata(existing);
|
|
914
|
+
if (candidate["x-interface-templates"] !== void 0 && next === void 0) return existing;
|
|
915
|
+
if (existing["x-interface-templates"] !== void 0 && prior === void 0) return candidate;
|
|
916
|
+
if (prior !== void 0 && next === void 0) return existing;
|
|
917
|
+
if (next !== void 0 && prior !== void 0) {
|
|
918
|
+
for (const [id, template] of Object.entries(prior.templates)) if (!Object.hasOwn(next.templates, id) || !interfaceTemplatesEqual(template, next.templates[id])) return existing;
|
|
919
|
+
for (const instance of prior.instances) if (!next.instances.some((value) => _jsonEqual(instance, value))) return existing;
|
|
920
|
+
for (const [key, value] of Object.entries(prior)) if (key !== "templates" && key !== "instances" && !_jsonEqual(value, next[key])) return existing;
|
|
921
|
+
}
|
|
922
|
+
const existingTags = getInterfaceTags(existing);
|
|
923
|
+
const candidateTags = getInterfaceTags(candidate);
|
|
924
|
+
if (existingTags.some((tag) => !candidateTags.includes(tag))) return existing;
|
|
925
|
+
return prior === void 0 && next !== void 0 || prior !== void 0 && next !== void 0 && (Object.keys(next.templates).length > Object.keys(prior.templates).length || next.instances.length > prior.instances.length) || candidateTags.length > existingTags.length ? candidate : existing;
|
|
926
|
+
}
|
|
927
|
+
function _rankingMetadata(schema) {
|
|
928
|
+
if (schema["x-interface-templates"] === void 0) return void 0;
|
|
929
|
+
try {
|
|
930
|
+
validateInterfaceTemplates(schema);
|
|
931
|
+
} catch (error) {
|
|
932
|
+
if (!(error instanceof SchemaValidationError)) throw error;
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
return schema[INTERFACE_TEMPLATES_EXTENSION];
|
|
936
|
+
}
|
|
937
|
+
function validateInterfaceTemplate(template) {
|
|
938
|
+
if (!_isRecord(template) || typeof template.id !== "string" || template.id.length === 0) throw new SchemaValidationError("Interface template id must be a non-empty string");
|
|
939
|
+
if (!Array.isArray(template.parameters) || !_isRecord(template.methods)) throw new SchemaValidationError(`Interface template "${template.id}" must contain parameters and methods`);
|
|
940
|
+
validateTags(template.tags, `Interface template "${template.id}"`);
|
|
941
|
+
const parameters = /* @__PURE__ */ new Set();
|
|
942
|
+
for (const parameter of template.parameters) {
|
|
943
|
+
if (typeof parameter !== "string" || parameter.length === 0) throw new SchemaValidationError(`Interface template "${template.id}" has an invalid parameter name`);
|
|
944
|
+
if (parameters.has(parameter)) throw new SchemaValidationError(`Interface template "${template.id}" repeats parameter "${parameter}"`);
|
|
945
|
+
parameters.add(parameter);
|
|
946
|
+
}
|
|
947
|
+
const components = _componentSchemas(template.components);
|
|
948
|
+
for (const [name, method] of Object.entries(template.methods)) {
|
|
949
|
+
_assertSegment(name, `member of interface template "${template.id}"`);
|
|
950
|
+
if (!_isRecord(method) || !Object.hasOwn(method, "params")) throw new SchemaValidationError(`Interface template "${template.id}" member "${name}" has no params`);
|
|
951
|
+
for (const field of _methodSchemaFields) {
|
|
952
|
+
const value = method[field];
|
|
953
|
+
if (field === "params" || value !== void 0) _validateTemplateSchema(value, parameters, components, /* @__PURE__ */ new Set());
|
|
954
|
+
}
|
|
955
|
+
if (method.errors !== void 0 && !Array.isArray(method.errors)) throw new SchemaValidationError("Interface template method errors must be an array");
|
|
956
|
+
for (const error of method.errors ?? []) {
|
|
957
|
+
if (!_isRecord(error)) throw new SchemaValidationError("Interface template method error must be an object");
|
|
958
|
+
if (error.schema !== void 0) _validateTemplateSchema(error.schema, parameters, components, /* @__PURE__ */ new Set());
|
|
959
|
+
if (error.data !== void 0) _validateTemplateSchema(error.data, parameters, components, /* @__PURE__ */ new Set());
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
for (const component of Object.values(components)) _validateTemplateSchema(component, parameters, components, /* @__PURE__ */ new Set());
|
|
963
|
+
}
|
|
964
|
+
function _validateTemplateSchema(schema, parameters, components, seen) {
|
|
965
|
+
if (typeof schema === "boolean") return;
|
|
966
|
+
if (!_isRecord(schema)) throw new SchemaValidationError("Interface template schema must be an object or boolean");
|
|
967
|
+
if (seen.has(schema)) return;
|
|
968
|
+
seen.add(schema);
|
|
969
|
+
if ("$parameter" in schema) {
|
|
970
|
+
if (Object.keys(schema).length !== 1 || typeof schema.$parameter !== "string") throw new SchemaValidationError("A schema parameter reference must contain only $parameter");
|
|
971
|
+
if (!parameters.has(schema.$parameter)) throw new SchemaValidationError(`Unknown schema parameter "${schema.$parameter}"`);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if ("$ref" in schema) {
|
|
975
|
+
if (typeof schema.$ref !== "string") throw new SchemaValidationError("Component reference must be a string");
|
|
976
|
+
const name = componentSchemaName(schema.$ref);
|
|
977
|
+
if (!Object.hasOwn(components, name)) throw new SchemaValidationError(`Unknown interface template component "${name}"`);
|
|
978
|
+
}
|
|
979
|
+
for (const child of _schemaChildren(schema)) _validateTemplateSchema(child, parameters, components, seen);
|
|
980
|
+
}
|
|
981
|
+
function _validateArguments(instance, template) {
|
|
982
|
+
const expected = new Set(template.parameters);
|
|
983
|
+
for (const name of Object.keys(instance.arguments)) if (!expected.has(name)) throw new SchemaValidationError(`Interface template instance "${instance.name}" supplies unknown parameter "${name}"`);
|
|
984
|
+
for (const name of expected) {
|
|
985
|
+
const argument = instance.arguments[name];
|
|
986
|
+
if (!_isRecord(argument) || !Object.hasOwn(argument, "schema")) throw new SchemaValidationError(`Interface template instance "${instance.name}" is missing parameter "${name}"`);
|
|
987
|
+
const components = _componentSchemas(argument.components);
|
|
988
|
+
_validateConcreteSchema(argument.schema, components, /* @__PURE__ */ new Set());
|
|
989
|
+
for (const component of Object.values(components)) _validateConcreteSchema(component, components, /* @__PURE__ */ new Set());
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function _validateConcreteSchema(schema, components, seen) {
|
|
993
|
+
if (typeof schema === "boolean") return;
|
|
994
|
+
if (!_isRecord(schema)) throw new SchemaValidationError("Concrete schema argument must be an object or boolean");
|
|
995
|
+
if (seen.has(schema)) return;
|
|
996
|
+
seen.add(schema);
|
|
997
|
+
if ("$parameter" in schema) throw new SchemaValidationError("Concrete schema arguments cannot contain $parameter");
|
|
998
|
+
if ("$ref" in schema) {
|
|
999
|
+
if (typeof schema.$ref !== "string") throw new SchemaValidationError("Component reference must be a string");
|
|
1000
|
+
const name = componentSchemaName(schema.$ref);
|
|
1001
|
+
if (!Object.hasOwn(components, name)) throw new SchemaValidationError(`Unknown schema argument component "${name}"`);
|
|
1002
|
+
}
|
|
1003
|
+
for (const child of _schemaChildren(schema)) _validateConcreteSchema(child, components, seen);
|
|
1004
|
+
}
|
|
1005
|
+
function _validateInstanceMethods(schema, instance, template, occupied) {
|
|
1006
|
+
const expectedComponents = {};
|
|
1007
|
+
const instantiatedMethods = Object.fromEntries(Object.entries(template.methods).map(([name, method]) => [name, _instantiateMethod(method, template, instance, expectedComponents)]));
|
|
1008
|
+
const actualComponents = schema.components?.schemas ?? {};
|
|
1009
|
+
for (const [name, expected] of Object.entries(instantiatedMethods)) {
|
|
1010
|
+
const flattenedName = interfaceTemplateMemberName(instance, name);
|
|
1011
|
+
_assertSegment(flattenedName, `mapped member of interface template instance "${instance.name}"`);
|
|
1012
|
+
const prior = occupied.get(flattenedName);
|
|
1013
|
+
if (prior !== void 0) throw new SchemaValidationError(`Interface template member collision at "${flattenedName}" between "${prior}" and "${instance.name}"`);
|
|
1014
|
+
occupied.set(flattenedName, instance.name);
|
|
1015
|
+
const actual = schema.methods[flattenedName];
|
|
1016
|
+
if (actual === void 0) throw new SchemaValidationError(`Interface template instance "${instance.name}" is missing mapped member "${flattenedName}"`);
|
|
1017
|
+
if (!_methodsEquivalent(expected, actual, expectedComponents, actualComponents)) throw new SchemaValidationError(`Interface template instance "${instance.name}" member "${flattenedName}" does not match its instantiated template`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function interfaceTemplateMemberName(instance, member) {
|
|
1021
|
+
const mapped = instance.members[member];
|
|
1022
|
+
if (mapped !== void 0) return mapped;
|
|
1023
|
+
throw new Error(`Missing mapped member "${member}"`);
|
|
1024
|
+
}
|
|
1025
|
+
function _instantiateMethod(method, template, instance, components) {
|
|
1026
|
+
const instantiate = (value) => _instantiateSchema(value, template, instance, components, /* @__PURE__ */ new Map());
|
|
1027
|
+
const result = {
|
|
1028
|
+
...method,
|
|
1029
|
+
params: instantiate(method.params)
|
|
1030
|
+
};
|
|
1031
|
+
for (const field of _methodSchemaFields.slice(1)) {
|
|
1032
|
+
const value = method[field];
|
|
1033
|
+
if (value !== void 0) result[field] = instantiate(value);
|
|
1034
|
+
}
|
|
1035
|
+
if (method.errors !== void 0) result.errors = method.errors.map((error) => error.schema !== void 0 ? {
|
|
1036
|
+
code: error.code,
|
|
1037
|
+
schema: instantiate(error.schema)
|
|
1038
|
+
} : {
|
|
1039
|
+
code: error.code,
|
|
1040
|
+
message: error.message,
|
|
1041
|
+
...error.type === void 0 ? {} : { type: error.type },
|
|
1042
|
+
...error.data === void 0 ? {} : { data: instantiate(error.data) }
|
|
1043
|
+
});
|
|
1044
|
+
return result;
|
|
1045
|
+
}
|
|
1046
|
+
function _instantiateSchema(schema, template, instance, destination, copied) {
|
|
1047
|
+
if (typeof schema === "boolean") return schema;
|
|
1048
|
+
if ("$parameter" in schema) return _copyArgumentSchema(schema.$parameter, instance, destination, copied);
|
|
1049
|
+
return _rewriteSchema(schema, (name) => {
|
|
1050
|
+
const key = `template:${template.id}:${name}`;
|
|
1051
|
+
if (!Object.hasOwn(destination, key)) {
|
|
1052
|
+
const component = template.components?.schemas?.[name];
|
|
1053
|
+
if (component === void 0) throw new Error(`Unknown interface template component "${name}"`);
|
|
1054
|
+
destination[key] = false;
|
|
1055
|
+
destination[key] = _instantiateSchema(component, template, instance, destination, copied);
|
|
1056
|
+
}
|
|
1057
|
+
return key;
|
|
1058
|
+
}, (child) => _instantiateSchema(child, template, instance, destination, copied));
|
|
1059
|
+
}
|
|
1060
|
+
function _copyArgumentSchema(parameter, instance, destination, copied) {
|
|
1061
|
+
const argument = instance.arguments[parameter];
|
|
1062
|
+
if (argument === void 0) throw new Error(`Missing schema argument "${parameter}"`);
|
|
1063
|
+
const rewrite = (schema) => _rewriteSchema(schema, (name) => {
|
|
1064
|
+
const sourceKey = `${parameter}:${name}`;
|
|
1065
|
+
const existing = copied.get(sourceKey);
|
|
1066
|
+
if (existing !== void 0) return existing;
|
|
1067
|
+
const target = `argument:${instance.name}:${parameter}:${name}`;
|
|
1068
|
+
copied.set(sourceKey, target);
|
|
1069
|
+
const component = argument.components?.schemas?.[name];
|
|
1070
|
+
if (component === void 0) throw new Error(`Unknown schema argument component "${name}"`);
|
|
1071
|
+
destination[target] = false;
|
|
1072
|
+
destination[target] = rewrite(component);
|
|
1073
|
+
return target;
|
|
1074
|
+
}, (child) => rewrite(child));
|
|
1075
|
+
return rewrite(argument.schema);
|
|
1076
|
+
}
|
|
1077
|
+
function _rewriteSchema(schema, rewriteRef, rewriteChild) {
|
|
1078
|
+
if (typeof schema === "boolean") return schema;
|
|
1079
|
+
const result = {};
|
|
1080
|
+
for (const [key, value] of Object.entries(schema)) if (key === "$ref" && typeof value === "string") result.$ref = componentSchemaRef(rewriteRef(componentSchemaName(value)));
|
|
1081
|
+
else if (key === "properties" && _isRecord(value)) result.properties = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, rewriteChild(child)]));
|
|
1082
|
+
else if (key === "additionalProperties" || key === "items") result[key] = typeof value === "boolean" ? value : rewriteChild(value);
|
|
1083
|
+
else if (key === "prefixItems" || key === "anyOf" || key === "oneOf") result[key] = value.map(rewriteChild);
|
|
1084
|
+
else result[key] = value;
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
function _methodsEquivalent(expected, actual, expectedComponents, actualComponents) {
|
|
1088
|
+
const expectedRest = { ...expected };
|
|
1089
|
+
const actualRest = { ...actual };
|
|
1090
|
+
for (const field of _methodSchemaFields) {
|
|
1091
|
+
const expectedSchema = expected[field];
|
|
1092
|
+
const actualSchema = actual[field];
|
|
1093
|
+
delete expectedRest[field];
|
|
1094
|
+
delete actualRest[field];
|
|
1095
|
+
if (expectedSchema === void 0 || actualSchema === void 0) {
|
|
1096
|
+
if (expectedSchema !== actualSchema) return false;
|
|
1097
|
+
} else if (!_schemasEquivalent(expectedSchema, actualSchema, expectedComponents, actualComponents)) return false;
|
|
1098
|
+
}
|
|
1099
|
+
const expectedErrors = expectedRest.errors;
|
|
1100
|
+
const actualErrors = actualRest.errors;
|
|
1101
|
+
delete expectedRest.errors;
|
|
1102
|
+
delete actualRest.errors;
|
|
1103
|
+
if (!_jsonEqual(expectedRest, actualRest)) return false;
|
|
1104
|
+
if (expectedErrors === void 0 || actualErrors === void 0) return expectedErrors === actualErrors;
|
|
1105
|
+
if (expectedErrors.length !== actualErrors.length) return false;
|
|
1106
|
+
return expectedErrors.every((error, index) => {
|
|
1107
|
+
const candidate = actualErrors[index];
|
|
1108
|
+
if (error.schema !== void 0 || candidate.schema !== void 0) return error.schema !== void 0 && candidate.schema !== void 0 && error.code === candidate.code && _schemasEquivalent(error.schema, candidate.schema, expectedComponents, actualComponents);
|
|
1109
|
+
return error.code === candidate.code && error.type === candidate.type && error.message === candidate.message && (error.data === void 0 || candidate.data === void 0 ? error.data === candidate.data : _schemasEquivalent(error.data, candidate.data, expectedComponents, actualComponents));
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
function interfaceTemplatesEqual(left, right) {
|
|
1113
|
+
const { tags: _leftTags, ...leftStructure } = left;
|
|
1114
|
+
const { tags: _rightTags, ...rightStructure } = right;
|
|
1115
|
+
return _jsonEqual(leftStructure, rightStructure);
|
|
1116
|
+
}
|
|
1117
|
+
function validateTags(tags, context) {
|
|
1118
|
+
if (tags === void 0) return;
|
|
1119
|
+
if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string")) throw new SchemaValidationError(`${context} tags must be an array of strings`);
|
|
1120
|
+
}
|
|
1121
|
+
function _schemasEquivalent(left, right, leftComponents, rightComponents, seen = /* @__PURE__ */ new WeakMap()) {
|
|
1122
|
+
if (typeof left === "boolean" || typeof right === "boolean") return left === right;
|
|
1123
|
+
let rights = seen.get(left);
|
|
1124
|
+
if (rights?.has(right)) return true;
|
|
1125
|
+
if (rights === void 0) {
|
|
1126
|
+
rights = /* @__PURE__ */ new WeakSet();
|
|
1127
|
+
seen.set(left, rights);
|
|
1128
|
+
}
|
|
1129
|
+
rights.add(right);
|
|
1130
|
+
const leftRef = "$ref" in left ? left.$ref : void 0;
|
|
1131
|
+
const rightRef = "$ref" in right ? right.$ref : void 0;
|
|
1132
|
+
const leftOwn = leftRef === void 0 ? _toRecord(left) : _withoutRef(left);
|
|
1133
|
+
const rightOwn = rightRef === void 0 ? _toRecord(right) : _withoutRef(right);
|
|
1134
|
+
if (leftRef !== void 0 && Object.keys(leftOwn).length === 0) {
|
|
1135
|
+
const resolved = leftComponents[componentSchemaName(leftRef)];
|
|
1136
|
+
return resolved !== void 0 && _schemasEquivalent(resolved, right, leftComponents, rightComponents, seen);
|
|
1137
|
+
}
|
|
1138
|
+
if (rightRef !== void 0 && Object.keys(rightOwn).length === 0) {
|
|
1139
|
+
const resolved = rightComponents[componentSchemaName(rightRef)];
|
|
1140
|
+
return resolved !== void 0 && _schemasEquivalent(left, resolved, leftComponents, rightComponents, seen);
|
|
1141
|
+
}
|
|
1142
|
+
if (!_schemaObjectsEquivalent(leftOwn, rightOwn, leftComponents, rightComponents, seen)) return false;
|
|
1143
|
+
if (leftRef === void 0 || rightRef === void 0) {
|
|
1144
|
+
if (leftRef === rightRef) return true;
|
|
1145
|
+
const ref = leftRef ?? rightRef;
|
|
1146
|
+
const components = leftRef === void 0 ? rightComponents : leftComponents;
|
|
1147
|
+
const resolved = components[componentSchemaName(ref)];
|
|
1148
|
+
if (resolved === void 0) return false;
|
|
1149
|
+
return leftRef === void 0 ? _schemasEquivalent(left, resolved, leftComponents, components, seen) : _schemasEquivalent(resolved, right, components, rightComponents, seen);
|
|
1150
|
+
}
|
|
1151
|
+
const leftResolved = leftComponents[componentSchemaName(leftRef)];
|
|
1152
|
+
const rightResolved = rightComponents[componentSchemaName(rightRef)];
|
|
1153
|
+
return leftResolved !== void 0 && rightResolved !== void 0 && _schemasEquivalent(leftResolved, rightResolved, leftComponents, rightComponents, seen);
|
|
1154
|
+
}
|
|
1155
|
+
function _schemaObjectsEquivalent(left, right, leftComponents, rightComponents, seen) {
|
|
1156
|
+
const leftKeys = Object.keys(left).sort();
|
|
1157
|
+
if (!_jsonEqual(leftKeys, Object.keys(right).sort())) return false;
|
|
1158
|
+
for (const key of leftKeys) {
|
|
1159
|
+
const l = left[key];
|
|
1160
|
+
const r = right[key];
|
|
1161
|
+
if (key === "properties" && _isRecord(l) && _isRecord(r)) {
|
|
1162
|
+
const names = Object.keys(l).sort();
|
|
1163
|
+
if (!_jsonEqual(names, Object.keys(r).sort())) return false;
|
|
1164
|
+
if (!names.every((name) => _schemasEquivalent(l[name], r[name], leftComponents, rightComponents, seen))) return false;
|
|
1165
|
+
} else if ((key === "items" || key === "additionalProperties") && _isSchema(l) && _isSchema(r)) {
|
|
1166
|
+
if (!_schemasEquivalent(l, r, leftComponents, rightComponents, seen)) return false;
|
|
1167
|
+
} else if ((key === "prefixItems" || key === "anyOf" || key === "oneOf") && Array.isArray(l) && Array.isArray(r)) {
|
|
1168
|
+
if (l.length !== r.length || !l.every((value, index) => _schemasEquivalent(value, r[index], leftComponents, rightComponents, seen))) return false;
|
|
1169
|
+
} else if (!_jsonEqual(l, r)) return false;
|
|
1170
|
+
}
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
function _componentSchemas(components) {
|
|
1174
|
+
if (components === void 0) return {};
|
|
1175
|
+
if (!_isRecord(components) || components.schemas !== void 0 && !_isRecord(components.schemas)) throw new SchemaValidationError("Schema components and component schemas must be objects");
|
|
1176
|
+
return components.schemas ?? {};
|
|
1177
|
+
}
|
|
1178
|
+
function _schemaChildren(schema) {
|
|
1179
|
+
const result = [];
|
|
1180
|
+
if (schema.properties !== void 0) {
|
|
1181
|
+
if (!_isRecord(schema.properties)) throw new SchemaValidationError("Schema properties must be an object");
|
|
1182
|
+
result.push(...Object.values(schema.properties));
|
|
1183
|
+
}
|
|
1184
|
+
for (const key of ["items", "additionalProperties"]) {
|
|
1185
|
+
const child = schema[key];
|
|
1186
|
+
if (child !== void 0) {
|
|
1187
|
+
if (!_isSchema(child)) throw new SchemaValidationError(`Schema ${key} must be an object or boolean`);
|
|
1188
|
+
result.push(child);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
for (const key of [
|
|
1192
|
+
"prefixItems",
|
|
1193
|
+
"anyOf",
|
|
1194
|
+
"oneOf"
|
|
1195
|
+
]) {
|
|
1196
|
+
const children = schema[key];
|
|
1197
|
+
if (children !== void 0) {
|
|
1198
|
+
if (!Array.isArray(children)) throw new SchemaValidationError(`Schema ${key} must be an array`);
|
|
1199
|
+
result.push(...children);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return result;
|
|
1203
|
+
}
|
|
1204
|
+
const _methodSchemaFields = [
|
|
1205
|
+
"params",
|
|
1206
|
+
"result",
|
|
1207
|
+
"clientStream",
|
|
1208
|
+
"serverStream"
|
|
1209
|
+
];
|
|
1210
|
+
const _segmentPattern = /^[A-Za-z0-9!#$%&'()*+,\-;=?@\[\]^_`{|}~]+$/;
|
|
1211
|
+
function _assertSegment(value, label) {
|
|
1212
|
+
if (!_segmentPattern.test(value)) throw new SchemaValidationError(`Invalid ${label} "${value}"`);
|
|
1213
|
+
}
|
|
1214
|
+
function _withoutRef(schema) {
|
|
1215
|
+
const result = _toRecord(schema);
|
|
1216
|
+
delete result.$ref;
|
|
1217
|
+
return result;
|
|
1218
|
+
}
|
|
1219
|
+
function _toRecord(value) {
|
|
1220
|
+
return Object.fromEntries(Object.entries(value));
|
|
1221
|
+
}
|
|
1222
|
+
function _isSchema(value) {
|
|
1223
|
+
return typeof value === "boolean" || _isRecord(value);
|
|
1224
|
+
}
|
|
1225
|
+
function _isRecord(value) {
|
|
1226
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1227
|
+
}
|
|
1228
|
+
function _jsonEqual(left, right) {
|
|
1229
|
+
if (left === right) return true;
|
|
1230
|
+
if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => _jsonEqual(value, right[index]));
|
|
1231
|
+
if (!_isRecord(left) || !_isRecord(right)) return false;
|
|
1232
|
+
const leftKeys = Object.keys(left).sort();
|
|
1233
|
+
const rightKeys = Object.keys(right).sort();
|
|
1234
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && _jsonEqual(left[key], right[key]));
|
|
1235
|
+
}
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region src/schema/validateInterfaceErrors.ts
|
|
1238
|
+
function validateInterfaceErrors(schema) {
|
|
1239
|
+
if (schema === void 0) return;
|
|
1240
|
+
for (const [methodName, method] of Object.entries(schema.methods)) {
|
|
1241
|
+
if (method.result === void 0 && method.errors !== void 0) throw new Error(`Notification "${methodName}" cannot declare application errors.`);
|
|
1242
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1243
|
+
const errors = method.errors ?? [];
|
|
1244
|
+
for (const error of errors) {
|
|
1245
|
+
if (!Number.isInteger(error.code) || error.code < -2147483648 || error.code > 2147483647) throw new Error(`Application error code ${error.code} on "${methodName}" must be a signed 32-bit integer.`);
|
|
1246
|
+
if (errors.some((other) => other !== error && other.code === error.code && (other.schema !== void 0 || error.schema !== void 0))) throw new Error(`Raw error code ${error.code} on "${methodName}" cannot be shared.`);
|
|
1247
|
+
if (error.schema !== void 0) {
|
|
1248
|
+
if ([
|
|
1249
|
+
"message",
|
|
1250
|
+
"type",
|
|
1251
|
+
"data"
|
|
1252
|
+
].some((key) => Object.hasOwn(error, key))) throw new Error(`Raw error ${error.code} on "${methodName}" must contain only code and schema.`);
|
|
1253
|
+
} else {
|
|
1254
|
+
if (typeof error.message !== "string") throw new Error(`Application error message on "${methodName}" must be a string.`);
|
|
1255
|
+
if (error.code >= -32768 && error.code <= -32e3 || error.code === -32800) throw new Error(`Application error code ${error.code} on "${methodName}" is reserved by JSON-RPC or LinkRPC.`);
|
|
1256
|
+
if (error.type !== void 0 && (typeof error.type !== "string" || error.type.length === 0)) throw new Error(`Application error type on "${methodName}" must be a nonempty string.`);
|
|
1257
|
+
}
|
|
1258
|
+
const key = error.type === void 0 ? `code:${error.code}` : `type:${error.type}`;
|
|
1259
|
+
if (seen.has(key)) throw new Error(`Duplicate application error ${key} on "${methodName}".`);
|
|
1260
|
+
seen.add(key);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
//#endregion
|
|
1265
|
+
//#region src/schema/interfaceTemplateGroup.ts
|
|
1266
|
+
/** Local authoring marker. Never serialized into the wire contract. */
|
|
1267
|
+
const interfaceTemplateGroup = Symbol.for("@hediet/linkrpc.interfaceTemplateGroup");
|
|
1268
|
+
//#endregion
|
|
1269
|
+
//#region src/connection/interfaceDefinition.ts
|
|
1270
|
+
const interfaceDefinitionState = /* @__PURE__ */ new WeakMap();
|
|
1271
|
+
var InterfaceDefinition = class {
|
|
1272
|
+
info;
|
|
1273
|
+
members;
|
|
1274
|
+
/** Typed wire references for capability and access-request construction. */
|
|
1275
|
+
ref;
|
|
1276
|
+
constructor(info, members, opts = {}) {
|
|
1277
|
+
this.info = info;
|
|
1278
|
+
this.members = members;
|
|
1279
|
+
if (opts.frozenSchema !== void 0 && opts.templates !== void 0) throw new Error("Cannot combine frozenSchema with authored templates");
|
|
1280
|
+
const frozenSchema = opts.templates === void 0 ? opts.frozenSchema : attachInterfaceTemplates(buildSchema(info, members, ""), opts.templates);
|
|
1281
|
+
validateInterfaceErrors(frozenSchema);
|
|
1282
|
+
if (frozenSchema !== void 0) validateInterfaceTemplates(frozenSchema);
|
|
1283
|
+
const effectiveTags = frozenSchema === void 0 ? normalizeInterfaceTags(info.tags ?? []) : getInterfaceTags(frozenSchema);
|
|
1284
|
+
if (info.tags !== void 0 || effectiveTags.length > 0) this.info = {
|
|
1285
|
+
...info,
|
|
1286
|
+
tags: effectiveTags
|
|
1287
|
+
};
|
|
1288
|
+
interfaceDefinitionState.set(this, {
|
|
1289
|
+
frozenSchema,
|
|
1290
|
+
schemaCache: void 0,
|
|
1291
|
+
hashCache: void 0,
|
|
1292
|
+
declarationGroups: void 0
|
|
1293
|
+
});
|
|
1294
|
+
this.ref = Object.fromEntries(Object.keys(members).map((member) => [member, {
|
|
1295
|
+
interfaceId: this.info.id,
|
|
1296
|
+
interfaceHash: this.schemaHash,
|
|
1297
|
+
member
|
|
1298
|
+
}]));
|
|
1299
|
+
if (info.hash !== void 0 && this.schemaHash !== info.hash) throw new Error(`Interface hash mismatch for "${info.id}": expected "${info.hash}", got "${this.schemaHash}". The interface's wire contract changed — update the pinned hash to "${this.schemaHash}" after reviewing the change.`);
|
|
1300
|
+
}
|
|
1301
|
+
/** @internal Materialize authored groups using the existing wire call functions verbatim. */
|
|
1302
|
+
nestClient(client) {
|
|
1303
|
+
const groups = interfaceDefinitionState.get(this).declarationGroups;
|
|
1304
|
+
if (groups === void 0) return client;
|
|
1305
|
+
const groupedWireNames = new Set(Object.values(groups).flatMap((group) => Object.values(group)));
|
|
1306
|
+
const result = Object.create(null);
|
|
1307
|
+
for (const [name, call] of Object.entries(client)) if (!groupedWireNames.has(name)) result[name] = call;
|
|
1308
|
+
for (const [name, mapping] of Object.entries(groups)) {
|
|
1309
|
+
const group = Object.create(null);
|
|
1310
|
+
for (const [member, wireName] of Object.entries(mapping)) group[member] = client[wireName];
|
|
1311
|
+
result[name] = group;
|
|
1312
|
+
}
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
/** Validate grouped implementations atomically, then lower them to wire handlers. */
|
|
1316
|
+
flattenHandlers(handlers) {
|
|
1317
|
+
const groups = interfaceDefinitionState.get(this).declarationGroups;
|
|
1318
|
+
if (groups === void 0) return handlers;
|
|
1319
|
+
const requireObject = (value, where) => {
|
|
1320
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`Expected handler group object at "${where}"`);
|
|
1321
|
+
return value;
|
|
1322
|
+
};
|
|
1323
|
+
const implementation = requireObject(handlers, this.info.id);
|
|
1324
|
+
const groupedWireNames = new Set(Object.values(groups).flatMap((group) => Object.values(group)));
|
|
1325
|
+
const flatNames = Object.keys(this.members).filter((name) => !groupedWireNames.has(name));
|
|
1326
|
+
const expected = /* @__PURE__ */ new Set([...Object.keys(groups), ...flatNames]);
|
|
1327
|
+
for (const key of Reflect.ownKeys(implementation)) if (typeof key !== "string" || !expected.has(key)) throw new Error(`Unexpected handler "${String(key)}"; grouped members require grouped handlers`);
|
|
1328
|
+
const result = Object.create(null);
|
|
1329
|
+
const requireHandler = (object, key, where) => {
|
|
1330
|
+
if (!Object.hasOwn(object, key) || typeof object[key] !== "function") throw new Error(`Missing or invalid handler "${where}"`);
|
|
1331
|
+
return object[key];
|
|
1332
|
+
};
|
|
1333
|
+
for (const name of flatNames) result[name] = requireHandler(implementation, name, name);
|
|
1334
|
+
for (const [name, mapping] of Object.entries(groups)) {
|
|
1335
|
+
if (!Object.hasOwn(implementation, name)) throw new Error(`Missing handler group "${name}"`);
|
|
1336
|
+
const group = requireObject(implementation[name], name);
|
|
1337
|
+
for (const member of Reflect.ownKeys(group)) if (typeof member !== "string" || !Object.hasOwn(mapping, member)) throw new Error(`Unexpected handler "${name}.${String(member)}"`);
|
|
1338
|
+
for (const [member, wireName] of Object.entries(mapping)) result[wireName] = requireHandler(group, member, `${name}.${member}`);
|
|
1339
|
+
}
|
|
1340
|
+
return result;
|
|
1341
|
+
}
|
|
1342
|
+
/** Content hash of this interface (see `computeInterfaceHash`). */
|
|
1343
|
+
get schemaHash() {
|
|
1344
|
+
const state = interfaceDefinitionState.get(this);
|
|
1345
|
+
if (state.hashCache === void 0) state.hashCache = computeInterfaceHash(state.frozenSchema ?? buildSchema(this.info, this.members, ""));
|
|
1346
|
+
return state.hashCache;
|
|
1347
|
+
}
|
|
1348
|
+
/** Lower the definition to a wire-format `LinkRpcInterfaceSchema`, hash filled in. */
|
|
1349
|
+
toSchema() {
|
|
1350
|
+
const state = interfaceDefinitionState.get(this);
|
|
1351
|
+
if (state.schemaCache === void 0) state.schemaCache = state.frozenSchema !== void 0 ? {
|
|
1352
|
+
...state.frozenSchema,
|
|
1353
|
+
hash: this.schemaHash,
|
|
1354
|
+
...this.info.tags !== void 0 ? { tags: [...this.info.tags] } : {}
|
|
1355
|
+
} : buildSchema(this.info, this.members, this.schemaHash);
|
|
1356
|
+
return state.schemaCache;
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
function buildSchema(info, members, hash) {
|
|
1360
|
+
const methods = Object.create(null);
|
|
1361
|
+
const components = {};
|
|
1362
|
+
for (const [name, member] of Object.entries(members)) methods[name] = toMethodSchema(name, member, components);
|
|
1363
|
+
const schema = {
|
|
1364
|
+
id: info.id,
|
|
1365
|
+
hash,
|
|
1366
|
+
methods
|
|
1367
|
+
};
|
|
1368
|
+
if (Object.keys(components).length > 0) schema.components = { schemas: components };
|
|
1369
|
+
if (info.description !== void 0) schema.description = info.description;
|
|
1370
|
+
if (info.comment !== void 0) schema.comment = info.comment;
|
|
1371
|
+
if (info.tags !== void 0) schema.tags = [...new Set(info.tags)];
|
|
1372
|
+
return schema;
|
|
1373
|
+
}
|
|
1374
|
+
function toMethodSchema(name, member, components, convert = (schema, position) => convertMemberSchema(name, position, schema, components)) {
|
|
1375
|
+
const docs = member.docs;
|
|
1376
|
+
if (member.kind === "request") {
|
|
1377
|
+
const m = {
|
|
1378
|
+
params: convert(member.paramsSchema, "params"),
|
|
1379
|
+
result: convert(member.resultSchema, "result")
|
|
1380
|
+
};
|
|
1381
|
+
if (member.clientStreamSchema !== void 0) m.clientStream = convert(member.clientStreamSchema, "clientStream");
|
|
1382
|
+
if (member.serverStreamSchema !== void 0) m.serverStream = convert(member.serverStreamSchema, "serverStream");
|
|
1383
|
+
if (member.applicationErrors.length > 0) m.errors = member.applicationErrors.map((error) => {
|
|
1384
|
+
if (error.bodySchema !== void 0) return {
|
|
1385
|
+
code: error.code,
|
|
1386
|
+
schema: convert(error.bodySchema, `error=${error.code}`)
|
|
1387
|
+
};
|
|
1388
|
+
const result = {
|
|
1389
|
+
code: error.code,
|
|
1390
|
+
message: error.message,
|
|
1391
|
+
...error.type === void 0 ? {} : { type: error.type }
|
|
1392
|
+
};
|
|
1393
|
+
if (error.dataSchema !== void 0) result.data = convert(error.dataSchema, `error=${error.code}`);
|
|
1394
|
+
return result;
|
|
1395
|
+
});
|
|
1396
|
+
if (docs.description !== void 0) m.description = docs.description;
|
|
1397
|
+
if (docs.comment !== void 0) m.comment = docs.comment;
|
|
1398
|
+
if (docs.annotations !== void 0) m.annotations = docs.annotations;
|
|
1399
|
+
return m;
|
|
1400
|
+
}
|
|
1401
|
+
const m = { params: convert(member.paramsSchema, "params") };
|
|
1402
|
+
if (docs.description !== void 0) m.description = docs.description;
|
|
1403
|
+
if (docs.comment !== void 0) m.comment = docs.comment;
|
|
1404
|
+
if (docs.annotations !== void 0) m.annotations = docs.annotations;
|
|
1405
|
+
return m;
|
|
1406
|
+
}
|
|
1407
|
+
function convertMemberSchema(methodName, position, schema, components) {
|
|
1408
|
+
return zodToSvcJsonSchema(schema, {
|
|
1409
|
+
methodName,
|
|
1410
|
+
schemaPosition: position,
|
|
1411
|
+
components
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Convenient builder for an interface definition. Tracks TypeScript types
|
|
1416
|
+
* through `requestType` / `notificationType` so client and server code can
|
|
1417
|
+
* derive their shapes from the definition.
|
|
1418
|
+
*
|
|
1419
|
+
* @example
|
|
1420
|
+
* const myInterface = defineInterface(
|
|
1421
|
+
* { id: "de.hediet.notification-target" },
|
|
1422
|
+
* {
|
|
1423
|
+
* send: requestType(z.object({ to: z.string() }), z.string()),
|
|
1424
|
+
* notify: notificationType(z.object({ message: z.string() })),
|
|
1425
|
+
* },
|
|
1426
|
+
* );
|
|
1427
|
+
*/
|
|
1428
|
+
function defineInterface(info, declarations, options = {}) {
|
|
1429
|
+
const entries = Object.entries(declarations);
|
|
1430
|
+
const isGroup = (value) => value !== null && typeof value === "object" && interfaceTemplateGroup in value && value[interfaceTemplateGroup] === true;
|
|
1431
|
+
if (!entries.some(([, value]) => isGroup(value))) return new InterfaceDefinition(info, declarations, options);
|
|
1432
|
+
const members = Object.create(null);
|
|
1433
|
+
const groups = Object.create(null);
|
|
1434
|
+
const templates = Object.assign(Object.create(null), options.templates);
|
|
1435
|
+
const groupNames = new Set(entries.filter(([, value]) => isGroup(value)).map(([name]) => name));
|
|
1436
|
+
const addMember = (name, member) => {
|
|
1437
|
+
if (Object.hasOwn(members, name) || groupNames.has(name)) throw new Error(`Interface member collision at "${name}"`);
|
|
1438
|
+
members[name] = member;
|
|
1439
|
+
};
|
|
1440
|
+
for (const [name, declaration] of entries) {
|
|
1441
|
+
if (!isGroup(declaration)) {
|
|
1442
|
+
addMember(name, declaration);
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
if (Object.hasOwn(templates, name)) throw new Error(`Interface template instance collision at "${name}"`);
|
|
1446
|
+
const mapping = Object.create(null);
|
|
1447
|
+
if (declaration.mapping !== void 0) {
|
|
1448
|
+
for (const member of Object.keys(declaration.mapping)) if (!Object.hasOwn(declaration.members, member)) throw new Error(`Unknown mapped member "${member}"`);
|
|
1449
|
+
}
|
|
1450
|
+
for (const [member, type] of Object.entries(declaration.members)) {
|
|
1451
|
+
if (declaration.mapping !== void 0 && !Object.hasOwn(declaration.mapping, member)) throw new Error(`Missing mapped member "${member}"`);
|
|
1452
|
+
const wireName = declaration.mapping === void 0 ? `${name}$${member}` : declaration.mapping[member];
|
|
1453
|
+
if (typeof wireName !== "string" || wireName.length === 0) throw new Error(`Missing mapped member "${member}"`);
|
|
1454
|
+
addMember(wireName, type);
|
|
1455
|
+
mapping[member] = wireName;
|
|
1456
|
+
}
|
|
1457
|
+
groups[name] = Object.freeze(mapping);
|
|
1458
|
+
templates[name] = {
|
|
1459
|
+
template: declaration.template,
|
|
1460
|
+
schema: {
|
|
1461
|
+
template: declaration.template.id,
|
|
1462
|
+
arguments: declaration.arguments,
|
|
1463
|
+
members: mapping
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
const definition = new InterfaceDefinition(info, members, {
|
|
1468
|
+
...options,
|
|
1469
|
+
templates
|
|
1470
|
+
});
|
|
1471
|
+
interfaceDefinitionState.get(definition).declarationGroups = Object.freeze(groups);
|
|
1472
|
+
return definition;
|
|
1473
|
+
}
|
|
1474
|
+
//#endregion
|
|
1475
|
+
//#region src/crypto/cryptoProvider.ts
|
|
1476
|
+
const _b64UrlAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
1477
|
+
function bytesToBase64Url(bytes) {
|
|
1478
|
+
let out = "";
|
|
1479
|
+
let i = 0;
|
|
1480
|
+
for (; i + 3 <= bytes.length; i += 3) {
|
|
1481
|
+
const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
|
|
1482
|
+
out += _b64UrlAlphabet[n >> 18 & 63] + _b64UrlAlphabet[n >> 12 & 63] + _b64UrlAlphabet[n >> 6 & 63] + _b64UrlAlphabet[n & 63];
|
|
1483
|
+
}
|
|
1484
|
+
const rem = bytes.length - i;
|
|
1485
|
+
if (rem === 1) {
|
|
1486
|
+
const n = bytes[i] << 16;
|
|
1487
|
+
out += _b64UrlAlphabet[n >> 18 & 63] + _b64UrlAlphabet[n >> 12 & 63];
|
|
1488
|
+
} else if (rem === 2) {
|
|
1489
|
+
const n = bytes[i] << 16 | bytes[i + 1] << 8;
|
|
1490
|
+
out += _b64UrlAlphabet[n >> 18 & 63] + _b64UrlAlphabet[n >> 12 & 63] + _b64UrlAlphabet[n >> 6 & 63];
|
|
1491
|
+
}
|
|
1492
|
+
return out;
|
|
1493
|
+
}
|
|
1494
|
+
//#endregion
|
|
1495
|
+
//#region src/inspection/inspection.interfaces.ts
|
|
1496
|
+
const zTopologyPort = object({
|
|
1497
|
+
portId: string(),
|
|
1498
|
+
label: optional(string())
|
|
1499
|
+
});
|
|
1500
|
+
const zParticipantDescriptor = object({
|
|
1501
|
+
type: optional(string()),
|
|
1502
|
+
label: optional(string()),
|
|
1503
|
+
processId: optional(number()),
|
|
1504
|
+
processType: optional(string()),
|
|
1505
|
+
nodeStatusServiceId: optional(string()),
|
|
1506
|
+
metadata: optional(record(string(), union([
|
|
1507
|
+
string(),
|
|
1508
|
+
number(),
|
|
1509
|
+
boolean()
|
|
1510
|
+
])))
|
|
1511
|
+
});
|
|
1512
|
+
const zParticipantDescriptorSource = object({
|
|
1513
|
+
source: _enum(["self", "attacher"]),
|
|
1514
|
+
descriptor: zParticipantDescriptor
|
|
1515
|
+
});
|
|
1516
|
+
const zTopologyNode = object({
|
|
1517
|
+
nodeId: string(),
|
|
1518
|
+
kind: optional(_enum(["endpoint", "hub"])),
|
|
1519
|
+
label: optional(string()),
|
|
1520
|
+
descriptors: optional(array(zParticipantDescriptorSource)),
|
|
1521
|
+
ports: array(zTopologyPort)
|
|
1522
|
+
});
|
|
1523
|
+
const zTopologyLinkEndpoint = object({
|
|
1524
|
+
nodeId: string(),
|
|
1525
|
+
portId: string()
|
|
1526
|
+
});
|
|
1527
|
+
const zTopologyTransportEndpoint = object({
|
|
1528
|
+
address: optional(string()),
|
|
1529
|
+
port: optional(number().check(int(), nonnegative()))
|
|
1530
|
+
});
|
|
1531
|
+
/**
|
|
1532
|
+
* Transport details as observed at a topology link. `local` corresponds to the
|
|
1533
|
+
* link's `from` endpoint and `remote` to its `to` endpoint.
|
|
1534
|
+
*/
|
|
1535
|
+
const zTopologyTransportInfo = object({
|
|
1536
|
+
type: string(),
|
|
1537
|
+
local: optional(zTopologyTransportEndpoint),
|
|
1538
|
+
remote: optional(zTopologyTransportEndpoint),
|
|
1539
|
+
path: optional(string()),
|
|
1540
|
+
metadata: optional(record(string(), union([
|
|
1541
|
+
string(),
|
|
1542
|
+
number(),
|
|
1543
|
+
boolean()
|
|
1544
|
+
])))
|
|
1545
|
+
});
|
|
1546
|
+
const zTopologyLink = object({
|
|
1547
|
+
from: zTopologyLinkEndpoint,
|
|
1548
|
+
to: zTopologyLinkEndpoint,
|
|
1549
|
+
label: optional(string()),
|
|
1550
|
+
peerState: optional(_enum([
|
|
1551
|
+
"identified",
|
|
1552
|
+
"pending",
|
|
1553
|
+
"unsupported",
|
|
1554
|
+
"error"
|
|
1555
|
+
])),
|
|
1556
|
+
transport: optional(zTopologyTransportInfo)
|
|
1557
|
+
});
|
|
1558
|
+
const zRouteClaim = object({
|
|
1559
|
+
serviceId: string(),
|
|
1560
|
+
nodeId: string(),
|
|
1561
|
+
portId: string(),
|
|
1562
|
+
match: _enum(["exact", "prefix"])
|
|
1563
|
+
});
|
|
1564
|
+
const zTopologyGraph = object({
|
|
1565
|
+
observerServiceId: string(),
|
|
1566
|
+
entryNodeId: string(),
|
|
1567
|
+
nodes: array(zTopologyNode),
|
|
1568
|
+
links: array(zTopologyLink),
|
|
1569
|
+
routes: array(zRouteClaim)
|
|
1570
|
+
});
|
|
1571
|
+
const topologyInterface = defineInterface({
|
|
1572
|
+
id: "hubrpc.topology",
|
|
1573
|
+
description: "Inspect the transport graph visible from a service. A watch emits invalidation ticks; consumers re-fetch getGraph after each tick."
|
|
1574
|
+
}, {
|
|
1575
|
+
getGraph: requestType(object({}), zTopologyGraph).withStream({ client: object({}) }),
|
|
1576
|
+
watchGraph: requestType(object({}), object({})).withStream({ server: object({}) })
|
|
1577
|
+
});
|
|
1578
|
+
const zTrafficTransitEndpoint = object({
|
|
1579
|
+
edgeId: string(),
|
|
1580
|
+
portId: string(),
|
|
1581
|
+
requestId: optional(union([number(), string()]))
|
|
1582
|
+
});
|
|
1583
|
+
const zTrafficTransitEvent = object({
|
|
1584
|
+
type: literal("transit"),
|
|
1585
|
+
timeMs: number(),
|
|
1586
|
+
nodeId: string(),
|
|
1587
|
+
in: optional(zTrafficTransitEndpoint),
|
|
1588
|
+
out: optional(zTrafficTransitEndpoint),
|
|
1589
|
+
disposition: _enum([
|
|
1590
|
+
"forwarded",
|
|
1591
|
+
"consumed",
|
|
1592
|
+
"dropped",
|
|
1593
|
+
"unroutable"
|
|
1594
|
+
]),
|
|
1595
|
+
kind: _enum([
|
|
1596
|
+
"request",
|
|
1597
|
+
"notification",
|
|
1598
|
+
"response",
|
|
1599
|
+
"stream"
|
|
1600
|
+
]),
|
|
1601
|
+
method: optional(string()),
|
|
1602
|
+
params: optional(unknown()),
|
|
1603
|
+
result: optional(unknown()),
|
|
1604
|
+
error: optional(object({
|
|
1605
|
+
code: number(),
|
|
1606
|
+
message: string(),
|
|
1607
|
+
data: optional(unknown())
|
|
1608
|
+
}))
|
|
1609
|
+
});
|
|
1610
|
+
const zTrafficOverflowEvent = object({
|
|
1611
|
+
type: literal("overflow"),
|
|
1612
|
+
dropped: number().check(int(), nonnegative())
|
|
1613
|
+
});
|
|
1614
|
+
const zTrafficEvent = union([zTrafficTransitEvent, zTrafficOverflowEvent]);
|
|
1615
|
+
const zTrafficWatchResult = object({
|
|
1616
|
+
delivered: number().check(int(), nonnegative()),
|
|
1617
|
+
dropped: number().check(int(), nonnegative())
|
|
1618
|
+
});
|
|
1619
|
+
const zTrafficWatchParams = object({
|
|
1620
|
+
methodPrefix: optional(string()),
|
|
1621
|
+
trafficIgnoreKey: optional(string()),
|
|
1622
|
+
focusRequest: optional(object({
|
|
1623
|
+
portId: string(),
|
|
1624
|
+
requestId: union([number(), string()])
|
|
1625
|
+
}))
|
|
1626
|
+
});
|
|
1627
|
+
const zTrafficWatchWithPayloadsParams = object({
|
|
1628
|
+
methodPrefix: optional(string()),
|
|
1629
|
+
trafficIgnoreKey: optional(string()),
|
|
1630
|
+
focusRequest: optional(object({
|
|
1631
|
+
portId: string(),
|
|
1632
|
+
requestId: union([number(), string()])
|
|
1633
|
+
})),
|
|
1634
|
+
maxPayloadBytes: number().check(int(), nonnegative())
|
|
1635
|
+
});
|
|
1636
|
+
/**
|
|
1637
|
+
* Observe raw message transits at the node hosting the addressed service.
|
|
1638
|
+
* Consumers may correlate adjacent transits by shared `(portId, requestId)`.
|
|
1639
|
+
*/
|
|
1640
|
+
const trafficInterface = defineInterface({
|
|
1641
|
+
id: "hubrpc.traffic",
|
|
1642
|
+
description: "Stream raw message transits for the entire node hosting the addressed service. Payload-free and explicitly payload-bearing variants keep disclosure opt-in."
|
|
1643
|
+
}, {
|
|
1644
|
+
watch: requestType(zTrafficWatchParams, zTrafficWatchResult).withStream({ server: zTrafficEvent }),
|
|
1645
|
+
watchWithPayloads: requestType(zTrafficWatchWithPayloadsParams, zTrafficWatchResult).withStream({ server: zTrafficEvent })
|
|
1646
|
+
});
|
|
1647
|
+
//#endregion
|
|
1648
|
+
//#region src/inspection/node.interfaces.ts
|
|
1649
|
+
const zNodeInfo = object({
|
|
1650
|
+
/**
|
|
1651
|
+
* Random identity of this connection's node. It is a topology-correlation
|
|
1652
|
+
* label only and must not be used for authentication or authorization.
|
|
1653
|
+
*/
|
|
1654
|
+
nodeId: string(),
|
|
1655
|
+
/** Random identity of this connection's port on its node. */
|
|
1656
|
+
portId: string(),
|
|
1657
|
+
/** Optional diagnostic descriptors; never used for authorization. */
|
|
1658
|
+
descriptors: optional(array(zParticipantDescriptorSource))
|
|
1659
|
+
});
|
|
1660
|
+
/**
|
|
1661
|
+
* Minimal root service for aligning the independently observed topologies at
|
|
1662
|
+
* both ends of a connection.
|
|
1663
|
+
*/
|
|
1664
|
+
const nodeInterface = defineInterface({
|
|
1665
|
+
id: "hubrpc.node",
|
|
1666
|
+
description: "Topology bootstrap: identify the node and port at this end of the direct connection."
|
|
1667
|
+
}, { getNodeId: requestType(object({}), zNodeInfo) });
|
|
1668
|
+
//#endregion
|
|
1669
|
+
//#region src/connection/streaming.ts
|
|
1670
|
+
/**
|
|
1671
|
+
* Direction a {@link STREAM_METHOD} message travels, relative to the
|
|
1672
|
+
* request it is correlated with. Made explicit on the wire so that a
|
|
1673
|
+
* middlebox (the hub) can *author* a stream message — e.g. inject a
|
|
1674
|
+
* cancel when a caller disconnects — without an inbound message whose
|
|
1675
|
+
* arrival link it could infer the direction from.
|
|
1676
|
+
*/
|
|
1677
|
+
const StreamDir = {
|
|
1678
|
+
/** callee → caller: progress, partial results. Routes like a response. */
|
|
1679
|
+
toCaller: "toCaller",
|
|
1680
|
+
/** caller → callee: input, cancellation, keepalive ping. */
|
|
1681
|
+
toCallee: "toCallee"
|
|
1682
|
+
};
|
|
1683
|
+
/**
|
|
1684
|
+
* Reserved control verbs. A control message carries no app `payload`; it
|
|
1685
|
+
* is interpreted by the runtime / hub itself, independently of the
|
|
1686
|
+
* originating method's stream schemas, so *any* request is cancellable
|
|
1687
|
+
* and keep-alive-able even when it declares no app stream.
|
|
1688
|
+
*/
|
|
1689
|
+
const StreamControlType = {
|
|
1690
|
+
/**
|
|
1691
|
+
* Ask the callee to abort the in-flight request. Always {@link
|
|
1692
|
+
* StreamDir.toCallee}. The callee surfaces this as an `AbortSignal`
|
|
1693
|
+
* and is expected to settle the request (typically with a
|
|
1694
|
+
* `cancelled` error response).
|
|
1695
|
+
*/
|
|
1696
|
+
cancel: "cancel",
|
|
1697
|
+
/**
|
|
1698
|
+
* Keepalive / liveness probe. Resets the hub's per-request idle timer
|
|
1699
|
+
* so a long-running call is not reaped, and lets either side actively
|
|
1700
|
+
* probe the peer. Carries a `nonce` the peer echoes in its {@link
|
|
1701
|
+
* StreamControlType.pong}. May travel in either direction. Emitted
|
|
1702
|
+
* automatically (without awaiting a pong) by the channel for streaming
|
|
1703
|
+
* calls; see {@link STREAM_METHOD}.
|
|
1704
|
+
*/
|
|
1705
|
+
ping: "ping",
|
|
1706
|
+
/**
|
|
1707
|
+
* Reply to a {@link StreamControlType.ping}, echoing the ping's
|
|
1708
|
+
* `nonce` so the prober can correlate it. Travels opposite the ping.
|
|
1709
|
+
* Carries no app effect — purely a liveness acknowledgement.
|
|
1710
|
+
*/
|
|
1711
|
+
pong: "pong"
|
|
1712
|
+
};
|
|
1713
|
+
/**
|
|
1714
|
+
* Streaming sub-protocol. Long-running requests can emit additional
|
|
1715
|
+
* notifications correlated with the original call in either direction
|
|
1716
|
+
* (callee → caller progress, caller → callee input / cancellation /
|
|
1717
|
+
* keepalive).
|
|
1718
|
+
*
|
|
1719
|
+
* Wire form: a JSON-RPC notification whose method is
|
|
1720
|
+
* `${streamInterface.info.id}::send` and whose params match
|
|
1721
|
+
* `streamInterface.members.send.paramsSchema`. The hub routes purely on
|
|
1722
|
+
* `requestId` (following the originating request's established path, like
|
|
1723
|
+
* a response); the originating request's capability authorization covers
|
|
1724
|
+
* its full stream lifetime, so stream notifications carry no method /
|
|
1725
|
+
* interface namespace of their own. App `payload` contents are typed per
|
|
1726
|
+
* request via the originating method's
|
|
1727
|
+
* `RequestType.clientStreamSchema` / `serverStreamSchema`; reserved
|
|
1728
|
+
* `control` messages are schema-independent.
|
|
1729
|
+
*
|
|
1730
|
+
* Idle timeout: a request that never streams (no `control` ping, no
|
|
1731
|
+
* stream message) is subject to the hub's per-request idle timeout
|
|
1732
|
+
* (default 30 minutes) — at which point it is cancelled and its caller
|
|
1733
|
+
* gets a `requestTimeout` error. This bounds the hub's pending-request
|
|
1734
|
+
* table, which is also what protects it from a slow-loris / DDoS that
|
|
1735
|
+
* opens requests and never completes them. Streaming-enabled calls keep
|
|
1736
|
+
* themselves alive by emitting a periodic {@link StreamControlType.ping}.
|
|
1737
|
+
*
|
|
1738
|
+
* The interface is declarative: nobody calls
|
|
1739
|
+
* `LinkRpcConnection.register(streamInterface, ...)`. The channel and the
|
|
1740
|
+
* hub intercept `$stream::send` directly. The definition exists so
|
|
1741
|
+
* reflection / consent UIs can describe the wire shape and so all wire
|
|
1742
|
+
* constants derive from one source.
|
|
1743
|
+
*/
|
|
1744
|
+
const streamInterface = defineInterface({
|
|
1745
|
+
id: "$stream",
|
|
1746
|
+
description: "Reserved sub-protocol for in-flight stream messages correlated to a request by `requestId`."
|
|
1747
|
+
}, { send: notificationType(object({
|
|
1748
|
+
/**
|
|
1749
|
+
* Id of the in-flight request the message belongs to.
|
|
1750
|
+
* The hub rewrites this across the forwarding boundary
|
|
1751
|
+
* in the same way it rewrites response ids.
|
|
1752
|
+
*/
|
|
1753
|
+
requestId: union([number(), string()]),
|
|
1754
|
+
/**
|
|
1755
|
+
* Which way this message travels relative to the request.
|
|
1756
|
+
* Explicit so the hub can author messages (e.g. cancel on
|
|
1757
|
+
* disconnect) without inferring direction from arrival.
|
|
1758
|
+
*/
|
|
1759
|
+
dir: _enum(["toCaller", "toCallee"]),
|
|
1760
|
+
/**
|
|
1761
|
+
* Reserved control verb (cancel / ping / pong). Mutually
|
|
1762
|
+
* exclusive with `payload` in practice: a control message
|
|
1763
|
+
* is interpreted by the runtime, not the application.
|
|
1764
|
+
*/
|
|
1765
|
+
control: optional(object({
|
|
1766
|
+
type: _enum([
|
|
1767
|
+
"cancel",
|
|
1768
|
+
"ping",
|
|
1769
|
+
"pong"
|
|
1770
|
+
]),
|
|
1771
|
+
/** Open-set human/diagnostic reason; see {@link StreamControlReason}. */
|
|
1772
|
+
reason: optional(string()),
|
|
1773
|
+
/**
|
|
1774
|
+
* Correlation token. A `ping` carries a fresh nonce;
|
|
1775
|
+
* the matching `pong` echoes it back so the prober
|
|
1776
|
+
* can resolve the right outstanding probe.
|
|
1777
|
+
*/
|
|
1778
|
+
nonce: optional(string())
|
|
1779
|
+
})),
|
|
1780
|
+
/** Opaque app payload — typed per call by the request's stream schema. */
|
|
1781
|
+
payload: optional(unknown())
|
|
1782
|
+
}), { description: "Emit a stream message tied to the request `requestId`." }) });
|
|
1783
|
+
/**
|
|
1784
|
+
* Full wire method name for stream notifications. Pulled from the
|
|
1785
|
+
* interface object so we have a single source of truth.
|
|
1786
|
+
*/
|
|
1787
|
+
const STREAM_METHOD = `${streamInterface.info.id}::send`;
|
|
1788
|
+
//#endregion
|
|
1789
|
+
//#region src/inspection/trafficFlowFilter.ts
|
|
1790
|
+
var TrafficFlowFilter = class {
|
|
1791
|
+
_focused;
|
|
1792
|
+
constructor(options) {
|
|
1793
|
+
this._focused = options.focus !== void 0 ? new CorrelatedFlow([options.focus]) : void 0;
|
|
1794
|
+
}
|
|
1795
|
+
shouldInclude(transit) {
|
|
1796
|
+
return this._focused?.accept(transit).matches ?? true;
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
var TrafficWatchFlowTracker = class {
|
|
1800
|
+
_active = /* @__PURE__ */ new Set();
|
|
1801
|
+
_unclaimed = /* @__PURE__ */ new Map();
|
|
1802
|
+
accept(transit) {
|
|
1803
|
+
if (transit.kind === "request" && isTrafficWatchMethod(transit.method)) {
|
|
1804
|
+
const trafficIgnoreKey = readTrafficIgnoreKey(transit.params);
|
|
1805
|
+
let flow = this._findFlow(transit);
|
|
1806
|
+
if (flow === void 0) {
|
|
1807
|
+
flow = new CorrelatedFlow(requestRefsOf(transit), completionRefOf(transit));
|
|
1808
|
+
if (!isTerminal(transit)) this._active.add(flow);
|
|
1809
|
+
} else flow.accept(transit);
|
|
1810
|
+
if (isTerminal(transit)) {
|
|
1811
|
+
this._removeFlow(flow);
|
|
1812
|
+
return true;
|
|
1813
|
+
}
|
|
1814
|
+
if (trafficIgnoreKey !== void 0) this._unclaimed.set(trafficIgnoreKey, flow);
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
for (const flow of this._active) {
|
|
1818
|
+
const match = flow.accept(transit);
|
|
1819
|
+
if (!match.matches) continue;
|
|
1820
|
+
if (match.completed) this._removeFlow(flow);
|
|
1821
|
+
return true;
|
|
1822
|
+
}
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
_findFlow(transit) {
|
|
1826
|
+
for (const flow of this._active) if (flow.matches(transit)) return flow;
|
|
1827
|
+
}
|
|
1828
|
+
_removeFlow(flow) {
|
|
1829
|
+
this._active.delete(flow);
|
|
1830
|
+
for (const [key, pending] of this._unclaimed) if (pending === flow) this._unclaimed.delete(key);
|
|
1831
|
+
}
|
|
1832
|
+
claim(trafficIgnoreKey) {
|
|
1833
|
+
return this._unclaimed.delete(trafficIgnoreKey);
|
|
1834
|
+
}
|
|
1835
|
+
clear() {
|
|
1836
|
+
this._active.clear();
|
|
1837
|
+
this._unclaimed.clear();
|
|
1838
|
+
}
|
|
1839
|
+
forgetPort(portId) {
|
|
1840
|
+
for (const flow of this._active) if (flow.hasPort(portId)) this._removeFlow(flow);
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
var CorrelatedFlow = class {
|
|
1844
|
+
_endpoints = /* @__PURE__ */ new Set();
|
|
1845
|
+
_ports = /* @__PURE__ */ new Set();
|
|
1846
|
+
_completionEndpoint;
|
|
1847
|
+
constructor(endpoints, completionEndpoint = endpoints[0]) {
|
|
1848
|
+
for (const endpoint of endpoints) {
|
|
1849
|
+
this._endpoints.add(endpointKey(endpoint));
|
|
1850
|
+
this._ports.add(endpoint.portId);
|
|
1851
|
+
}
|
|
1852
|
+
this._completionEndpoint = completionEndpoint === void 0 ? void 0 : endpointKey(completionEndpoint);
|
|
1853
|
+
}
|
|
1854
|
+
matches(transit) {
|
|
1855
|
+
return requestRefsOf(transit).some((endpoint) => this._endpoints.has(endpointKey(endpoint)));
|
|
1856
|
+
}
|
|
1857
|
+
hasPort(portId) {
|
|
1858
|
+
return this._ports.has(portId);
|
|
1859
|
+
}
|
|
1860
|
+
accept(transit) {
|
|
1861
|
+
const endpoints = requestRefsOf(transit);
|
|
1862
|
+
if (!this.matches(transit)) return {
|
|
1863
|
+
matches: false,
|
|
1864
|
+
completed: false
|
|
1865
|
+
};
|
|
1866
|
+
for (const endpoint of endpoints) {
|
|
1867
|
+
this._endpoints.add(endpointKey(endpoint));
|
|
1868
|
+
this._ports.add(endpoint.portId);
|
|
1869
|
+
}
|
|
1870
|
+
const completed = transit.kind === "response" && (isTerminal(transit) || this._completionEndpoint !== void 0 && endpoints.some((endpoint) => endpointKey(endpoint) === this._completionEndpoint));
|
|
1871
|
+
if (completed) this._endpoints.clear();
|
|
1872
|
+
return {
|
|
1873
|
+
matches: true,
|
|
1874
|
+
completed
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
function isTerminal(transit) {
|
|
1879
|
+
return transit.disposition === "dropped" || transit.disposition === "unroutable";
|
|
1880
|
+
}
|
|
1881
|
+
function requestRefsOf(transit) {
|
|
1882
|
+
const result = [];
|
|
1883
|
+
if (transit.in?.requestId !== void 0) result.push(requestRef(transit.in));
|
|
1884
|
+
if (transit.out?.requestId !== void 0) result.push(requestRef(transit.out));
|
|
1885
|
+
return result;
|
|
1886
|
+
}
|
|
1887
|
+
function requestRef(endpoint) {
|
|
1888
|
+
return {
|
|
1889
|
+
portId: endpoint.portId,
|
|
1890
|
+
requestId: endpoint.requestId
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function completionRefOf(transit) {
|
|
1894
|
+
if (transit.in?.requestId !== void 0) return requestRef(transit.in);
|
|
1895
|
+
if (transit.out?.requestId !== void 0) return requestRef(transit.out);
|
|
1896
|
+
}
|
|
1897
|
+
function endpointKey(endpoint) {
|
|
1898
|
+
return JSON.stringify([
|
|
1899
|
+
endpoint.portId,
|
|
1900
|
+
typeof endpoint.requestId,
|
|
1901
|
+
endpoint.requestId
|
|
1902
|
+
]);
|
|
1903
|
+
}
|
|
1904
|
+
function isTrafficWatchMethod(method) {
|
|
1905
|
+
return method?.endsWith("::hubrpc.traffic::watch") === true || method?.endsWith("::hubrpc.traffic::watchWithPayloads") === true || method === "hubrpc.traffic::watch" || method === "hubrpc.traffic::watchWithPayloads";
|
|
1906
|
+
}
|
|
1907
|
+
function readTrafficIgnoreKey(params) {
|
|
1908
|
+
if (params === null || Array.isArray(params) || typeof params !== "object" || !("trafficIgnoreKey" in params)) return;
|
|
1909
|
+
const value = params.trafficIgnoreKey;
|
|
1910
|
+
return typeof value === "string" ? value : void 0;
|
|
1911
|
+
}
|
|
1912
|
+
//#endregion
|
|
1913
|
+
//#region src/inspection/boundedTrafficSubscription.ts
|
|
1914
|
+
const DEFAULT_QUEUE_LIMIT = 256;
|
|
1915
|
+
/**
|
|
1916
|
+
* Applies per-subscriber filtering and payload policy while isolating traffic
|
|
1917
|
+
* producers from slow or failed stream consumers.
|
|
1918
|
+
*/
|
|
1919
|
+
var BoundedTrafficSubscription = class {
|
|
1920
|
+
_options;
|
|
1921
|
+
_send;
|
|
1922
|
+
_onDispose;
|
|
1923
|
+
_queueLimit;
|
|
1924
|
+
_queue = [];
|
|
1925
|
+
_active = true;
|
|
1926
|
+
_pumping = false;
|
|
1927
|
+
_overflowPending = 0;
|
|
1928
|
+
_delivered = 0;
|
|
1929
|
+
_dropped = 0;
|
|
1930
|
+
_resolveClosed;
|
|
1931
|
+
_filter;
|
|
1932
|
+
closed;
|
|
1933
|
+
constructor(_options, _send, _onDispose, _queueLimit = DEFAULT_QUEUE_LIMIT) {
|
|
1934
|
+
this._options = _options;
|
|
1935
|
+
this._send = _send;
|
|
1936
|
+
this._onDispose = _onDispose;
|
|
1937
|
+
this._queueLimit = _queueLimit;
|
|
1938
|
+
let resolve;
|
|
1939
|
+
this.closed = new Promise((r) => resolve = r);
|
|
1940
|
+
this._resolveClosed = resolve;
|
|
1941
|
+
this._filter = new TrafficFlowFilter({ focus: _options.focusRequest });
|
|
1942
|
+
}
|
|
1943
|
+
enqueue(transit) {
|
|
1944
|
+
if (!this._active) return;
|
|
1945
|
+
if (!this._filter.shouldInclude(transit)) return;
|
|
1946
|
+
if (this._options.methodPrefix !== void 0 && !transit.method?.startsWith(this._options.methodPrefix)) return;
|
|
1947
|
+
const event = this._options.maxPayloadBytes === void 0 ? withoutPayloads(transit) : withCappedPayloads(transit, this._options.maxPayloadBytes);
|
|
1948
|
+
if (this._queue.length >= this._queueLimit) {
|
|
1949
|
+
this._queue.shift();
|
|
1950
|
+
this._dropped++;
|
|
1951
|
+
this._overflowPending++;
|
|
1952
|
+
}
|
|
1953
|
+
this._queue.push(event);
|
|
1954
|
+
if (!this._pumping) {
|
|
1955
|
+
this._pumping = true;
|
|
1956
|
+
queueMicrotask(() => void this._pump());
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
dispose() {
|
|
1960
|
+
if (!this._active) return;
|
|
1961
|
+
this._active = false;
|
|
1962
|
+
this._dropped += this._queue.length;
|
|
1963
|
+
this._queue.length = 0;
|
|
1964
|
+
this._overflowPending = 0;
|
|
1965
|
+
this._onDispose();
|
|
1966
|
+
if (!this._pumping) this._complete();
|
|
1967
|
+
}
|
|
1968
|
+
async _pump() {
|
|
1969
|
+
try {
|
|
1970
|
+
while (this._active) {
|
|
1971
|
+
if (this._overflowPending > 0) {
|
|
1972
|
+
const dropped = this._overflowPending;
|
|
1973
|
+
this._overflowPending = 0;
|
|
1974
|
+
await this._send({
|
|
1975
|
+
type: "overflow",
|
|
1976
|
+
dropped
|
|
1977
|
+
});
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
const event = this._queue.shift();
|
|
1981
|
+
if (event === void 0) break;
|
|
1982
|
+
await this._send(event);
|
|
1983
|
+
this._delivered++;
|
|
1984
|
+
}
|
|
1985
|
+
} catch {
|
|
1986
|
+
this.dispose();
|
|
1987
|
+
} finally {
|
|
1988
|
+
this._pumping = false;
|
|
1989
|
+
if (!this._active) this._complete();
|
|
1990
|
+
else if (this._queue.length > 0 || this._overflowPending > 0) {
|
|
1991
|
+
this._pumping = true;
|
|
1992
|
+
queueMicrotask(() => void this._pump());
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
_complete() {
|
|
1997
|
+
this._resolveClosed({
|
|
1998
|
+
delivered: this._delivered,
|
|
1999
|
+
dropped: this._dropped
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
function withoutPayloads(transit) {
|
|
2004
|
+
const { params: _params, result: _result, error, ...rest } = transit;
|
|
2005
|
+
if (error === void 0) return rest;
|
|
2006
|
+
return {
|
|
2007
|
+
...rest,
|
|
2008
|
+
error: {
|
|
2009
|
+
code: error.code,
|
|
2010
|
+
message: error.message
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
function withCappedPayloads(transit, maxPayloadBytes) {
|
|
2015
|
+
return {
|
|
2016
|
+
...transit,
|
|
2017
|
+
params: capPayload(transit.params, maxPayloadBytes),
|
|
2018
|
+
result: capPayload(transit.result, maxPayloadBytes),
|
|
2019
|
+
error: transit.error === void 0 ? void 0 : {
|
|
2020
|
+
...transit.error,
|
|
2021
|
+
data: capPayload(transit.error.data, maxPayloadBytes)
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
function capPayload(value, maxBytes) {
|
|
2026
|
+
if (value === void 0) return void 0;
|
|
2027
|
+
if (maxBytes === Number.POSITIVE_INFINITY) return value;
|
|
2028
|
+
const json = JSON.stringify(value);
|
|
2029
|
+
if (json === void 0) return void 0;
|
|
2030
|
+
const encoded = new TextEncoder().encode(json);
|
|
2031
|
+
if (encoded.byteLength <= maxBytes) return value;
|
|
2032
|
+
const encoder = new TextEncoder();
|
|
2033
|
+
let preview = new TextDecoder().decode(encoded.slice(0, maxBytes));
|
|
2034
|
+
while (encoder.encode(preview).byteLength > maxBytes) preview = preview.slice(0, -1);
|
|
2035
|
+
return preview;
|
|
2036
|
+
}
|
|
2037
|
+
//#endregion
|
|
2038
|
+
//#region src/inspection/endpointTrafficInspector.ts
|
|
2039
|
+
const REQUEST_CORRELATION_RETENTION_MS = 18e5;
|
|
2040
|
+
const MAX_REQUEST_CORRELATIONS = 4096;
|
|
2041
|
+
var EndpointTrafficInspector = class {
|
|
2042
|
+
_nodeId;
|
|
2043
|
+
_portId;
|
|
2044
|
+
_emit;
|
|
2045
|
+
_isObserved;
|
|
2046
|
+
_outboundRequests = /* @__PURE__ */ new Map();
|
|
2047
|
+
_inboundRequests = /* @__PURE__ */ new Map();
|
|
2048
|
+
observe = (direction, message) => {
|
|
2049
|
+
this._onMessage(direction, message);
|
|
2050
|
+
};
|
|
2051
|
+
constructor(_nodeId, _portId, _emit, _isObserved) {
|
|
2052
|
+
this._nodeId = _nodeId;
|
|
2053
|
+
this._portId = _portId;
|
|
2054
|
+
this._emit = _emit;
|
|
2055
|
+
this._isObserved = _isObserved;
|
|
2056
|
+
}
|
|
2057
|
+
dispose() {
|
|
2058
|
+
this._outboundRequests.clear();
|
|
2059
|
+
this._inboundRequests.clear();
|
|
2060
|
+
}
|
|
2061
|
+
_onMessage(direction, message) {
|
|
2062
|
+
const now = Date.now();
|
|
2063
|
+
this._pruneRequestCorrelations(now);
|
|
2064
|
+
const publiclyObserved = this._isObserved();
|
|
2065
|
+
if (!publiclyObserved) this.dispose();
|
|
2066
|
+
let transit;
|
|
2067
|
+
const endpoint = this._endpoint("id" in message ? message.id ?? void 0 : void 0);
|
|
2068
|
+
const base = {
|
|
2069
|
+
type: "transit",
|
|
2070
|
+
timeMs: now,
|
|
2071
|
+
nodeId: this._nodeId,
|
|
2072
|
+
...direction === "inbound" ? { in: endpoint } : { out: endpoint },
|
|
2073
|
+
disposition: direction === "inbound" ? "consumed" : "forwarded"
|
|
2074
|
+
};
|
|
2075
|
+
if (isRequest(message)) {
|
|
2076
|
+
const correlation = {
|
|
2077
|
+
method: message.method,
|
|
2078
|
+
timeMs: now
|
|
2079
|
+
};
|
|
2080
|
+
if (publiclyObserved) {
|
|
2081
|
+
const map = this._requestMap(direction);
|
|
2082
|
+
const key = requestKey(message.id);
|
|
2083
|
+
map.delete(key);
|
|
2084
|
+
map.set(key, correlation);
|
|
2085
|
+
}
|
|
2086
|
+
transit = {
|
|
2087
|
+
...base,
|
|
2088
|
+
kind: "request",
|
|
2089
|
+
method: message.method,
|
|
2090
|
+
params: message.params
|
|
2091
|
+
};
|
|
2092
|
+
} else if (isResponse(message)) {
|
|
2093
|
+
const responseMap = direction === "inbound" ? this._outboundRequests : this._inboundRequests;
|
|
2094
|
+
const key = requestKey(message.id);
|
|
2095
|
+
const correlation = responseMap.get(key);
|
|
2096
|
+
responseMap.delete(key);
|
|
2097
|
+
transit = {
|
|
2098
|
+
...base,
|
|
2099
|
+
kind: "response",
|
|
2100
|
+
method: correlation?.method,
|
|
2101
|
+
result: "result" in message ? message.result : void 0,
|
|
2102
|
+
error: "error" in message ? message.error : void 0
|
|
2103
|
+
};
|
|
2104
|
+
} else if (isNotification(message) && message.method === STREAM_METHOD) {
|
|
2105
|
+
const params = message.params;
|
|
2106
|
+
const streamEndpoint = this._endpoint(params?.requestId);
|
|
2107
|
+
transit = {
|
|
2108
|
+
...base,
|
|
2109
|
+
...direction === "inbound" ? { in: streamEndpoint } : { out: streamEndpoint },
|
|
2110
|
+
kind: "stream",
|
|
2111
|
+
method: STREAM_METHOD,
|
|
2112
|
+
params: message.params
|
|
2113
|
+
};
|
|
2114
|
+
} else transit = {
|
|
2115
|
+
...base,
|
|
2116
|
+
kind: "notification",
|
|
2117
|
+
method: message.method,
|
|
2118
|
+
params: message.params
|
|
2119
|
+
};
|
|
2120
|
+
function requestKey(requestId) {
|
|
2121
|
+
return JSON.stringify([typeof requestId, requestId]);
|
|
2122
|
+
}
|
|
2123
|
+
this._emit(transit);
|
|
2124
|
+
}
|
|
2125
|
+
_requestMap(direction) {
|
|
2126
|
+
return direction === "outbound" ? this._outboundRequests : this._inboundRequests;
|
|
2127
|
+
}
|
|
2128
|
+
_endpoint(requestId) {
|
|
2129
|
+
return {
|
|
2130
|
+
edgeId: this._portId,
|
|
2131
|
+
portId: this._portId,
|
|
2132
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
_pruneRequestCorrelations(timeMs) {
|
|
2136
|
+
this._pruneRequestMap(this._outboundRequests, timeMs);
|
|
2137
|
+
this._pruneRequestMap(this._inboundRequests, timeMs);
|
|
2138
|
+
}
|
|
2139
|
+
_pruneRequestMap(map, timeMs) {
|
|
2140
|
+
while (map.size !== 0) {
|
|
2141
|
+
const first = map.entries().next().value;
|
|
2142
|
+
if (first === void 0 || map.size <= MAX_REQUEST_CORRELATIONS && timeMs - first[1].timeMs <= REQUEST_CORRELATION_RETENTION_MS) return;
|
|
2143
|
+
map.delete(first[0]);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
//#endregion
|
|
2148
|
+
//#region src/inspection/connectionInspectionSource.ts
|
|
2149
|
+
var ConnectionInspectionSource = class {
|
|
2150
|
+
_connection;
|
|
2151
|
+
_nodeId;
|
|
2152
|
+
_options;
|
|
2153
|
+
_isObserved;
|
|
2154
|
+
constructor(_connection, _nodeId, _options, _isObserved) {
|
|
2155
|
+
this._connection = _connection;
|
|
2156
|
+
this._nodeId = _nodeId;
|
|
2157
|
+
this._options = _options;
|
|
2158
|
+
this._isObserved = _isObserved;
|
|
2159
|
+
}
|
|
2160
|
+
snapshotTopology() {
|
|
2161
|
+
const { portId, label, peer, transport } = this._options;
|
|
2162
|
+
const nodes = [{
|
|
2163
|
+
nodeId: this._nodeId,
|
|
2164
|
+
ports: [{
|
|
2165
|
+
portId,
|
|
2166
|
+
...label === void 0 ? {} : { label }
|
|
2167
|
+
}]
|
|
2168
|
+
}];
|
|
2169
|
+
const links = [];
|
|
2170
|
+
if (peer !== void 0) {
|
|
2171
|
+
nodes.push({
|
|
2172
|
+
nodeId: peer.nodeId,
|
|
2173
|
+
kind: "endpoint",
|
|
2174
|
+
...peer.label === void 0 ? {} : { label: peer.label },
|
|
2175
|
+
ports: [{ portId: peer.portId }]
|
|
2176
|
+
});
|
|
2177
|
+
links.push({
|
|
2178
|
+
from: {
|
|
2179
|
+
nodeId: this._nodeId,
|
|
2180
|
+
portId
|
|
2181
|
+
},
|
|
2182
|
+
to: {
|
|
2183
|
+
nodeId: peer.nodeId,
|
|
2184
|
+
portId: peer.portId
|
|
2185
|
+
},
|
|
2186
|
+
peerState: "identified",
|
|
2187
|
+
...transport === void 0 ? {} : { transport }
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
nodes,
|
|
2192
|
+
links,
|
|
2193
|
+
routes: []
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
onTopologyChanged(_listener) {
|
|
2197
|
+
return { dispose: () => {} };
|
|
2198
|
+
}
|
|
2199
|
+
observeTraffic(listener) {
|
|
2200
|
+
const inspector = new EndpointTrafficInspector(this._nodeId, this._options.portId, listener, this._isObserved);
|
|
2201
|
+
const observation = this._connection.observeWireMessages(inspector.observe);
|
|
2202
|
+
return { dispose: () => {
|
|
2203
|
+
observation.dispose();
|
|
2204
|
+
inspector.dispose();
|
|
2205
|
+
} };
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
//#endregion
|
|
2209
|
+
//#region src/inspection/inspectionHost.ts
|
|
2210
|
+
/** A shared inspection scope, independent of both routing and its RPC connections. */
|
|
2211
|
+
var InspectionHost = class {
|
|
2212
|
+
_options;
|
|
2213
|
+
_sources = /* @__PURE__ */ new Map();
|
|
2214
|
+
_trackedConnections = /* @__PURE__ */ new Set();
|
|
2215
|
+
_bindings = /* @__PURE__ */ new Set();
|
|
2216
|
+
_subscribers = /* @__PURE__ */ new Set();
|
|
2217
|
+
_observers = /* @__PURE__ */ new Set();
|
|
2218
|
+
_topologyListeners = /* @__PURE__ */ new Set();
|
|
2219
|
+
_trafficSources = /* @__PURE__ */ new Set();
|
|
2220
|
+
_trafficWatches = new TrafficWatchFlowTracker();
|
|
2221
|
+
_disposed = false;
|
|
2222
|
+
nodeId;
|
|
2223
|
+
constructor(_options) {
|
|
2224
|
+
this._options = _options;
|
|
2225
|
+
this.nodeId = _options.nodeId;
|
|
2226
|
+
}
|
|
2227
|
+
get observerCount() {
|
|
2228
|
+
return this._subscribers.size + this._observers.size;
|
|
2229
|
+
}
|
|
2230
|
+
addSource(source) {
|
|
2231
|
+
this._assertActive();
|
|
2232
|
+
if (this._sources.has(source)) throw new Error("Inspection source is already registered");
|
|
2233
|
+
const readPorts = () => new Set(source.snapshotTopology().nodes.flatMap((node) => node.ports.map((port) => port.portId)));
|
|
2234
|
+
let ports = readPorts();
|
|
2235
|
+
const topology = source.onTopologyChanged(() => {
|
|
2236
|
+
const nextPorts = readPorts();
|
|
2237
|
+
const removed = [...ports].filter((port) => !nextPorts.has(port));
|
|
2238
|
+
ports = nextPorts;
|
|
2239
|
+
this._forgetUnusedPorts(removed);
|
|
2240
|
+
this._invalidateTopology();
|
|
2241
|
+
});
|
|
2242
|
+
let traffic;
|
|
2243
|
+
try {
|
|
2244
|
+
traffic = source.observeTraffic((event) => this.publishTransit(event));
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
topology.dispose();
|
|
2247
|
+
throw error;
|
|
2248
|
+
}
|
|
2249
|
+
const registration = { dispose: () => {
|
|
2250
|
+
if (this._sources.get(source) !== registration) return;
|
|
2251
|
+
this._sources.delete(source);
|
|
2252
|
+
topology.dispose();
|
|
2253
|
+
traffic.dispose();
|
|
2254
|
+
this._forgetUnusedPorts([...ports]);
|
|
2255
|
+
this._invalidateTopology();
|
|
2256
|
+
} };
|
|
2257
|
+
this._sources.set(source, registration);
|
|
2258
|
+
this._invalidateTopology();
|
|
2259
|
+
return registration;
|
|
2260
|
+
}
|
|
2261
|
+
trackConnection(connection, options) {
|
|
2262
|
+
this._assertActive();
|
|
2263
|
+
if (this.getGraph("").nodes.some((node) => node.ports.some((port) => port.portId === options.portId))) throw new Error(`Inspection port "${options.portId}" is already registered`);
|
|
2264
|
+
const source = new ConnectionInspectionSource(connection, this.nodeId, options, () => this.observerCount !== 0);
|
|
2265
|
+
const registration = this.addSource(source);
|
|
2266
|
+
let close;
|
|
2267
|
+
try {
|
|
2268
|
+
close = connection.onDidClose(() => result.dispose());
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
registration.dispose();
|
|
2271
|
+
throw error;
|
|
2272
|
+
}
|
|
2273
|
+
const result = { dispose: () => {
|
|
2274
|
+
this._trackedConnections.delete(result);
|
|
2275
|
+
close.dispose();
|
|
2276
|
+
registration.dispose();
|
|
2277
|
+
} };
|
|
2278
|
+
this._trackedConnections.add(result);
|
|
2279
|
+
return result;
|
|
2280
|
+
}
|
|
2281
|
+
expose(connection, options) {
|
|
2282
|
+
this._assertActive();
|
|
2283
|
+
const portId = options.portId ?? createPortId();
|
|
2284
|
+
const binding = new InspectionBinding(portId);
|
|
2285
|
+
const registrations = [];
|
|
2286
|
+
this._bindings.add(binding);
|
|
2287
|
+
const registerOptions = { serviceId: options.serviceId };
|
|
2288
|
+
try {
|
|
2289
|
+
const controls = new EndpointTrafficInspector(this.nodeId, portId, (event) => this._trafficWatches.accept(event), () => false);
|
|
2290
|
+
registrations.push(connection.observeWireMessages(controls.observe));
|
|
2291
|
+
registrations.push({ dispose: () => controls.dispose() });
|
|
2292
|
+
registrations.push(connection.registerInspection(nodeInterface, { getNodeId: () => ({
|
|
2293
|
+
nodeId: this.nodeId,
|
|
2294
|
+
portId,
|
|
2295
|
+
...(options.descriptors ?? this._options.descriptors) === void 0 ? {} : { descriptors: [...options.descriptors ?? this._options.descriptors ?? []] }
|
|
2296
|
+
}) }, registerOptions));
|
|
2297
|
+
registrations.push(connection.registerInspection(topologyInterface, {
|
|
2298
|
+
getGraph: () => this.getGraph(options.serviceId, options.includeRoute === false ? void 0 : portId),
|
|
2299
|
+
watchGraph: (_params, _ctx, stream) => this._watchGraph(binding, stream)
|
|
2300
|
+
}, registerOptions));
|
|
2301
|
+
registrations.push(connection.registerInspection(trafficInterface, {
|
|
2302
|
+
watch: (params, _ctx, stream) => this._watchTraffic(binding, params, stream),
|
|
2303
|
+
watchWithPayloads: (params, _ctx, stream) => this._watchTraffic(binding, params, stream)
|
|
2304
|
+
}, registerOptions));
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
for (const registration of registrations.reverse()) registration.dispose();
|
|
2307
|
+
this._bindings.delete(binding);
|
|
2308
|
+
throw error;
|
|
2309
|
+
}
|
|
2310
|
+
binding.onDispose = () => {
|
|
2311
|
+
this._bindings.delete(binding);
|
|
2312
|
+
for (const registration of registrations.reverse()) registration.dispose();
|
|
2313
|
+
this._forgetUnusedPorts([portId]);
|
|
2314
|
+
this._invalidateTopology();
|
|
2315
|
+
};
|
|
2316
|
+
registrations.push(connection.onDidClose(() => binding.dispose()));
|
|
2317
|
+
this._invalidateTopology();
|
|
2318
|
+
return binding;
|
|
2319
|
+
}
|
|
2320
|
+
getGraph(observerServiceId, routePortId) {
|
|
2321
|
+
this._assertActive();
|
|
2322
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2323
|
+
const mergeNode = (node) => {
|
|
2324
|
+
const previous = nodes.get(node.nodeId);
|
|
2325
|
+
const ports = new Map(previous?.ports.map((port) => [port.portId, port]));
|
|
2326
|
+
for (const port of node.ports) ports.set(port.portId, {
|
|
2327
|
+
...ports.get(port.portId),
|
|
2328
|
+
...port
|
|
2329
|
+
});
|
|
2330
|
+
nodes.set(node.nodeId, {
|
|
2331
|
+
...previous,
|
|
2332
|
+
...node,
|
|
2333
|
+
ports: [...ports.values()]
|
|
2334
|
+
});
|
|
2335
|
+
};
|
|
2336
|
+
mergeNode({
|
|
2337
|
+
nodeId: this.nodeId,
|
|
2338
|
+
kind: this._options.kind ?? "endpoint",
|
|
2339
|
+
...this._options.label === void 0 ? {} : { label: this._options.label },
|
|
2340
|
+
...this._options.descriptors === void 0 ? {} : { descriptors: [...this._options.descriptors] },
|
|
2341
|
+
ports: []
|
|
2342
|
+
});
|
|
2343
|
+
const links = [];
|
|
2344
|
+
const routes = [];
|
|
2345
|
+
for (const source of this._sources.keys()) {
|
|
2346
|
+
const fragment = source.snapshotTopology();
|
|
2347
|
+
for (const node of fragment.nodes) mergeNode(node);
|
|
2348
|
+
links.push(...fragment.links);
|
|
2349
|
+
routes.push(...fragment.routes);
|
|
2350
|
+
}
|
|
2351
|
+
for (const binding of this._bindings) mergeNode({
|
|
2352
|
+
nodeId: this.nodeId,
|
|
2353
|
+
ports: [{ portId: binding.portId }]
|
|
2354
|
+
});
|
|
2355
|
+
if (routePortId !== void 0) routes.push({
|
|
2356
|
+
serviceId: observerServiceId,
|
|
2357
|
+
nodeId: this.nodeId,
|
|
2358
|
+
portId: routePortId,
|
|
2359
|
+
match: "exact"
|
|
2360
|
+
});
|
|
2361
|
+
return {
|
|
2362
|
+
observerServiceId,
|
|
2363
|
+
entryNodeId: this.nodeId,
|
|
2364
|
+
nodes: [...nodes.values()],
|
|
2365
|
+
links,
|
|
2366
|
+
routes
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
publishTransit(event) {
|
|
2370
|
+
if (this._disposed || this._trafficWatches.accept(event)) return;
|
|
2371
|
+
for (const observer of [...this._observers]) try {
|
|
2372
|
+
observer(event);
|
|
2373
|
+
} catch {}
|
|
2374
|
+
for (const subscriber of this._subscribers) subscriber.enqueue(event);
|
|
2375
|
+
}
|
|
2376
|
+
subscribe(options, send) {
|
|
2377
|
+
this._assertActive();
|
|
2378
|
+
if (options.trafficIgnoreKey !== void 0 && !this._trafficWatches.claim(options.trafficIgnoreKey)) throw new Error("Traffic watch request was not observed before subscription");
|
|
2379
|
+
const subscriber = new BoundedTrafficSubscription(options, send, () => {
|
|
2380
|
+
this._subscribers.delete(subscriber);
|
|
2381
|
+
this._stopUnusedTrafficSources();
|
|
2382
|
+
});
|
|
2383
|
+
this._subscribers.add(subscriber);
|
|
2384
|
+
try {
|
|
2385
|
+
this._startTrafficSources();
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
subscriber.dispose();
|
|
2388
|
+
throw error;
|
|
2389
|
+
}
|
|
2390
|
+
return subscriber;
|
|
2391
|
+
}
|
|
2392
|
+
observeTraffic(observer) {
|
|
2393
|
+
this._assertActive();
|
|
2394
|
+
this._observers.add(observer);
|
|
2395
|
+
const registration = { dispose: () => {
|
|
2396
|
+
this._observers.delete(observer);
|
|
2397
|
+
this._stopUnusedTrafficSources();
|
|
2398
|
+
} };
|
|
2399
|
+
try {
|
|
2400
|
+
this._startTrafficSources();
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
registration.dispose();
|
|
2403
|
+
throw error;
|
|
2404
|
+
}
|
|
2405
|
+
return registration;
|
|
2406
|
+
}
|
|
2407
|
+
addTrafficSource(source) {
|
|
2408
|
+
this._assertActive();
|
|
2409
|
+
const entry = { source };
|
|
2410
|
+
this._trafficSources.add(entry);
|
|
2411
|
+
try {
|
|
2412
|
+
this._startTrafficSources();
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
this._trafficSources.delete(entry);
|
|
2415
|
+
throw error;
|
|
2416
|
+
}
|
|
2417
|
+
return { dispose: () => {
|
|
2418
|
+
this._trafficSources.delete(entry);
|
|
2419
|
+
entry.observation?.dispose();
|
|
2420
|
+
entry.observation = void 0;
|
|
2421
|
+
} };
|
|
2422
|
+
}
|
|
2423
|
+
dispose() {
|
|
2424
|
+
if (this._disposed) return;
|
|
2425
|
+
this._disposed = true;
|
|
2426
|
+
for (const binding of [...this._bindings]) binding.dispose();
|
|
2427
|
+
for (const subscriber of [...this._subscribers]) subscriber.dispose();
|
|
2428
|
+
this._observers.clear();
|
|
2429
|
+
this._stopUnusedTrafficSources();
|
|
2430
|
+
this._trafficSources.clear();
|
|
2431
|
+
for (const connection of [...this._trackedConnections]) connection.dispose();
|
|
2432
|
+
for (const source of [...this._sources.values()]) source.dispose();
|
|
2433
|
+
this._topologyListeners.clear();
|
|
2434
|
+
this._trafficWatches.clear();
|
|
2435
|
+
}
|
|
2436
|
+
_startTrafficSources() {
|
|
2437
|
+
if (this.observerCount === 0) return;
|
|
2438
|
+
for (const entry of this._trafficSources) entry.observation ??= entry.source.observe((event) => this.publishTransit(event));
|
|
2439
|
+
}
|
|
2440
|
+
_stopUnusedTrafficSources() {
|
|
2441
|
+
if (this.observerCount !== 0) return;
|
|
2442
|
+
for (const entry of this._trafficSources) {
|
|
2443
|
+
entry.observation?.dispose();
|
|
2444
|
+
entry.observation = void 0;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
_invalidateTopology() {
|
|
2448
|
+
if (this._disposed) return;
|
|
2449
|
+
for (const listener of this._topologyListeners) listener();
|
|
2450
|
+
}
|
|
2451
|
+
_forgetUnusedPorts(portIds) {
|
|
2452
|
+
if (this._disposed) return;
|
|
2453
|
+
const remaining = new Set(this.getGraph("").nodes.flatMap((node) => node.ports.map((port) => port.portId)));
|
|
2454
|
+
for (const portId of portIds) if (!remaining.has(portId)) this._trafficWatches.forgetPort(portId);
|
|
2455
|
+
}
|
|
2456
|
+
_watchGraph(binding, stream) {
|
|
2457
|
+
return new Promise((resolve, reject) => {
|
|
2458
|
+
let pending = false;
|
|
2459
|
+
let active = true;
|
|
2460
|
+
const finish = () => {
|
|
2461
|
+
active = false;
|
|
2462
|
+
this._topologyListeners.delete(invalidate);
|
|
2463
|
+
stream.signal.removeEventListener("abort", finish);
|
|
2464
|
+
binding.abort.signal.removeEventListener("abort", finish);
|
|
2465
|
+
resolve({});
|
|
2466
|
+
};
|
|
2467
|
+
const invalidate = () => {
|
|
2468
|
+
if (pending || !active) return;
|
|
2469
|
+
pending = true;
|
|
2470
|
+
queueMicrotask(() => {
|
|
2471
|
+
pending = false;
|
|
2472
|
+
if (!active) return;
|
|
2473
|
+
stream.send({}).catch((error) => {
|
|
2474
|
+
reject(error);
|
|
2475
|
+
finish();
|
|
2476
|
+
});
|
|
2477
|
+
});
|
|
2478
|
+
};
|
|
2479
|
+
this._topologyListeners.add(invalidate);
|
|
2480
|
+
stream.signal.addEventListener("abort", finish, { once: true });
|
|
2481
|
+
binding.abort.signal.addEventListener("abort", finish, { once: true });
|
|
2482
|
+
if (stream.signal.aborted || binding.abort.signal.aborted) finish();
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
async _watchTraffic(binding, options, stream) {
|
|
2486
|
+
const subscription = this.subscribe(options, (event) => stream.send(event));
|
|
2487
|
+
const dispose = () => subscription.dispose();
|
|
2488
|
+
stream.signal.addEventListener("abort", dispose, { once: true });
|
|
2489
|
+
binding.abort.signal.addEventListener("abort", dispose, { once: true });
|
|
2490
|
+
if (stream.signal.aborted || binding.abort.signal.aborted) dispose();
|
|
2491
|
+
try {
|
|
2492
|
+
return await subscription.closed;
|
|
2493
|
+
} finally {
|
|
2494
|
+
stream.signal.removeEventListener("abort", dispose);
|
|
2495
|
+
binding.abort.signal.removeEventListener("abort", dispose);
|
|
2496
|
+
subscription.dispose();
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
_assertActive() {
|
|
2500
|
+
if (this._disposed) throw new Error("Inspection host is disposed");
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
var InspectionBinding = class {
|
|
2504
|
+
portId;
|
|
2505
|
+
abort = new AbortController();
|
|
2506
|
+
onDispose = () => {};
|
|
2507
|
+
constructor(portId) {
|
|
2508
|
+
this.portId = portId;
|
|
2509
|
+
}
|
|
2510
|
+
dispose() {
|
|
2511
|
+
if (this.abort.signal.aborted) return;
|
|
2512
|
+
this.abort.abort();
|
|
2513
|
+
this.onDispose();
|
|
2514
|
+
}
|
|
2515
|
+
};
|
|
2516
|
+
function createPortId() {
|
|
2517
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
2518
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
2519
|
+
return `port:${bytesToBase64Url(bytes)}`;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
export { validateInterfaceTemplate as $, zTopologyTransportInfo as C, isRequest as Ct, zTrafficTransitEvent as D, normalizeJsonSchema as Dt, zTrafficTransitEndpoint as E, interfaceTemplateGroup as H, getInterfaceTags as J, INTERFACE_TEMPLATES_EXTENSION as K, bytesToBase64Url as M, zTrafficWatchResult as O, preferRicherInterfaceSchema as Q, InterfaceDefinition as R, zTopologyTransportEndpoint as S, isNotification as St, zTrafficOverflowEvent as T, RpcFailure as Tt, validateInterfaceErrors as U, toMethodSchema as V, interfaceTemplateMemberName as X, interfaceTemplatesEqual as Z, zTopologyGraph as _, jsonIssues as _t, TrafficWatchFlowTracker as a, zTopologyNode as b, brandApplicationError as bt, StreamControlType as c, componentSchemaRef as ct, nodeInterface as d, validateInterfaceTemplates as et, topologyInterface as f, isApplicationErrorValue as ft, zRouteClaim as g, zodToSvcJsonSchema as gt, zParticipantDescriptorSource as h, TrafficFlowFilter as i, jcsCanonicalize as it, StreamDir as l, NotificationType as lt, zParticipantDescriptor as m, requestType as mt, ConnectionInspectionSource as n, computeInterfaceHash as nt, STREAM_METHOD as o, trafficInterface as p, notificationType as pt, BoundedTrafficSubscription as r, sha256 as rt, componentSchemaName as st, InspectionHost as t, streamInterface as u, RequestType as ut, zTopologyLink as v, parseDeclaredErrorBody as vt, zTrafficEvent as w, isResponse as wt, zTopologyPort as x, ErrorCode as xt, zTopologyLinkEndpoint as y, jsonPointerSegment as yt, defineInterface as z };
|
|
2523
|
+
//# sourceMappingURL=inspectionHost-BA6ZmC_P.js.map
|