@jingyi0605/codingns4dsh 0.1.0
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 +1114 -0
- package/assets/provider-icons/claude-code.png +0 -0
- package/assets/provider-icons/codex.png +0 -0
- package/assets/provider-icons/command-code.svg +5 -0
- package/assets/provider-icons/deepseek-harness.svg +9 -0
- package/assets/provider-icons/gemini.png +0 -0
- package/assets/provider-icons/grok.png +0 -0
- package/assets/provider-icons/kimi.png +0 -0
- package/assets/provider-icons/opencode.png +0 -0
- package/assets/provider-icons/pi.svg +7 -0
- package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +97 -0
- package/data/build/dist/bootstrap/dsh-connection-adapter.js +169 -0
- package/data/build/dist/bootstrap/index.d.ts +38 -0
- package/data/build/dist/bootstrap/index.js +44 -0
- package/data/build/dist/client/account-bar.d.ts +9 -0
- package/data/build/dist/client/account-bar.js +398 -0
- package/data/build/dist/client/bundle.js +26167 -0
- package/data/build/dist/client/cli-catalog.d.ts +24 -0
- package/data/build/dist/client/cli-catalog.js +108 -0
- package/data/build/dist/client/cli-slots.d.ts +38 -0
- package/data/build/dist/client/cli-slots.js +297 -0
- package/data/build/dist/client/debug/ui.d.ts +12 -0
- package/data/build/dist/client/debug/ui.js +451 -0
- package/data/build/dist/client/dsh-h5-bootstrap.d.ts +64 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +293 -0
- package/data/build/dist/client/dsh-runtime-version.d.ts +3 -0
- package/data/build/dist/client/dsh-runtime-version.js +11 -0
- package/data/build/dist/client/external-tool-stream.d.ts +24 -0
- package/data/build/dist/client/external-tool-stream.js +114 -0
- package/data/build/dist/client/features/cli-adapters.d.ts +7 -0
- package/data/build/dist/client/features/cli-adapters.js +215 -0
- package/data/build/dist/client/features/debug.d.ts +4 -0
- package/data/build/dist/client/features/debug.js +24 -0
- package/data/build/dist/client/features/index.d.ts +33 -0
- package/data/build/dist/client/features/index.js +44 -0
- package/data/build/dist/client/features/lan-access-panel.d.ts +5 -0
- package/data/build/dist/client/features/lan-access-panel.js +146 -0
- package/data/build/dist/client/features/lan-access.d.ts +9 -0
- package/data/build/dist/client/features/lan-access.js +30 -0
- package/data/build/dist/client/features/login-protection-panel.d.ts +5 -0
- package/data/build/dist/client/features/login-protection-panel.js +91 -0
- package/data/build/dist/client/features/login-protection-session.d.ts +6 -0
- package/data/build/dist/client/features/login-protection-session.js +25 -0
- package/data/build/dist/client/features/login-protection.d.ts +4 -0
- package/data/build/dist/client/features/login-protection.js +23 -0
- package/data/build/dist/client/features/reverse-proxy-panel.d.ts +9 -0
- package/data/build/dist/client/features/reverse-proxy-panel.js +159 -0
- package/data/build/dist/client/features/reverse-proxy.d.ts +12 -0
- package/data/build/dist/client/features/reverse-proxy.js +112 -0
- package/data/build/dist/client/features/terminal-enhancement-panel.d.ts +5 -0
- package/data/build/dist/client/features/terminal-enhancement-panel.js +160 -0
- package/data/build/dist/client/features/terminal-enhancement.d.ts +4 -0
- package/data/build/dist/client/features/terminal-enhancement.js +24 -0
- package/data/build/dist/client/features/types.d.ts +58 -0
- package/data/build/dist/client/features/types.js +2 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.d.ts +5 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +66 -0
- package/data/build/dist/client/features/workspace-session-enhancement.d.ts +4 -0
- package/data/build/dist/client/features/workspace-session-enhancement.js +101 -0
- package/data/build/dist/client/h5-bootstrap-entry.d.ts +2 -0
- package/data/build/dist/client/h5-bootstrap-entry.js +17 -0
- package/data/build/dist/client/index.d.ts +31 -0
- package/data/build/dist/client/index.js +98 -0
- package/data/build/dist/client/lan-access.d.ts +28 -0
- package/data/build/dist/client/lan-access.js +68 -0
- package/data/build/dist/client/locale.d.ts +20 -0
- package/data/build/dist/client/locale.js +393 -0
- package/data/build/dist/client/provider-icon-assets.d.ts +2 -0
- package/data/build/dist/client/provider-icon-assets.js +23 -0
- package/data/build/dist/client/provider-icons.d.ts +19 -0
- package/data/build/dist/client/provider-icons.js +39 -0
- package/data/build/dist/client/remote-web-context.d.ts +57 -0
- package/data/build/dist/client/remote-web-context.js +939 -0
- package/data/build/dist/client/session-adapter-cache.d.ts +18 -0
- package/data/build/dist/client/session-adapter-cache.js +84 -0
- package/data/build/dist/client/settings-bridge.d.ts +32 -0
- package/data/build/dist/client/settings-bridge.js +126 -0
- package/data/build/dist/client/settings-section.d.ts +30 -0
- package/data/build/dist/client/settings-section.js +93 -0
- package/data/build/dist/client/subscription-slot.d.ts +18 -0
- package/data/build/dist/client/subscription-slot.js +223 -0
- package/data/build/dist/client/terminal/index.d.ts +4 -0
- package/data/build/dist/client/terminal/index.js +3 -0
- package/data/build/dist/client/terminal/model.d.ts +116 -0
- package/data/build/dist/client/terminal/model.js +435 -0
- package/data/build/dist/client/terminal/styles.d.ts +21 -0
- package/data/build/dist/client/terminal/styles.js +51 -0
- package/data/build/dist/client/terminal/ui.d.ts +19 -0
- package/data/build/dist/client/terminal/ui.js +257 -0
- package/data/build/dist/client/terminal/xterm-view.d.ts +13 -0
- package/data/build/dist/client/terminal/xterm-view.js +213 -0
- package/data/build/dist/client/theme.d.ts +56 -0
- package/data/build/dist/client/theme.js +186 -0
- package/data/build/dist/client/workspace-session-archive-dom.d.ts +29 -0
- package/data/build/dist/client/workspace-session-archive-dom.js +525 -0
- package/data/build/dist/client/workspace-session-fiber.d.ts +3 -0
- package/data/build/dist/client/workspace-session-fiber.js +33 -0
- package/data/build/dist/client/workspace-session-logo-dom.d.ts +26 -0
- package/data/build/dist/client/workspace-session-logo-dom.js +128 -0
- package/data/build/dist/features/index.d.ts +3 -0
- package/data/build/dist/features/index.js +2 -0
- package/data/build/dist/features/registry.d.ts +74 -0
- package/data/build/dist/features/registry.js +382 -0
- package/data/build/dist/features/resource-scope/index.d.ts +39 -0
- package/data/build/dist/features/resource-scope/index.js +161 -0
- package/data/build/dist/host/auth-session.d.ts +42 -0
- package/data/build/dist/host/auth-session.js +186 -0
- package/data/build/dist/host/cli-adapters/claude-driver.d.ts +14 -0
- package/data/build/dist/host/cli-adapters/claude-driver.js +103 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +45 -0
- package/data/build/dist/host/cli-adapters/codex-driver.js +534 -0
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +38 -0
- package/data/build/dist/host/cli-adapters/command-code-driver.js +394 -0
- package/data/build/dist/host/cli-adapters/command-code-subscription.d.ts +19 -0
- package/data/build/dist/host/cli-adapters/command-code-subscription.js +126 -0
- package/data/build/dist/host/cli-adapters/driver.d.ts +41 -0
- package/data/build/dist/host/cli-adapters/driver.js +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +37 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +180 -0
- package/data/build/dist/host/cli-adapters/dsh-tool-history.d.ts +48 -0
- package/data/build/dist/host/cli-adapters/dsh-tool-history.js +373 -0
- package/data/build/dist/host/cli-adapters/feature.d.ts +7 -0
- package/data/build/dist/host/cli-adapters/feature.js +335 -0
- package/data/build/dist/host/cli-adapters/gemini-driver.d.ts +24 -0
- package/data/build/dist/host/cli-adapters/gemini-driver.js +427 -0
- package/data/build/dist/host/cli-adapters/grok-driver.d.ts +38 -0
- package/data/build/dist/host/cli-adapters/grok-driver.js +372 -0
- package/data/build/dist/host/cli-adapters/http-sse-client.d.ts +22 -0
- package/data/build/dist/host/cli-adapters/http-sse-client.js +99 -0
- package/data/build/dist/host/cli-adapters/interaction-events.d.ts +11 -0
- package/data/build/dist/host/cli-adapters/interaction-events.js +80 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +65 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.js +256 -0
- package/data/build/dist/host/cli-adapters/kimi-driver.d.ts +21 -0
- package/data/build/dist/host/cli-adapters/kimi-driver.js +382 -0
- package/data/build/dist/host/cli-adapters/model-catalog.d.ts +22 -0
- package/data/build/dist/host/cli-adapters/model-catalog.js +89 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.d.ts +55 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.js +728 -0
- package/data/build/dist/host/cli-adapters/pi-driver.d.ts +43 -0
- package/data/build/dist/host/cli-adapters/pi-driver.js +312 -0
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +16 -0
- package/data/build/dist/host/cli-adapters/process-utils.js +27 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +82 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.js +610 -0
- package/data/build/dist/host/cli-adapters/registry.d.ts +95 -0
- package/data/build/dist/host/cli-adapters/registry.js +659 -0
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.d.ts +24 -0
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +119 -0
- package/data/build/dist/host/cli-adapters/session-probe.d.ts +20 -0
- package/data/build/dist/host/cli-adapters/session-probe.js +139 -0
- package/data/build/dist/host/cli-adapters/session-store.d.ts +68 -0
- package/data/build/dist/host/cli-adapters/session-store.js +180 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +45 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.js +205 -0
- package/data/build/dist/host/cli-adapters/stream-normalizer.d.ts +25 -0
- package/data/build/dist/host/cli-adapters/stream-normalizer.js +83 -0
- package/data/build/dist/host/cli-adapters/tool-observation.d.ts +9 -0
- package/data/build/dist/host/cli-adapters/tool-observation.js +39 -0
- package/data/build/dist/host/control-api-client.d.ts +84 -0
- package/data/build/dist/host/control-api-client.js +130 -0
- package/data/build/dist/host/credential-store.d.ts +54 -0
- package/data/build/dist/host/credential-store.js +114 -0
- package/data/build/dist/host/debug.d.ts +79 -0
- package/data/build/dist/host/debug.js +295 -0
- package/data/build/dist/host/dsh-device-runtime.d.ts +39 -0
- package/data/build/dist/host/dsh-device-runtime.js +118 -0
- package/data/build/dist/host/dsh-gateway-feature.d.ts +5 -0
- package/data/build/dist/host/dsh-gateway-feature.js +55 -0
- package/data/build/dist/host/dsh-runtime-version.d.ts +12 -0
- package/data/build/dist/host/dsh-runtime-version.js +69 -0
- package/data/build/dist/host/features/auth.d.ts +11 -0
- package/data/build/dist/host/features/auth.js +273 -0
- package/data/build/dist/host/features/debug.d.ts +5 -0
- package/data/build/dist/host/features/debug.js +90 -0
- package/data/build/dist/host/features/host-status.d.ts +12 -0
- package/data/build/dist/host/features/host-status.js +54 -0
- package/data/build/dist/host/features/index.d.ts +19 -0
- package/data/build/dist/host/features/index.js +29 -0
- package/data/build/dist/host/features/lan-access-dsh.d.ts +8 -0
- package/data/build/dist/host/features/lan-access-dsh.js +45 -0
- package/data/build/dist/host/features/terminal-process.d.ts +5 -0
- package/data/build/dist/host/features/terminal-process.js +110 -0
- package/data/build/dist/host/features/terminal-status.d.ts +18 -0
- package/data/build/dist/host/features/terminal-status.js +56 -0
- package/data/build/dist/host/features/types.d.ts +41 -0
- package/data/build/dist/host/features/types.js +2 -0
- package/data/build/dist/host/index.d.ts +43 -0
- package/data/build/dist/host/index.js +135 -0
- package/data/build/dist/host/lan-access-dsh.d.ts +83 -0
- package/data/build/dist/host/lan-access-dsh.js +831 -0
- package/data/build/dist/host/native-session-bridge.d.ts +130 -0
- package/data/build/dist/host/native-session-bridge.js +317 -0
- package/data/build/dist/host/relay-tunnel-runtime.d.ts +97 -0
- package/data/build/dist/host/relay-tunnel-runtime.js +361 -0
- package/data/build/dist/host/remote-web-runtime.d.ts +78 -0
- package/data/build/dist/host/remote-web-runtime.js +559 -0
- package/data/build/dist/host/rpc-table.d.ts +32 -0
- package/data/build/dist/host/rpc-table.js +47 -0
- package/data/build/dist/host/rpc.d.ts +18 -0
- package/data/build/dist/host/rpc.js +248 -0
- package/data/build/dist/host/settings.d.ts +19 -0
- package/data/build/dist/host/settings.js +80 -0
- package/data/build/dist/host/terminal/attachment-registry.d.ts +17 -0
- package/data/build/dist/host/terminal/attachment-registry.js +49 -0
- package/data/build/dist/host/terminal/backends/conpty-backend.d.ts +39 -0
- package/data/build/dist/host/terminal/backends/conpty-backend.js +160 -0
- package/data/build/dist/host/terminal/backends/local-pty-backend.d.ts +36 -0
- package/data/build/dist/host/terminal/backends/local-pty-backend.js +178 -0
- package/data/build/dist/host/terminal/backends/tmux-backend.d.ts +40 -0
- package/data/build/dist/host/terminal/backends/tmux-backend.js +191 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-client.d.ts +27 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-client.js +154 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-process.d.ts +10 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-process.js +147 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-protocol.d.ts +92 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-protocol.js +134 -0
- package/data/build/dist/host/terminal/controller-factory.d.ts +37 -0
- package/data/build/dist/host/terminal/controller-factory.js +82 -0
- package/data/build/dist/host/terminal/index.d.ts +17 -0
- package/data/build/dist/host/terminal/index.js +17 -0
- package/data/build/dist/host/terminal/runtime-adapter.d.ts +77 -0
- package/data/build/dist/host/terminal/runtime-adapter.js +18 -0
- package/data/build/dist/host/terminal/runtime-manager.d.ts +41 -0
- package/data/build/dist/host/terminal/runtime-manager.js +179 -0
- package/data/build/dist/host/terminal/shell-detection.d.ts +23 -0
- package/data/build/dist/host/terminal/shell-detection.js +105 -0
- package/data/build/dist/host/terminal/startup.d.ts +33 -0
- package/data/build/dist/host/terminal/startup.js +100 -0
- package/data/build/dist/host/terminal/terminal-controller.d.ts +78 -0
- package/data/build/dist/host/terminal/terminal-controller.js +324 -0
- package/data/build/dist/host/terminal/terminal-process-service.d.ts +30 -0
- package/data/build/dist/host/terminal/terminal-process-service.js +194 -0
- package/data/build/dist/host/terminal/terminal-process-store.d.ts +45 -0
- package/data/build/dist/host/terminal/terminal-process-store.js +183 -0
- package/data/build/dist/host/terminal/terminal-service.d.ts +87 -0
- package/data/build/dist/host/terminal/terminal-service.js +528 -0
- package/data/build/dist/host/terminal/terminal-store.d.ts +49 -0
- package/data/build/dist/host/terminal/terminal-store.js +242 -0
- package/data/build/dist/index.d.ts +37 -0
- package/data/build/dist/index.js +26 -0
- package/data/build/dist/shared/contracts/auth.d.ts +122 -0
- package/data/build/dist/shared/contracts/auth.js +8 -0
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +192 -0
- package/data/build/dist/shared/contracts/cli-adapter.js +2 -0
- package/data/build/dist/shared/contracts/command-code.d.ts +5 -0
- package/data/build/dist/shared/contracts/command-code.js +2 -0
- package/data/build/dist/shared/contracts/config.d.ts +113 -0
- package/data/build/dist/shared/contracts/config.js +86 -0
- package/data/build/dist/shared/contracts/debug-config.d.ts +28 -0
- package/data/build/dist/shared/contracts/debug-config.js +78 -0
- package/data/build/dist/shared/contracts/debug.d.ts +75 -0
- package/data/build/dist/shared/contracts/debug.js +121 -0
- package/data/build/dist/shared/contracts/dsh-device.d.ts +72 -0
- package/data/build/dist/shared/contracts/dsh-device.js +2 -0
- package/data/build/dist/shared/contracts/errors.d.ts +33 -0
- package/data/build/dist/shared/contracts/errors.js +41 -0
- package/data/build/dist/shared/contracts/feature.d.ts +71 -0
- package/data/build/dist/shared/contracts/feature.js +2 -0
- package/data/build/dist/shared/contracts/host-status.d.ts +14 -0
- package/data/build/dist/shared/contracts/host-status.js +2 -0
- package/data/build/dist/shared/contracts/lan-access-dsh.d.ts +30 -0
- package/data/build/dist/shared/contracts/lan-access-dsh.js +2 -0
- package/data/build/dist/shared/contracts/peer-host.d.ts +37 -0
- package/data/build/dist/shared/contracts/peer-host.js +9 -0
- package/data/build/dist/shared/contracts/signaling.d.ts +67 -0
- package/data/build/dist/shared/contracts/signaling.js +2 -0
- package/data/build/dist/shared/contracts/subscription.d.ts +69 -0
- package/data/build/dist/shared/contracts/subscription.js +2 -0
- package/data/build/dist/shared/contracts/terminal-process.d.ts +65 -0
- package/data/build/dist/shared/contracts/terminal-process.js +2 -0
- package/data/build/dist/shared/contracts/terminal.d.ts +127 -0
- package/data/build/dist/shared/contracts/terminal.js +2 -0
- package/data/build/dist/shared/contracts/transport.d.ts +45 -0
- package/data/build/dist/shared/contracts/transport.js +3 -0
- package/data/build/dist/shared/contracts/version.d.ts +19 -0
- package/data/build/dist/shared/contracts/version.js +74 -0
- package/data/build/dist/shared/index.d.ts +22 -0
- package/data/build/dist/shared/index.js +9 -0
- package/data/build/dist/transport/carrier.d.ts +38 -0
- package/data/build/dist/transport/carrier.js +268 -0
- package/data/build/dist/transport/debug.d.ts +22 -0
- package/data/build/dist/transport/debug.js +57 -0
- package/data/build/dist/transport/dsh-envelope.d.ts +41 -0
- package/data/build/dist/transport/dsh-envelope.js +107 -0
- package/data/build/dist/transport/dsh-gateway.d.ts +64 -0
- package/data/build/dist/transport/dsh-gateway.js +254 -0
- package/data/build/dist/transport/dsh-session.d.ts +55 -0
- package/data/build/dist/transport/dsh-session.js +256 -0
- package/data/build/dist/transport/dsh-transport.d.ts +57 -0
- package/data/build/dist/transport/dsh-transport.js +125 -0
- package/data/build/dist/transport/frame.d.ts +132 -0
- package/data/build/dist/transport/frame.js +168 -0
- package/data/build/dist/transport/index.d.ts +11 -0
- package/data/build/dist/transport/index.js +11 -0
- package/data/build/dist/transport/multiplexer.d.ts +63 -0
- package/data/build/dist/transport/multiplexer.js +323 -0
- package/data/build/dist/transport/webrtc-client.d.ts +65 -0
- package/data/build/dist/transport/webrtc-client.js +255 -0
- package/data/build/dist/transport/webrtc-host.d.ts +58 -0
- package/data/build/dist/transport/webrtc-host.js +252 -0
- package/data/build/dist/typert.host.d.ts +25 -0
- package/data/build/dist/typert.host.js +150 -0
- package/dsh.bundle.patch +12 -0
- package/package.json +135 -0
- package/profile/README.md +27 -0
- package/profile/cordis.patch.yml +2 -0
- package/profile/package.json +20 -0
- package/profile/scripts/check-dsh-install.mjs +71 -0
- package/profile/version.json +5 -0
- package/scripts/check-dsh-install.mjs +88 -0
- package/version.json +6 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { decodeDshEnvelope, encodeDshEnvelope } from './dsh-envelope.js';
|
|
2
|
+
import { createDshTransportDebugLogger } from './debug.js';
|
|
3
|
+
const DEFAULT_SCOPE = { hostId: 'unknown', kind: 'remote' };
|
|
4
|
+
/** DSH Envelope 层多路复用器;逻辑 API 保持 rpc/openStream,物理线路只发送二进制 Envelope。 */
|
|
5
|
+
export class DshTunnelMultiplexer {
|
|
6
|
+
carrier;
|
|
7
|
+
pending = new Map();
|
|
8
|
+
streams = new Map();
|
|
9
|
+
sequence = 0;
|
|
10
|
+
nextId = 0;
|
|
11
|
+
disposed = false;
|
|
12
|
+
unsubscribe;
|
|
13
|
+
idPrefix;
|
|
14
|
+
flowControl;
|
|
15
|
+
generation;
|
|
16
|
+
hostScope;
|
|
17
|
+
session;
|
|
18
|
+
requireSessionReady;
|
|
19
|
+
debug;
|
|
20
|
+
constructor(carrier, options = {}) {
|
|
21
|
+
this.carrier = carrier;
|
|
22
|
+
this.idPrefix = options.idPrefix ?? 'g0';
|
|
23
|
+
this.flowControl = options.flowControl;
|
|
24
|
+
this.generation = options.generation ?? this.idPrefix.replace(/^g/u, '');
|
|
25
|
+
this.hostScope = options.hostScope ?? DEFAULT_SCOPE;
|
|
26
|
+
this.session = options.session;
|
|
27
|
+
this.requireSessionReady = options.requireSessionReady ?? options.session !== undefined;
|
|
28
|
+
this.debug = options.debug ?? createDshTransportDebugLogger({ component: 'multiplexer' });
|
|
29
|
+
this.unsubscribe = carrier.subscribe((data) => this.receive(data));
|
|
30
|
+
}
|
|
31
|
+
request(channel, payload, signal) {
|
|
32
|
+
return this.requestOperation(channel, channel === 'fetch' ? 'web.request' : 'rpc.request', payload, signal);
|
|
33
|
+
}
|
|
34
|
+
requestOperation(channel, operation, payload, signal) {
|
|
35
|
+
this.ensureOpen();
|
|
36
|
+
const id = `${this.idPrefix}_req_${this.nextRequestId()}`;
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const abort = () => {
|
|
39
|
+
if (!this.pending.delete(id))
|
|
40
|
+
return;
|
|
41
|
+
if (!this.disposed && this.carrier.state === 'open') {
|
|
42
|
+
try {
|
|
43
|
+
this.send({ streamId: id, channel: mapChannel(channel), type: 'stream.cancel', meta: { reason: String(signal?.reason ?? 'cancelled') } });
|
|
44
|
+
}
|
|
45
|
+
catch { /* 取消尽力而为 */ }
|
|
46
|
+
}
|
|
47
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error('请求已取消'));
|
|
48
|
+
};
|
|
49
|
+
if (signal?.aborted)
|
|
50
|
+
return abort();
|
|
51
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
52
|
+
this.pending.set(id, {
|
|
53
|
+
resolve: (value) => { signal?.removeEventListener('abort', abort); resolve(value); },
|
|
54
|
+
reject: (error) => { signal?.removeEventListener('abort', abort); reject(error); },
|
|
55
|
+
accepted: false,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
this.debug.log('request.send', { channel, operation, streamId: id, generation: this.generation, hostId: this.hostScope.hostId, bodyBytes: encodeJson(payload).byteLength });
|
|
59
|
+
this.send({
|
|
60
|
+
streamId: id,
|
|
61
|
+
channel: mapChannel(channel),
|
|
62
|
+
type: 'stream.open',
|
|
63
|
+
meta: { operation, encoding: 'json' },
|
|
64
|
+
body: encodeJson(payload),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.pending.delete(id);
|
|
69
|
+
signal?.removeEventListener('abort', abort);
|
|
70
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
openStream(payload, signal) {
|
|
75
|
+
return this.openStreamOperation('rpc', 'rpc.stream', payload, signal);
|
|
76
|
+
}
|
|
77
|
+
openStreamOperation(channel, operation, payload, signal) {
|
|
78
|
+
return this.openStreamOperationWithId(channel, operation, payload, signal).stream;
|
|
79
|
+
}
|
|
80
|
+
openStreamOperationWithId(channel, operation, payload, signal) {
|
|
81
|
+
this.ensureOpen();
|
|
82
|
+
if (signal?.aborted)
|
|
83
|
+
return { streamId: '', stream: this.closedStream() };
|
|
84
|
+
const id = `${this.idPrefix}_stream_${this.nextRequestId()}`;
|
|
85
|
+
const state = { queue: [], queuedBytes: 0, waiters: [], closed: false, removeAbort: undefined };
|
|
86
|
+
this.streams.set(id, state);
|
|
87
|
+
const cancel = () => {
|
|
88
|
+
if (state.closed)
|
|
89
|
+
return;
|
|
90
|
+
state.closed = true;
|
|
91
|
+
if (!this.disposed && this.carrier.state === 'open') {
|
|
92
|
+
try {
|
|
93
|
+
this.send({ streamId: id, channel, type: 'stream.cancel', meta: {} });
|
|
94
|
+
}
|
|
95
|
+
catch { /* 关闭路径尽力而为 */ }
|
|
96
|
+
}
|
|
97
|
+
this.finishStream(id, state);
|
|
98
|
+
};
|
|
99
|
+
if (signal) {
|
|
100
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
101
|
+
state.removeAbort = () => signal.removeEventListener('abort', cancel);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
this.send({ streamId: id, channel, type: 'stream.open', meta: { operation, encoding: 'json' }, body: encodeJson(payload) });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
state.error = error instanceof Error ? error : new Error(String(error));
|
|
108
|
+
this.finishStream(id, state);
|
|
109
|
+
}
|
|
110
|
+
const stream = { [Symbol.asyncIterator]: () => ({
|
|
111
|
+
next: async () => {
|
|
112
|
+
if (state.error)
|
|
113
|
+
throw state.error;
|
|
114
|
+
if (state.queue.length > 0) {
|
|
115
|
+
state.queuedBytes = 0;
|
|
116
|
+
return { done: false, value: state.queue.shift() };
|
|
117
|
+
}
|
|
118
|
+
if (state.closed)
|
|
119
|
+
return { done: true, value: undefined };
|
|
120
|
+
return new Promise((resolve, reject) => state.waiters.push({ resolve: (result) => resolve({ done: result.done === true, value: result.value }), reject }));
|
|
121
|
+
},
|
|
122
|
+
return: async () => { cancel(); return { done: true, value: undefined }; },
|
|
123
|
+
}) };
|
|
124
|
+
return { streamId: id, stream };
|
|
125
|
+
}
|
|
126
|
+
sendStreamMessage(streamId, channel, type, meta = {}, body) {
|
|
127
|
+
this.ensureOpen();
|
|
128
|
+
this.send({ streamId, channel, type, meta, ...(body === undefined ? {} : { body }) });
|
|
129
|
+
}
|
|
130
|
+
closeStream(streamId, channel = 'web') {
|
|
131
|
+
if (this.disposed || this.carrier.state !== 'open')
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
this.sendStreamMessage(streamId, channel, 'stream.cancel', {});
|
|
135
|
+
}
|
|
136
|
+
catch { /* 关闭路径尽力而为 */ }
|
|
137
|
+
}
|
|
138
|
+
close(error = new Error('Transport 已关闭')) {
|
|
139
|
+
if (this.disposed)
|
|
140
|
+
return;
|
|
141
|
+
this.disposed = true;
|
|
142
|
+
this.unsubscribe();
|
|
143
|
+
for (const pending of this.pending.values())
|
|
144
|
+
pending.reject(error);
|
|
145
|
+
this.pending.clear();
|
|
146
|
+
for (const [id, stream] of this.streams) {
|
|
147
|
+
stream.error = error;
|
|
148
|
+
this.finishStream(id, stream);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
invalidate(error = new Error('Transport generation 已过期')) {
|
|
152
|
+
for (const pending of this.pending.values())
|
|
153
|
+
pending.reject(error);
|
|
154
|
+
this.pending.clear();
|
|
155
|
+
for (const [id, stream] of this.streams) {
|
|
156
|
+
stream.error = error;
|
|
157
|
+
this.finishStream(id, stream);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** 替换物理 carrier;逻辑请求已在 invalidate 中失败,后续请求使用新线路。 */
|
|
161
|
+
replaceCarrier(carrier) {
|
|
162
|
+
if (this.disposed)
|
|
163
|
+
throw new Error('Transport 已关闭');
|
|
164
|
+
this.unsubscribe();
|
|
165
|
+
this.carrier = carrier;
|
|
166
|
+
this.unsubscribe = carrier.subscribe((data) => this.receive(data));
|
|
167
|
+
}
|
|
168
|
+
setSession(session) {
|
|
169
|
+
this.session = session;
|
|
170
|
+
}
|
|
171
|
+
rotateGeneration(idPrefix, error = new Error('Transport generation 已过期')) {
|
|
172
|
+
this.invalidate(error);
|
|
173
|
+
this.idPrefix = idPrefix;
|
|
174
|
+
this.generation = idPrefix.replace(/^g/u, '');
|
|
175
|
+
}
|
|
176
|
+
receive(data) {
|
|
177
|
+
let envelope;
|
|
178
|
+
try {
|
|
179
|
+
if (typeof data === 'string') {
|
|
180
|
+
const legacy = JSON.parse(data);
|
|
181
|
+
envelope = legacyEnvelope(legacy);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const options = this.flowControl?.maxFrameBytes === undefined ? {} : { maxBytes: this.flowControl.maxFrameBytes };
|
|
185
|
+
envelope = decodeDshEnvelope(data, options);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
this.debug.log('envelope.decode.error', { bytes: typeof data === 'string' ? data.length : data.byteLength, error: error instanceof Error ? error.message : String(error) });
|
|
190
|
+
this.close(error instanceof Error ? error : new Error(String(error)));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.debug.log('envelope.receive', envelopeDebugFields(envelope, typeof data === 'string' ? new TextEncoder().encode(data).byteLength : data.byteLength));
|
|
194
|
+
if (envelope.generation !== this.generation || envelope.hostScope.hostId !== this.hostScope.hostId || envelope.hostScope.kind !== this.hostScope.kind) {
|
|
195
|
+
this.debug.log('envelope.scope.drop', { expectedGeneration: this.generation, expectedHostId: this.hostScope.hostId, expectedHostKind: this.hostScope.kind, ...envelopeDebugFields(envelope, undefined) });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.flowControl?.onReceive?.(typeof data === 'string' ? new TextEncoder().encode(data).byteLength : data.byteLength, envelope);
|
|
199
|
+
const state = this.streams.get(envelope.streamId);
|
|
200
|
+
const pending = this.pending.get(envelope.streamId);
|
|
201
|
+
if (state)
|
|
202
|
+
return this.receiveStream(envelope, state);
|
|
203
|
+
if (!pending)
|
|
204
|
+
return;
|
|
205
|
+
if (envelope.type === 'stream.accepted') {
|
|
206
|
+
pending.accepted = true;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (envelope.type.endsWith('.response')) {
|
|
210
|
+
this.pending.delete(envelope.streamId);
|
|
211
|
+
pending.resolve(decodePayload(envelope));
|
|
212
|
+
}
|
|
213
|
+
else if (envelope.type === 'stream.close') {
|
|
214
|
+
this.pending.delete(envelope.streamId);
|
|
215
|
+
pending.resolve(decodePayload(envelope));
|
|
216
|
+
}
|
|
217
|
+
else if (envelope.type === 'stream.error' || envelope.type.endsWith('.error')) {
|
|
218
|
+
this.pending.delete(envelope.streamId);
|
|
219
|
+
pending.reject(new Error(typeof envelope.meta.detail === 'string' ? envelope.meta.detail : '远端请求失败'));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
receiveStream(envelope, state) {
|
|
223
|
+
if (envelope.type === 'stream.accepted')
|
|
224
|
+
return;
|
|
225
|
+
if (envelope.type === 'stream.error' || envelope.type.endsWith('.error')) {
|
|
226
|
+
state.error = new Error(typeof envelope.meta.detail === 'string' ? envelope.meta.detail : '远端流失败');
|
|
227
|
+
this.finishStream(envelope.streamId, state);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (envelope.type === 'stream.close' || envelope.flags?.endOfStream) {
|
|
231
|
+
this.finishStream(envelope.streamId, state);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (envelope.type === 'stream.window')
|
|
235
|
+
return;
|
|
236
|
+
const value = decodePayload(envelope);
|
|
237
|
+
state.queuedBytes += envelope.body?.byteLength ?? 0;
|
|
238
|
+
if (state.queuedBytes > (this.flowControl?.maxQueueBytes ?? 4 * 1024 * 1024)) {
|
|
239
|
+
state.error = new Error('Transport 队列超过上限');
|
|
240
|
+
this.finishStream(envelope.streamId, state);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (state.waiters.length > 0)
|
|
244
|
+
state.waiters.shift()?.resolve({ done: false, value });
|
|
245
|
+
else
|
|
246
|
+
state.queue.push(value);
|
|
247
|
+
}
|
|
248
|
+
finishStream(id, state) {
|
|
249
|
+
state.closed = true;
|
|
250
|
+
state.removeAbort?.();
|
|
251
|
+
state.removeAbort = undefined;
|
|
252
|
+
this.streams.delete(id);
|
|
253
|
+
for (const waiter of state.waiters.splice(0))
|
|
254
|
+
state.error ? waiter.reject(state.error) : waiter.resolve({ done: true, value: undefined });
|
|
255
|
+
}
|
|
256
|
+
send(input) {
|
|
257
|
+
const envelope = { version: 1, messageId: `${this.idPrefix}_m_${this.nextSequence()}`, streamId: input.streamId, channel: input.channel, type: input.type, sequence: this.nextSequence(), generation: this.generation, hostScope: this.hostScope, meta: input.meta ?? {}, ...(input.body === undefined ? {} : { body: input.body }), ...(input.flags === undefined ? {} : { flags: input.flags }) };
|
|
258
|
+
const encoded = encodeDshEnvelope(envelope, this.flowControl?.maxFrameBytes === undefined ? {} : { maxBytes: this.flowControl.maxFrameBytes });
|
|
259
|
+
if (this.flowControl?.canSend && !this.flowControl.canSend(encoded.byteLength, envelope))
|
|
260
|
+
throw new Error('Transport 背压窗口不足');
|
|
261
|
+
const pending = this.carrier.send(encoded);
|
|
262
|
+
this.debug.log('envelope.send', envelopeDebugFields(envelope, encoded.byteLength));
|
|
263
|
+
if (pending && typeof pending.catch === 'function') {
|
|
264
|
+
void pending.catch((error) => this.close(error instanceof Error ? error : new Error(String(error))));
|
|
265
|
+
}
|
|
266
|
+
this.flowControl?.onSend?.(encoded.byteLength, envelope);
|
|
267
|
+
}
|
|
268
|
+
nextSequence() { if (this.sequence >= Number.MAX_SAFE_INTEGER)
|
|
269
|
+
throw new Error('DSH Envelope sequence 已耗尽'); return ++this.sequence; }
|
|
270
|
+
nextRequestId() { if (this.nextId >= Number.MAX_SAFE_INTEGER)
|
|
271
|
+
throw new Error('Transport request id 已耗尽'); return ++this.nextId; }
|
|
272
|
+
closedStream(error) { return { [Symbol.asyncIterator]: () => ({ next: async () => error ? Promise.reject(error) : ({ done: true, value: undefined }) }) }; }
|
|
273
|
+
ensureOpen() { if (this.disposed || this.carrier.state !== 'open')
|
|
274
|
+
throw new Error('Transport 尚未 ready'); if (this.requireSessionReady && !this.session?.ready)
|
|
275
|
+
throw new Error('SESSION_NOT_READY'); }
|
|
276
|
+
}
|
|
277
|
+
function envelopeDebugFields(envelope, bytes) {
|
|
278
|
+
return {
|
|
279
|
+
type: envelope.type,
|
|
280
|
+
channel: envelope.channel,
|
|
281
|
+
streamId: envelope.streamId,
|
|
282
|
+
sequence: envelope.sequence,
|
|
283
|
+
generation: envelope.generation,
|
|
284
|
+
hostId: envelope.hostScope.hostId,
|
|
285
|
+
hostKind: envelope.hostScope.kind,
|
|
286
|
+
operation: typeof envelope.meta.operation === 'string' ? envelope.meta.operation : undefined,
|
|
287
|
+
bodyBytes: envelope.body?.byteLength ?? 0,
|
|
288
|
+
...(bytes === undefined ? {} : { frameBytes: bytes }),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function mapChannel(channel) { return channel === 'fetch' || channel === 'web' ? 'web' : channel === 'control' ? 'session' : 'rpc'; }
|
|
292
|
+
function encodeJson(value) { return new TextEncoder().encode(JSON.stringify(value === undefined ? null : value)); }
|
|
293
|
+
function decodePayload(envelope) {
|
|
294
|
+
if (envelope.meta.encoding === 'json' && envelope.body) {
|
|
295
|
+
try {
|
|
296
|
+
return JSON.parse(new TextDecoder().decode(envelope.body));
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return envelope.body;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (envelope.meta.encoding === 'text' && envelope.body)
|
|
303
|
+
return new TextDecoder().decode(envelope.body);
|
|
304
|
+
return envelope.body ?? envelope.meta.payload;
|
|
305
|
+
}
|
|
306
|
+
function legacyEnvelope(value) {
|
|
307
|
+
if (typeof value.id !== 'string' || typeof value.kind !== 'string' || !Number.isSafeInteger(value.sequence))
|
|
308
|
+
throw new Error('Tunnel Frame 无效');
|
|
309
|
+
const channel = value.channel === 'fetch' ? 'web' : value.channel === 'control' ? 'session' : 'rpc';
|
|
310
|
+
return {
|
|
311
|
+
version: 1,
|
|
312
|
+
messageId: value.id,
|
|
313
|
+
streamId: value.id,
|
|
314
|
+
channel,
|
|
315
|
+
type: `${value.channel}.${value.kind}`,
|
|
316
|
+
sequence: value.sequence,
|
|
317
|
+
generation: '1',
|
|
318
|
+
hostScope: { hostId: 'unknown', kind: 'remote' },
|
|
319
|
+
meta: { encoding: 'json', ...(value.payload === undefined ? {} : { payload: value.payload }) },
|
|
320
|
+
...(value.payload === undefined ? {} : { body: new TextEncoder().encode(JSON.stringify(value.payload)) }),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=multiplexer.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RelayIceServer, RelaySignalingTicketResponse } from '../shared/index.js';
|
|
2
|
+
import { type CodingNsCarrier, type DataChannelLike } from './carrier.js';
|
|
3
|
+
import { type DshTransportDebugLogger } from './debug.js';
|
|
4
|
+
export interface SignalingSocketLike {
|
|
5
|
+
send(data: string): void;
|
|
6
|
+
close(code?: number, reason?: string): void;
|
|
7
|
+
addEventListener(type: 'message' | 'close' | 'error', listener: (event: Event) => void): void;
|
|
8
|
+
removeEventListener(type: 'message' | 'close' | 'error', listener: (event: Event) => void): void;
|
|
9
|
+
}
|
|
10
|
+
export interface PeerConnectionLike {
|
|
11
|
+
createDataChannel(label: string): DataChannelLike;
|
|
12
|
+
createOffer(): Promise<{
|
|
13
|
+
type: 'offer';
|
|
14
|
+
sdp?: string;
|
|
15
|
+
}>;
|
|
16
|
+
setLocalDescription(description: {
|
|
17
|
+
type: string;
|
|
18
|
+
sdp?: string;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
setRemoteDescription(description: {
|
|
21
|
+
type: string;
|
|
22
|
+
sdp: string;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
addIceCandidate(candidate: {
|
|
25
|
+
candidate: string;
|
|
26
|
+
sdpMid: string | null;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
close(): void;
|
|
29
|
+
onicecandidate: ((event: {
|
|
30
|
+
candidate: {
|
|
31
|
+
candidate: string;
|
|
32
|
+
sdpMid: string | null;
|
|
33
|
+
} | null;
|
|
34
|
+
}) => void) | null;
|
|
35
|
+
addEventListener?(type: string, listener: (event: Event) => void): void;
|
|
36
|
+
removeEventListener?(type: string, listener: (event: Event) => void): void;
|
|
37
|
+
}
|
|
38
|
+
export interface WebRtcClientConnectorOptions {
|
|
39
|
+
signalingTicket: RelaySignalingTicketResponse;
|
|
40
|
+
signalingSocketFactory(url: string): Promise<SignalingSocketLike> | SignalingSocketLike;
|
|
41
|
+
peerConnectionFactory(options: {
|
|
42
|
+
iceServers: RelayIceServer[];
|
|
43
|
+
iceTransportPolicy: 'all' | 'relay';
|
|
44
|
+
}): PeerConnectionLike;
|
|
45
|
+
channelLabel?: string;
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
heartbeatIntervalMs?: number;
|
|
48
|
+
debug?: DshTransportDebugLogger;
|
|
49
|
+
}
|
|
50
|
+
export interface WebRtcClientConnection {
|
|
51
|
+
carrier: CodingNsCarrier;
|
|
52
|
+
peerConnection: PeerConnectionLike;
|
|
53
|
+
signaling: SignalingSocketLike;
|
|
54
|
+
onClosed(listener: (error?: Error) => void): () => void;
|
|
55
|
+
close(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** 客户端发起 offer,校验 Host fingerprint 后返回 DataChannel Carrier。 */
|
|
58
|
+
export declare function connectWebRtcClient(options: WebRtcClientConnectorOptions): Promise<WebRtcClientConnection>;
|
|
59
|
+
export declare function createSignalingUrl(baseUrl: string, ticket: string): string;
|
|
60
|
+
export declare function waitForSignalingRegistered(socket: SignalingSocketLike, timeoutMs: number, cleanup?: Array<() => void>): Promise<void>;
|
|
61
|
+
/** Relay 只有在 Host 在线后才接受客户端 offer,避免过早发送被 HOST_NOT_CONNECTED 拒绝。 */
|
|
62
|
+
export declare function waitForPeerReady(socket: SignalingSocketLike, timeoutMs: number, cleanup?: Array<() => void>): Promise<void>;
|
|
63
|
+
export declare function extractDtlsFingerprint(sdp: string): string | null;
|
|
64
|
+
export declare function assertDtlsFingerprint(expected: string, sdp: string): void;
|
|
65
|
+
//# sourceMappingURL=webrtc-client.d.ts.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createDataChannelCarrier, TUNNEL_DATA_CHANNEL_LABEL } from './carrier.js';
|
|
2
|
+
import { encodeFrame } from './frame.js';
|
|
3
|
+
import { createDshTransportDebugLogger } from './debug.js';
|
|
4
|
+
/** 客户端发起 offer,校验 Host fingerprint 后返回 DataChannel Carrier。 */
|
|
5
|
+
export async function connectWebRtcClient(options) {
|
|
6
|
+
const ticket = options.signalingTicket;
|
|
7
|
+
const debug = options.debug ?? createDshTransportDebugLogger({ side: 'h5', component: 'webrtc-client' });
|
|
8
|
+
const signalingUrl = createSignalingUrl(ticket.signalingBaseUrl, ticket.ticket);
|
|
9
|
+
debug.log('signaling.connect', { signalingBaseUrl: ticket.signalingBaseUrl });
|
|
10
|
+
const signaling = await options.signalingSocketFactory(signalingUrl);
|
|
11
|
+
const cleanupListeners = [];
|
|
12
|
+
// 真实 WebSocket 暴露 readyState;测试注入的最小 socket 可能没有该字段。
|
|
13
|
+
// 生产路径严格等待 registered,旧的纯协议 fake 仍可直接进入 offer 流程。
|
|
14
|
+
if ('readyState' in signaling) {
|
|
15
|
+
await waitForSignalingRegistered(signaling, options.timeoutMs ?? 15_000, cleanupListeners);
|
|
16
|
+
await waitForPeerReady(signaling, options.timeoutMs ?? 15_000, cleanupListeners);
|
|
17
|
+
}
|
|
18
|
+
const heartbeat = options.heartbeatIntervalMs === 0 ? null : setInterval(() => {
|
|
19
|
+
try {
|
|
20
|
+
signaling.send(JSON.stringify({ type: 'ping', at: new Date().toISOString() }));
|
|
21
|
+
}
|
|
22
|
+
catch { /* 断线由 close 事件处理 */ }
|
|
23
|
+
}, options.heartbeatIntervalMs ?? 20_000);
|
|
24
|
+
cleanupListeners.push(() => { if (heartbeat)
|
|
25
|
+
clearInterval(heartbeat); });
|
|
26
|
+
const peerConnection = options.peerConnectionFactory({
|
|
27
|
+
iceServers: ticket.iceServers,
|
|
28
|
+
iceTransportPolicy: ticket.iceTransportPolicy,
|
|
29
|
+
});
|
|
30
|
+
// Relay Tunnel 的标签属于线协议,不能由调用方改写。
|
|
31
|
+
const channel = peerConnection.createDataChannel(TUNNEL_DATA_CHANNEL_LABEL);
|
|
32
|
+
const carrier = createDataChannelCarrier(channel, { ...(options.debug ? { debug: options.debug } : {}) });
|
|
33
|
+
let closed = false;
|
|
34
|
+
let closeNotified = false;
|
|
35
|
+
const closeListeners = new Set();
|
|
36
|
+
const notifyClosed = (error) => {
|
|
37
|
+
if (closeNotified)
|
|
38
|
+
return;
|
|
39
|
+
closeNotified = true;
|
|
40
|
+
debug.log('webrtc.connection.closed', { reason: error?.message ?? 'closed' });
|
|
41
|
+
for (const listener of [...closeListeners])
|
|
42
|
+
listener(error);
|
|
43
|
+
closeListeners.clear();
|
|
44
|
+
};
|
|
45
|
+
const onSignalingClose = () => notifyClosed(new Error('Relay signaling closed'));
|
|
46
|
+
const onSignalingError = () => notifyClosed(new Error('Relay signaling error'));
|
|
47
|
+
const onPeerState = () => {
|
|
48
|
+
const state = peerConnection;
|
|
49
|
+
const value = state.connectionState ?? state.iceConnectionState;
|
|
50
|
+
debug.log('webrtc.peer.state', { state: value ?? 'unknown' });
|
|
51
|
+
if (value === 'failed' || value === 'disconnected' || value === 'closed')
|
|
52
|
+
notifyClosed(new Error(`PeerConnection ${value}`));
|
|
53
|
+
};
|
|
54
|
+
signaling.addEventListener('close', onSignalingClose);
|
|
55
|
+
signaling.addEventListener('error', onSignalingError);
|
|
56
|
+
peerConnection.addEventListener?.('connectionstatechange', onPeerState);
|
|
57
|
+
peerConnection.addEventListener?.('iceconnectionstatechange', onPeerState);
|
|
58
|
+
const carrierClosed = (reason) => notifyClosed(new Error(reason ?? 'DataChannel closed'));
|
|
59
|
+
carrier.onClosed?.(carrierClosed);
|
|
60
|
+
const close = async () => {
|
|
61
|
+
if (closed)
|
|
62
|
+
return;
|
|
63
|
+
closed = true;
|
|
64
|
+
notifyClosed(new Error('client closed'));
|
|
65
|
+
signaling.removeEventListener('close', onSignalingClose);
|
|
66
|
+
signaling.removeEventListener('error', onSignalingError);
|
|
67
|
+
peerConnection.removeEventListener?.('connectionstatechange', onPeerState);
|
|
68
|
+
peerConnection.removeEventListener?.('iceconnectionstatechange', onPeerState);
|
|
69
|
+
for (const cleanup of cleanupListeners.splice(0))
|
|
70
|
+
cleanup();
|
|
71
|
+
await carrier.close();
|
|
72
|
+
peerConnection.close();
|
|
73
|
+
signaling.close(1000, 'client closed');
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
const answerPromise = waitForAnswer(signaling, options.timeoutMs ?? 15_000, cleanupListeners);
|
|
77
|
+
peerConnection.onicecandidate = (event) => {
|
|
78
|
+
if (!event.candidate)
|
|
79
|
+
return;
|
|
80
|
+
signaling.send(JSON.stringify({
|
|
81
|
+
type: 'candidate',
|
|
82
|
+
candidate: event.candidate.candidate,
|
|
83
|
+
mid: event.candidate.sdpMid,
|
|
84
|
+
}));
|
|
85
|
+
};
|
|
86
|
+
const offer = await peerConnection.createOffer();
|
|
87
|
+
await peerConnection.setLocalDescription(offer);
|
|
88
|
+
signaling.send(JSON.stringify({ type: 'offer', sdp: offer.sdp ?? '' }));
|
|
89
|
+
debug.log('signaling.offer.sent', { sdpBytes: offer.sdp?.length ?? 0 });
|
|
90
|
+
const answer = await answerPromise;
|
|
91
|
+
assertDtlsFingerprint(ticket.hostDtlsFingerprint, answer.sdp);
|
|
92
|
+
await peerConnection.setRemoteDescription({ type: 'answer', sdp: answer.sdp });
|
|
93
|
+
for (const candidate of answer.candidates)
|
|
94
|
+
await peerConnection.addIceCandidate(candidate);
|
|
95
|
+
await waitForOpen(channel, options.timeoutMs ?? 15_000, cleanupListeners);
|
|
96
|
+
await carrier.send(encodeFrame({ type: 'hello', clientContext: null, protocolVersion: '1' }));
|
|
97
|
+
debug.log('webrtc.connected', { channelLabel: TUNNEL_DATA_CHANNEL_LABEL });
|
|
98
|
+
return { carrier, peerConnection, signaling, onClosed: (listener) => { if (closeNotified) {
|
|
99
|
+
listener(new Error('connection closed'));
|
|
100
|
+
return () => undefined;
|
|
101
|
+
} closeListeners.add(listener); return () => closeListeners.delete(listener); }, close };
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
await close();
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function createSignalingUrl(baseUrl, ticket) {
|
|
109
|
+
const base = new URL(ensureWebSocketProtocol(baseUrl));
|
|
110
|
+
const pathname = base.pathname.endsWith('/') ? base.pathname : `${base.pathname}/`;
|
|
111
|
+
const url = new URL('signal', `${base.origin}${pathname}`);
|
|
112
|
+
url.searchParams.set('ticket', ticket);
|
|
113
|
+
return url.toString();
|
|
114
|
+
}
|
|
115
|
+
export function waitForSignalingRegistered(socket, timeoutMs, cleanup = []) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const timer = setTimeout(() => { remove(); reject(new Error('等待 Relay registered 超时')); }, timeoutMs);
|
|
118
|
+
const onMessage = (event) => {
|
|
119
|
+
const raw = event.data;
|
|
120
|
+
if (typeof raw !== 'string')
|
|
121
|
+
return;
|
|
122
|
+
try {
|
|
123
|
+
const message = JSON.parse(raw);
|
|
124
|
+
if (message.type === 'error') {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
remove();
|
|
127
|
+
const code = message.errorCode?.trim() || 'RELAY_ERROR';
|
|
128
|
+
const detail = message.detail?.trim();
|
|
129
|
+
reject(new Error(detail ? `Relay ${code}: ${detail}` : `Relay ${code}`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (message.type !== 'registered')
|
|
133
|
+
return;
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
remove();
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
catch { /* 忽略非 JSON 信令 */ }
|
|
139
|
+
};
|
|
140
|
+
const onClose = () => { clearTimeout(timer); remove(); reject(new Error('信令连接在 registered 前关闭')); };
|
|
141
|
+
const remove = () => { socket.removeEventListener('message', onMessage); socket.removeEventListener('close', onClose); };
|
|
142
|
+
socket.addEventListener('message', onMessage);
|
|
143
|
+
socket.addEventListener('close', onClose);
|
|
144
|
+
cleanup.push(remove);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Relay 只有在 Host 在线后才接受客户端 offer,避免过早发送被 HOST_NOT_CONNECTED 拒绝。 */
|
|
148
|
+
export function waitForPeerReady(socket, timeoutMs, cleanup = []) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const timer = setTimeout(() => { remove(); reject(new Error('等待 Relay peer-ready 超时')); }, timeoutMs);
|
|
151
|
+
const onMessage = (event) => {
|
|
152
|
+
const raw = event.data;
|
|
153
|
+
if (typeof raw !== 'string')
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
const message = JSON.parse(raw);
|
|
157
|
+
if (message.type === 'peer-ready') {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
remove();
|
|
160
|
+
resolve();
|
|
161
|
+
}
|
|
162
|
+
else if (message.type === 'error') {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
remove();
|
|
165
|
+
reject(new Error('Relay 拒绝 peer-ready'));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch { /* 忽略非 JSON 信令 */ }
|
|
169
|
+
};
|
|
170
|
+
const onClose = () => { clearTimeout(timer); remove(); reject(new Error('信令连接在 peer-ready 前关闭')); };
|
|
171
|
+
const remove = () => { socket.removeEventListener('message', onMessage); socket.removeEventListener('close', onClose); };
|
|
172
|
+
socket.addEventListener('message', onMessage);
|
|
173
|
+
socket.addEventListener('close', onClose);
|
|
174
|
+
cleanup.push(remove);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export function extractDtlsFingerprint(sdp) {
|
|
178
|
+
const match = sdp.match(/^a=fingerprint:([^\r\n]+)$/mu);
|
|
179
|
+
return match?.[1] ? normalizeFingerprint(match[1]) : null;
|
|
180
|
+
}
|
|
181
|
+
export function assertDtlsFingerprint(expected, sdp) {
|
|
182
|
+
const actual = extractDtlsFingerprint(sdp);
|
|
183
|
+
if (!actual || actual !== normalizeFingerprint(expected)) {
|
|
184
|
+
throw new Error('Host DTLS fingerprint 校验失败');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function waitForAnswer(socket, timeoutMs, cleanup) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const timer = setTimeout(() => reject(new Error('等待 Host answer 超时')), timeoutMs);
|
|
190
|
+
const candidates = [];
|
|
191
|
+
const onMessage = (event) => {
|
|
192
|
+
const raw = event.data;
|
|
193
|
+
if (typeof raw !== 'string')
|
|
194
|
+
return;
|
|
195
|
+
let message;
|
|
196
|
+
try {
|
|
197
|
+
message = JSON.parse(raw);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (message.type === 'answer') {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
remove();
|
|
205
|
+
resolve({ sdp: message.sdp, candidates });
|
|
206
|
+
}
|
|
207
|
+
else if (message.type === 'candidate') {
|
|
208
|
+
candidates.push({ candidate: message.candidate, sdpMid: message.mid });
|
|
209
|
+
}
|
|
210
|
+
else if (message.type === 'error') {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
remove();
|
|
213
|
+
reject(new Error(`${message.errorCode}: ${message.detail}`));
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const onClose = () => { clearTimeout(timer); remove(); reject(new Error('信令连接已关闭')); };
|
|
217
|
+
const remove = () => {
|
|
218
|
+
socket.removeEventListener('message', onMessage);
|
|
219
|
+
socket.removeEventListener('close', onClose);
|
|
220
|
+
};
|
|
221
|
+
socket.addEventListener('message', onMessage);
|
|
222
|
+
socket.addEventListener('close', onClose);
|
|
223
|
+
cleanup.push(remove);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function waitForOpen(channel, timeoutMs, cleanup) {
|
|
227
|
+
if (channel.readyState === 'open')
|
|
228
|
+
return Promise.resolve();
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const timer = setTimeout(() => { remove(); reject(new Error('等待 DataChannel open 超时')); }, timeoutMs);
|
|
231
|
+
const onOpen = () => { clearTimeout(timer); remove(); resolve(); };
|
|
232
|
+
const onClose = () => { clearTimeout(timer); remove(); reject(new Error('DataChannel 在 ready 前关闭')); };
|
|
233
|
+
const remove = () => {
|
|
234
|
+
channel.removeEventListener('open', onOpen);
|
|
235
|
+
channel.removeEventListener('close', onClose);
|
|
236
|
+
};
|
|
237
|
+
channel.addEventListener('open', onOpen);
|
|
238
|
+
channel.addEventListener('close', onClose);
|
|
239
|
+
cleanup.push(remove);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function normalizeFingerprint(value) {
|
|
243
|
+
return value.trim().replace(/^[a-z0-9-]+(?:\s+|:)+/iu, '').replace(/[^0-9a-f]/giu, '').toLowerCase();
|
|
244
|
+
}
|
|
245
|
+
function ensureWebSocketProtocol(value) {
|
|
246
|
+
const url = new URL(value);
|
|
247
|
+
if (url.protocol === 'http:')
|
|
248
|
+
url.protocol = 'ws:';
|
|
249
|
+
if (url.protocol === 'https:')
|
|
250
|
+
url.protocol = 'wss:';
|
|
251
|
+
if (url.protocol !== 'ws:' && url.protocol !== 'wss:')
|
|
252
|
+
throw new Error('Relay URL 必须使用 HTTP(S) 或 WS(S)');
|
|
253
|
+
return url.toString();
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=webrtc-client.js.map
|