@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildWarmupCompletionFrames,
|
|
3
|
+
buildWsErrorFrame,
|
|
4
|
+
selectForwardHeaders,
|
|
5
|
+
sendJsonFrame,
|
|
6
|
+
buildResponsesWsData,
|
|
7
|
+
sendResponseToWebSocket,
|
|
8
|
+
sendTextFrame,
|
|
9
|
+
type LiveSidebandUpstreamFailure,
|
|
10
|
+
type LiveSidebandUpstreamHandoff,
|
|
11
|
+
type WsData,
|
|
12
|
+
} from "../ws-bridge";
|
|
13
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
14
|
+
import {
|
|
15
|
+
handleLive,
|
|
16
|
+
logLiveSidebandFrame,
|
|
17
|
+
logLiveSidebandStage,
|
|
18
|
+
parseLiveSidebandTarget,
|
|
19
|
+
resolveLiveSidebandUpgrade,
|
|
20
|
+
} from "../live";
|
|
21
|
+
import { RESPONSE_TTL_MS } from "../../responses/state";
|
|
22
|
+
|
|
23
|
+
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
24
|
+
/**
|
|
25
|
+
* 0 means Bun never closes an idle socket, and this one value covers every socket kind the
|
|
26
|
+
* server accepts — the live sideband relay, where a quiet call is normal, and the Responses data
|
|
27
|
+
* plane, where quiet means the client is simply between turns.
|
|
28
|
+
*
|
|
29
|
+
* It is coupled to `RESPONSE_TTL_MS` whether or not anyone says so, which is why it is said here.
|
|
30
|
+
* A codex-rs client caches its `WebsocketSession` across turns and chains `previous_response_id`
|
|
31
|
+
* onto it; it only clears `last_request`/`last_response_rx` when it finds the connection closed.
|
|
32
|
+
* So a socket that outlives retention is a client that keeps referencing continuation state this
|
|
33
|
+
* process has already evicted. Two settings can hold that line and only these two:
|
|
34
|
+
*
|
|
35
|
+
* - a FINITE idle timeout below `MAX_WEBSOCKET_IDLE_TIMEOUT_SECONDS`, which closes the socket
|
|
36
|
+
* first and lets the client reset its own chain, or
|
|
37
|
+
* - this 0, which obliges the proxy to fail closed on the expired reference instead —
|
|
38
|
+
* `server/responses/request-prepare.ts` returns `previous_response_not_found`, the error
|
|
39
|
+
* codex-rs recognizes on a WebSocket turn and answers by replaying its full input.
|
|
40
|
+
*
|
|
41
|
+
* What must never happen is neither: an immortal socket plus a destination that silently accepts
|
|
42
|
+
* the orphaned delta. `tests/responses/ws-endpoint.test.ts` holds exactly that pair together.
|
|
43
|
+
* Raising the timeout off 0 is still worth doing for its own reasons (a dead peer holds a socket
|
|
44
|
+
* forever today), and Bun caps the value at 255 seconds, well inside the bound below.
|
|
45
|
+
*/
|
|
46
|
+
export const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
47
|
+
/** Ceiling a finite websocket idle timeout must stay under, in seconds. See above. */
|
|
48
|
+
export const MAX_WEBSOCKET_IDLE_TIMEOUT_SECONDS = Math.floor(RESPONSE_TTL_MS / 1_000);
|
|
49
|
+
|
|
50
|
+
const LIVE_SIDEBAND_PENDING_MAX = 32;
|
|
51
|
+
const LIVE_SIDEBAND_PENDING_BYTES_MAX = 1024 * 1024;
|
|
52
|
+
const LIVE_SIDEBAND_CLOSE_FALLBACK_MS = 1_000;
|
|
53
|
+
/**
|
|
54
|
+
* Bound the pre-upgrade upstream handshake. A sideband join that cannot reach 101
|
|
55
|
+
* must fail the client upgrade promptly rather than hold it open indefinitely.
|
|
56
|
+
*/
|
|
57
|
+
export const LIVE_SIDEBAND_UPSTREAM_OPEN_TIMEOUT_MS = 10_000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Outcome of the upstream sideband handshake performed before the client upgrade.
|
|
61
|
+
*
|
|
62
|
+
* `ok: false` carries the HTTP status the client upgrade must fail with. Only an
|
|
63
|
+
* upgrade failure reaches codex-rs as a connect error, and only a connect error
|
|
64
|
+
* ends its sideband reconnect loop (`realtime_conversation/sideband.rs`: the `Err`
|
|
65
|
+
* arm always breaks). A 101 followed by a close is instead read as `TransportLost`
|
|
66
|
+
* and retried forever against the same, permanently dead call id.
|
|
67
|
+
*/
|
|
68
|
+
export type LiveSidebandUpstreamOpenResult =
|
|
69
|
+
| {
|
|
70
|
+
ok: true;
|
|
71
|
+
socket: WebSocket;
|
|
72
|
+
/** Owns capture and terminal events until the downstream relay attaches. */
|
|
73
|
+
handoff: LiveSidebandUpstreamHandoff;
|
|
74
|
+
}
|
|
75
|
+
| { ok: false; status: number; code: string; message: string; socket?: WebSocket };
|
|
76
|
+
|
|
77
|
+
export function exceedsLiveSidebandFrameByteLimit(frameBytes: number): boolean {
|
|
78
|
+
return frameBytes > MAX_WS_FRAME_BYTES;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function exceedsLiveSidebandPendingByteLimit(pendingBytes: number, incomingBytes: number): boolean {
|
|
82
|
+
return incomingBytes > LIVE_SIDEBAND_PENDING_BYTES_MAX - pendingBytes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function webSocketFrameBytes(frame: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): number {
|
|
86
|
+
if (typeof frame === "string") return Buffer.byteLength(frame);
|
|
87
|
+
if (frame instanceof ArrayBuffer || ArrayBuffer.isView(frame)) return frame.byteLength;
|
|
88
|
+
return frame.size;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type LiveSidebandPendingEnqueueResult = "queued" | "too-many-frames" | "too-many-bytes";
|
|
92
|
+
|
|
93
|
+
export function enqueueLiveSidebandPendingFrame(
|
|
94
|
+
data: Pick<WsData, "livePending" | "livePendingBytes">,
|
|
95
|
+
frame: string | Buffer,
|
|
96
|
+
frameBytes = webSocketFrameBytes(frame),
|
|
97
|
+
): LiveSidebandPendingEnqueueResult {
|
|
98
|
+
const pending = data.livePending ?? (data.livePending = []);
|
|
99
|
+
if (pending.length >= LIVE_SIDEBAND_PENDING_MAX) return "too-many-frames";
|
|
100
|
+
const pendingBytes = data.livePendingBytes ?? 0;
|
|
101
|
+
if (exceedsLiveSidebandPendingByteLimit(pendingBytes, frameBytes)) return "too-many-bytes";
|
|
102
|
+
pending.push(frame);
|
|
103
|
+
data.livePendingBytes = pendingBytes + frameBytes;
|
|
104
|
+
return "queued";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type LiveSidebandWebSocketFactory = (
|
|
108
|
+
url: string,
|
|
109
|
+
headers: Record<string, string>,
|
|
110
|
+
protocols?: string[],
|
|
111
|
+
) => WebSocket;
|
|
112
|
+
|
|
113
|
+
function releaseLiveSidebandAdmission(ws: ServerWebSocket<WsData>): void {
|
|
114
|
+
ws.data.liveTurnAdmissionLease?.release();
|
|
115
|
+
ws.data.liveTurnAdmissionLease = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Send one live-sideband frame to the upstream socket.
|
|
120
|
+
*
|
|
121
|
+
* Bun's `WebSocket.send` accepts `string | Blob | BufferSource`, but the DOM-lib
|
|
122
|
+
* `Buffer` can be backed by a `SharedArrayBuffer`, which `BufferSource` rejects.
|
|
123
|
+
* `Uint8Array.from` copies into a fresh `ArrayBuffer`-backed view, so a frame
|
|
124
|
+
* arriving from `node:buffer` still round-trips byte-for-byte.
|
|
125
|
+
*/
|
|
126
|
+
export function sendUpstreamFrame(upstream: WebSocket, frame: string | Buffer): void {
|
|
127
|
+
if (typeof frame === "string") {
|
|
128
|
+
upstream.send(frame);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
upstream.send(Uint8Array.from(frame));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Translate the close a downstream client sent into one the upstream socket can carry.
|
|
136
|
+
*
|
|
137
|
+
* The client's code is the only evidence of WHY the call ended, and the upstream needs it:
|
|
138
|
+
* a user hanging up (1001) and a protocol fault (1002/1011) are different events on the
|
|
139
|
+
* account, and collapsing both into a bare 1000 erases that at the proxy. Two bounds make
|
|
140
|
+
* the relay safe anyway. Codes a WebSocket endpoint may never send — 1005 and 1006 are
|
|
141
|
+
* status codes the local runtime synthesizes for "no status" and "abnormal", 1015 is
|
|
142
|
+
* TLS-reserved, and anything outside the registered and private ranges is undefined — become
|
|
143
|
+
* 1000, because `upstream.close` throws on them and a throw here would strand the upstream
|
|
144
|
+
* socket. The reason is truncated to the 123-byte control-frame payload limit by bytes, not
|
|
145
|
+
* characters, so a multibyte reason cannot overrun the frame.
|
|
146
|
+
*/
|
|
147
|
+
export function clientCloseForUpstream(code: number, reason?: string): { code: number; reason: string } {
|
|
148
|
+
const sendable = code === 1000
|
|
149
|
+
|| code === 1001
|
|
150
|
+
|| code === 1003
|
|
151
|
+
|| (code >= 1007 && code <= 1011)
|
|
152
|
+
|| (code >= 3000 && code <= 4999);
|
|
153
|
+
let text = reason ?? "";
|
|
154
|
+
while (Buffer.byteLength(text) > 123) text = text.slice(0, -1);
|
|
155
|
+
return { code: sendable ? code : 1000, reason: text };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function finalizeLiveSideband(ws: ServerWebSocket<WsData>, upstream?: WebSocket): void {
|
|
159
|
+
if (upstream && ws.data.liveUpstream !== upstream) return;
|
|
160
|
+
logLiveSidebandStage("relay-closed");
|
|
161
|
+
if (ws.data.liveCloseFallback !== undefined) {
|
|
162
|
+
clearTimeout(ws.data.liveCloseFallback);
|
|
163
|
+
ws.data.liveCloseFallback = undefined;
|
|
164
|
+
}
|
|
165
|
+
ws.data.liveUpstream = undefined;
|
|
166
|
+
ws.data.livePending = undefined;
|
|
167
|
+
ws.data.livePendingBytes = undefined;
|
|
168
|
+
if (ws.data.liveConnectTimer !== undefined) clearTimeout(ws.data.liveConnectTimer);
|
|
169
|
+
if (ws.data.liveSessionTimer !== undefined) clearTimeout(ws.data.liveSessionTimer);
|
|
170
|
+
ws.data.liveConnectTimer = undefined;
|
|
171
|
+
ws.data.liveSessionTimer = undefined;
|
|
172
|
+
ws.data.liveUpstreamHeaders = undefined;
|
|
173
|
+
ws.data.liveUpstreamProtocols = undefined;
|
|
174
|
+
ws.data.liveValidateFrame = undefined;
|
|
175
|
+
if (ws.data.liveAbortListener) ws.data.liveAbortSignal?.removeEventListener("abort", ws.data.liveAbortListener);
|
|
176
|
+
ws.data.liveAbortSignal = undefined;
|
|
177
|
+
ws.data.liveAbortListener = undefined;
|
|
178
|
+
ws.data.cancel = undefined;
|
|
179
|
+
const finish = ws.data.liveFinish;
|
|
180
|
+
ws.data.liveFinish = undefined;
|
|
181
|
+
try { finish?.(ws.data.liveOutcome); }
|
|
182
|
+
catch { console.warn("[audio] upstream accounting failed during close"); }
|
|
183
|
+
finally { releaseLiveSidebandAdmission(ws); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function armLiveSidebandCloseFallback(ws: ServerWebSocket<WsData>, upstream: WebSocket): void {
|
|
187
|
+
if (ws.data.liveCloseFallback !== undefined) return;
|
|
188
|
+
ws.data.liveCloseFallback = setTimeout(() => {
|
|
189
|
+
ws.data.liveCloseFallback = undefined;
|
|
190
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
191
|
+
if (upstream.readyState === WebSocket.CLOSED) {
|
|
192
|
+
finalizeLiveSideband(ws, upstream);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// A close frame was already sent below. Retry once, but never surrender
|
|
196
|
+
// native-main ownership while the authenticated transport remains live.
|
|
197
|
+
try {
|
|
198
|
+
upstream.close(1000, "upstream close timeout");
|
|
199
|
+
} catch {
|
|
200
|
+
/* upstream is already unusable */
|
|
201
|
+
}
|
|
202
|
+
// Some implementations transition synchronously without delivering the
|
|
203
|
+
// close event. That is still an observed CLOSED transport and is safe to
|
|
204
|
+
// finalize. CONNECTING/CLOSING peers keep the lease so profile switching
|
|
205
|
+
// fails at its own bounded drain deadline instead of racing live traffic.
|
|
206
|
+
// The earlier CLOSED check narrowed `readyState` to 0|1|2 in the type
|
|
207
|
+
// system, but the socket can still transition to CLOSED (3) before this
|
|
208
|
+
// fallback fires; the cast keeps the runtime-identical check.
|
|
209
|
+
if ((upstream.readyState as number) === 3) finalizeLiveSideband(ws, upstream);
|
|
210
|
+
}, LIVE_SIDEBAND_CLOSE_FALLBACK_MS);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function closeLiveSidebandBeforeUpgrade(
|
|
214
|
+
upstream: WebSocket,
|
|
215
|
+
release: () => void,
|
|
216
|
+
code = 1000,
|
|
217
|
+
reason = "",
|
|
218
|
+
): void {
|
|
219
|
+
// There is no downstream socket to own this transport yet. Mirror
|
|
220
|
+
// closeLiveSideband's bounded close contract directly: release only after a
|
|
221
|
+
// close event or an observed CLOSED state, never merely after requesting close.
|
|
222
|
+
let released = false;
|
|
223
|
+
let fallback: ReturnType<typeof setTimeout> | undefined;
|
|
224
|
+
const releaseOnce = (): void => {
|
|
225
|
+
if (released) return;
|
|
226
|
+
released = true;
|
|
227
|
+
if (fallback !== undefined) clearTimeout(fallback);
|
|
228
|
+
release();
|
|
229
|
+
};
|
|
230
|
+
upstream.addEventListener("close", releaseOnce, { once: true });
|
|
231
|
+
if (upstream.readyState === WebSocket.CLOSED) {
|
|
232
|
+
releaseOnce();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
fallback = setTimeout(() => {
|
|
236
|
+
if (upstream.readyState === WebSocket.CLOSED) {
|
|
237
|
+
releaseOnce();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
upstream.close(1000, "upstream close timeout");
|
|
242
|
+
} catch {
|
|
243
|
+
/* retain ownership until CLOSED is observed */
|
|
244
|
+
}
|
|
245
|
+
if ((upstream.readyState as number) === 3) releaseOnce();
|
|
246
|
+
}, LIVE_SIDEBAND_CLOSE_FALLBACK_MS);
|
|
247
|
+
try {
|
|
248
|
+
upstream.close(code, reason);
|
|
249
|
+
} catch {
|
|
250
|
+
/* the bounded fallback retries without releasing ownership */
|
|
251
|
+
}
|
|
252
|
+
if ((upstream.readyState as number) === 3) releaseOnce();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
|
|
256
|
+
if (ws.data.liveClosing) return;
|
|
257
|
+
ws.data.liveClosing = true;
|
|
258
|
+
if (ws.data.liveConnectTimer !== undefined) clearTimeout(ws.data.liveConnectTimer);
|
|
259
|
+
if (ws.data.liveSessionTimer !== undefined) clearTimeout(ws.data.liveSessionTimer);
|
|
260
|
+
ws.data.liveConnectTimer = undefined;
|
|
261
|
+
ws.data.liveSessionTimer = undefined;
|
|
262
|
+
ws.data.livePending = undefined;
|
|
263
|
+
ws.data.livePendingBytes = undefined;
|
|
264
|
+
ws.data.cancel = undefined;
|
|
265
|
+
const upstream = ws.data.liveUpstream;
|
|
266
|
+
// Bun's `WebSocket` type narrows `readyState` to 0|1|2 even though the DOM
|
|
267
|
+
// constant CLOSED is 3; the numeric literal is the runtime-identical check.
|
|
268
|
+
if (!upstream || upstream.readyState === 3) {
|
|
269
|
+
finalizeLiveSideband(ws, upstream);
|
|
270
|
+
} else {
|
|
271
|
+
// The sideband holds a native-main admission lease. Do not release it just
|
|
272
|
+
// because the downstream left: its authenticated upstream remains live
|
|
273
|
+
// until the close event arrives or the transport is observed CLOSED. The
|
|
274
|
+
// bounded fallback only retries close; it does not release ownership.
|
|
275
|
+
armLiveSidebandCloseFallback(ws, upstream);
|
|
276
|
+
try {
|
|
277
|
+
upstream.close(code, reason);
|
|
278
|
+
} catch {
|
|
279
|
+
/* the fallback retries close without releasing ownership */
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
284
|
+
ws.close(code, reason);
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
/* client already gone */
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Dial the upstream sideband and report whether its handshake reached 101.
|
|
293
|
+
*
|
|
294
|
+
* Bun's client WebSocket does not surface the upstream handshake status, so the
|
|
295
|
+
* result is "opened" or "failed" and nothing finer. That is sufficient for the
|
|
296
|
+
* property this exists to guarantee: the client is never told the relay is live
|
|
297
|
+
* when it is not. Frames the upstream sends before the client socket exists are
|
|
298
|
+
* captured and handed back by `drain`, because a session preamble such as
|
|
299
|
+
* `session.created` arrives immediately after the upstream opens.
|
|
300
|
+
*/
|
|
301
|
+
export function openLiveSidebandUpstream(
|
|
302
|
+
url: string,
|
|
303
|
+
headers: Record<string, string>,
|
|
304
|
+
createWebSocket: LiveSidebandWebSocketFactory = (socketUrl, socketHeaders) => (
|
|
305
|
+
new WebSocket(socketUrl, { headers: socketHeaders } as unknown as string[])
|
|
306
|
+
),
|
|
307
|
+
timeoutMs: number = LIVE_SIDEBAND_UPSTREAM_OPEN_TIMEOUT_MS,
|
|
308
|
+
signal?: AbortSignal,
|
|
309
|
+
): Promise<LiveSidebandUpstreamOpenResult> {
|
|
310
|
+
return new Promise(resolve => {
|
|
311
|
+
let socket: WebSocket;
|
|
312
|
+
try {
|
|
313
|
+
socket = createWebSocket(url, headers);
|
|
314
|
+
} catch {
|
|
315
|
+
logLiveSidebandStage("upstream-failed", { status: 502, code: "upstream_error" });
|
|
316
|
+
resolve({ ok: false, status: 502, code: "upstream_error", message: "voice upstream connect failed" });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const buffered: Array<string | Buffer> = [];
|
|
321
|
+
let bufferedBytes = 0;
|
|
322
|
+
let capturing = true;
|
|
323
|
+
let settled = false;
|
|
324
|
+
let terminalFailure: LiveSidebandUpstreamFailure | undefined;
|
|
325
|
+
let removeAbortListener = (): void => {};
|
|
326
|
+
|
|
327
|
+
const finish = (result: LiveSidebandUpstreamOpenResult): void => {
|
|
328
|
+
if (settled) return;
|
|
329
|
+
settled = true;
|
|
330
|
+
clearTimeout(timer);
|
|
331
|
+
removeAbortListener();
|
|
332
|
+
if (result.ok) logLiveSidebandStage("upstream-open");
|
|
333
|
+
else logLiveSidebandStage("upstream-failed", { status: result.status, code: result.code });
|
|
334
|
+
resolve(result);
|
|
335
|
+
};
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
const failure = { status: 504, code: "upstream_timeout", message: "voice upstream did not open in time" };
|
|
338
|
+
terminalFailure = failure;
|
|
339
|
+
capturing = false;
|
|
340
|
+
buffered.length = 0;
|
|
341
|
+
bufferedBytes = 0;
|
|
342
|
+
finish({ ok: false, ...failure, socket });
|
|
343
|
+
try {
|
|
344
|
+
socket.close();
|
|
345
|
+
} catch {
|
|
346
|
+
/* ignore */
|
|
347
|
+
}
|
|
348
|
+
}, timeoutMs);
|
|
349
|
+
|
|
350
|
+
const failCapture = (failure: LiveSidebandUpstreamFailure): void => {
|
|
351
|
+
if (!capturing || terminalFailure) return;
|
|
352
|
+
terminalFailure = failure;
|
|
353
|
+
capturing = false;
|
|
354
|
+
buffered.length = 0;
|
|
355
|
+
bufferedBytes = 0;
|
|
356
|
+
finish({ ok: false, ...failure, socket });
|
|
357
|
+
try {
|
|
358
|
+
socket.close(1009, "sideband preamble overflow");
|
|
359
|
+
} catch {
|
|
360
|
+
/* the terminal failure is already retained for the downstream handoff */
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const handoff: LiveSidebandUpstreamHandoff = {
|
|
364
|
+
failure: () => terminalFailure,
|
|
365
|
+
take: () => {
|
|
366
|
+
capturing = false;
|
|
367
|
+
if (terminalFailure) return { ok: false, failure: terminalFailure };
|
|
368
|
+
const frames = buffered.slice();
|
|
369
|
+
buffered.length = 0;
|
|
370
|
+
bufferedBytes = 0;
|
|
371
|
+
return { ok: true, frames };
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
socket.addEventListener("message", event => {
|
|
376
|
+
if (!capturing) return;
|
|
377
|
+
const frameBytes = webSocketFrameBytes(event.data);
|
|
378
|
+
if (exceedsLiveSidebandFrameByteLimit(frameBytes)) {
|
|
379
|
+
failCapture({ status: 502, code: "upstream_overflow", message: "voice upstream preamble frame is too large" });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (buffered.length >= LIVE_SIDEBAND_PENDING_MAX) {
|
|
383
|
+
failCapture({ status: 502, code: "upstream_overflow", message: "voice upstream sent too many preamble frames" });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (exceedsLiveSidebandPendingByteLimit(bufferedBytes, frameBytes)) {
|
|
387
|
+
failCapture({ status: 502, code: "upstream_overflow", message: "voice upstream preamble is too large" });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (typeof event.data === "string") buffered.push(event.data);
|
|
391
|
+
else if (event.data instanceof ArrayBuffer) buffered.push(Buffer.from(new Uint8Array(event.data)));
|
|
392
|
+
else if (ArrayBuffer.isView(event.data)) {
|
|
393
|
+
buffered.push(Buffer.from(new Uint8Array(event.data.buffer, event.data.byteOffset, event.data.byteLength)));
|
|
394
|
+
} else return;
|
|
395
|
+
bufferedBytes += frameBytes;
|
|
396
|
+
});
|
|
397
|
+
socket.addEventListener("open", () => {
|
|
398
|
+
finish({
|
|
399
|
+
ok: true,
|
|
400
|
+
socket,
|
|
401
|
+
handoff,
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
socket.addEventListener("error", () => {
|
|
405
|
+
const failure = { status: 502, code: "upstream_error", message: "voice upstream rejected the sideband join" };
|
|
406
|
+
terminalFailure ??= failure;
|
|
407
|
+
capturing = false;
|
|
408
|
+
buffered.length = 0;
|
|
409
|
+
bufferedBytes = 0;
|
|
410
|
+
finish({ ok: false, ...terminalFailure, socket });
|
|
411
|
+
try {
|
|
412
|
+
socket.close();
|
|
413
|
+
} catch {
|
|
414
|
+
/* the terminal failure is already retained */
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
socket.addEventListener("close", event => {
|
|
418
|
+
const failure = {
|
|
419
|
+
status: 502,
|
|
420
|
+
code: "upstream_error",
|
|
421
|
+
message: `voice upstream closed before opening (code ${event.code})`,
|
|
422
|
+
closeCode: event.code,
|
|
423
|
+
closeReason: event.reason,
|
|
424
|
+
};
|
|
425
|
+
terminalFailure ??= failure;
|
|
426
|
+
capturing = false;
|
|
427
|
+
buffered.length = 0;
|
|
428
|
+
bufferedBytes = 0;
|
|
429
|
+
finish({ ok: false, ...terminalFailure, socket });
|
|
430
|
+
});
|
|
431
|
+
const abortOpen = (): void => {
|
|
432
|
+
const failure = { status: 499, code: "request_cancelled", message: "voice sideband join was cancelled" };
|
|
433
|
+
terminalFailure ??= failure;
|
|
434
|
+
capturing = false;
|
|
435
|
+
buffered.length = 0;
|
|
436
|
+
bufferedBytes = 0;
|
|
437
|
+
finish({ ok: false, ...terminalFailure, socket });
|
|
438
|
+
try {
|
|
439
|
+
socket.close();
|
|
440
|
+
} catch {
|
|
441
|
+
/* the cancelled join no longer owns the socket */
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
if (signal) {
|
|
445
|
+
signal.addEventListener("abort", abortOpen, { once: true });
|
|
446
|
+
removeAbortListener = () => signal.removeEventListener("abort", abortOpen);
|
|
447
|
+
if (signal.aborted) abortOpen();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function attachLiveSidebandUpstream(
|
|
453
|
+
ws: ServerWebSocket<WsData>,
|
|
454
|
+
createWebSocket: LiveSidebandWebSocketFactory = (url, headers, protocols) => (
|
|
455
|
+
new WebSocket(url, { headers, protocols } as unknown as string[])
|
|
456
|
+
),
|
|
457
|
+
): void {
|
|
458
|
+
if (ws.data.liveAbortSignal?.aborted) {
|
|
459
|
+
closeLiveSideband(ws, 1000, "audio connection canceled");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const preOpened = ws.data.liveUpstream;
|
|
463
|
+
let upstream: WebSocket;
|
|
464
|
+
if (preOpened) {
|
|
465
|
+
upstream = preOpened;
|
|
466
|
+
} else {
|
|
467
|
+
const url = ws.data.liveUpstreamUrl;
|
|
468
|
+
if (!url) {
|
|
469
|
+
closeLiveSideband(ws, 1011, "missing upstream");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
// Bun accepts per-handshake headers; the DOM lib types only list protocol arrays.
|
|
474
|
+
upstream = createWebSocket(url, ws.data.liveUpstreamHeaders ?? {}, ws.data.liveUpstreamProtocols);
|
|
475
|
+
} catch {
|
|
476
|
+
closeLiveSideband(ws, 1011, "upstream connect failed");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
ws.data.liveUpstream = upstream;
|
|
481
|
+
ws.data.liveUpstreamHeaders = undefined;
|
|
482
|
+
ws.data.liveUpstreamProtocols = undefined;
|
|
483
|
+
ws.data.liveClosing = false;
|
|
484
|
+
ws.data.cancel = () => closeLiveSideband(ws, 1000, "client closed");
|
|
485
|
+
if (ws.data.liveMaxSessionMs !== undefined) {
|
|
486
|
+
ws.data.liveConnectTimer = setTimeout(() => {
|
|
487
|
+
ws.data.liveOutcome = "timeout";
|
|
488
|
+
closeLiveSideband(ws, 1011, "audio connection timed out");
|
|
489
|
+
}, 10_000);
|
|
490
|
+
ws.data.liveSessionTimer = setTimeout(() => closeLiveSideband(ws, 1000, "audio session expired"), ws.data.liveMaxSessionMs);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
upstream.addEventListener("close", (event) => {
|
|
494
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
495
|
+
if (ws.data.liveFinish && !ws.data.liveClosing && event.code !== 1000) ws.data.liveOutcome = "connect_error";
|
|
496
|
+
ws.data.liveClosing = true;
|
|
497
|
+
finalizeLiveSideband(ws, upstream);
|
|
498
|
+
try {
|
|
499
|
+
const external = ws.data.liveMaxSessionMs !== undefined;
|
|
500
|
+
const validCode = event.code === 1000 || (event.code >= 1001 && event.code <= 1014 && ![1004, 1005, 1006].includes(event.code))
|
|
501
|
+
|| (event.code >= 3000 && event.code <= 4999);
|
|
502
|
+
ws.close(external && !validCode ? 1011 : event.code || 1000, external ? "audio upstream closed" : event.reason || "");
|
|
503
|
+
} catch {
|
|
504
|
+
/* ignore */
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
upstream.addEventListener("error", () => {
|
|
508
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
509
|
+
if (ws.data.liveFinish && !ws.data.liveClosing) ws.data.liveOutcome = "connect_error";
|
|
510
|
+
closeLiveSideband(ws, 1011, "upstream error");
|
|
511
|
+
});
|
|
512
|
+
if (ws.data.liveAbortSignal) {
|
|
513
|
+
ws.data.liveAbortListener = () => closeLiveSideband(ws, 1000, "audio connection canceled");
|
|
514
|
+
ws.data.liveAbortSignal.addEventListener("abort", ws.data.liveAbortListener, { once: true });
|
|
515
|
+
if (ws.data.liveAbortSignal.aborted) closeLiveSideband(ws, 1000, "audio connection canceled");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (preOpened) {
|
|
519
|
+
// The upstream opened before this socket existed, so its `open` event has already
|
|
520
|
+
// fired and the listener below will never run. Its early frames were captured for
|
|
521
|
+
// us; forward the capture now rather than dropping the session preamble.
|
|
522
|
+
const handoff = ws.data.liveUpstreamHandoff;
|
|
523
|
+
ws.data.liveUpstreamHandoff = undefined;
|
|
524
|
+
const takeover = handoff?.take();
|
|
525
|
+
if (!takeover?.ok || preOpened.readyState !== WebSocket.OPEN) {
|
|
526
|
+
const failure = takeover && !takeover.ok ? takeover.failure : undefined;
|
|
527
|
+
closeLiveSideband(
|
|
528
|
+
ws,
|
|
529
|
+
failure?.closeCode ?? 1011,
|
|
530
|
+
failure?.closeReason ?? "upstream closed before relay attachment",
|
|
531
|
+
);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
ws.data.liveOpened = true;
|
|
535
|
+
// The upstream opened before this socket existed, so the "open" listener
|
|
536
|
+
// below can never fire for it. Disarm the connect watchdog exactly as that
|
|
537
|
+
// listener would, or every session with a max lifetime is force-closed ten
|
|
538
|
+
// seconds after attach. The session timer stays armed: it bounds the whole
|
|
539
|
+
// session, not the connect phase.
|
|
540
|
+
if (ws.data.liveConnectTimer !== undefined) clearTimeout(ws.data.liveConnectTimer);
|
|
541
|
+
ws.data.liveConnectTimer = undefined;
|
|
542
|
+
logLiveSidebandStage("relay-attached");
|
|
543
|
+
for (const frame of takeover.frames) {
|
|
544
|
+
try {
|
|
545
|
+
// Mirror the live message listener exactly: same ceiling, same diagnostic
|
|
546
|
+
// record. These frames are upstream-to-client like any other.
|
|
547
|
+
if (exceedsLiveSidebandFrameByteLimit(webSocketFrameBytes(frame))) {
|
|
548
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
logLiveSidebandFrame("u2c", frame);
|
|
552
|
+
ws.send(frame);
|
|
553
|
+
} catch {
|
|
554
|
+
closeLiveSideband(ws, 1011, "client send failed");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
upstream.addEventListener("open", () => {
|
|
561
|
+
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
562
|
+
ws.data.liveOpened = true;
|
|
563
|
+
if (ws.data.liveConnectTimer !== undefined) clearTimeout(ws.data.liveConnectTimer);
|
|
564
|
+
ws.data.liveConnectTimer = undefined;
|
|
565
|
+
logLiveSidebandStage("relay-attached");
|
|
566
|
+
// An accepted transport alone does not prove inference/quota recovery.
|
|
567
|
+
// Keep healthy closes neutral; explicit transport failures are recorded below.
|
|
568
|
+
const pending = ws.data.livePending ?? [];
|
|
569
|
+
ws.data.livePending = undefined;
|
|
570
|
+
ws.data.livePendingBytes = undefined;
|
|
571
|
+
for (const frame of pending) {
|
|
572
|
+
try {
|
|
573
|
+
sendUpstreamFrame(upstream, frame);
|
|
574
|
+
} catch {
|
|
575
|
+
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
upstream.addEventListener("message", (event) => {
|
|
581
|
+
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
582
|
+
try {
|
|
583
|
+
if (exceedsLiveSidebandFrameByteLimit(webSocketFrameBytes(event.data))) {
|
|
584
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
logLiveSidebandFrame("u2c", event.data);
|
|
588
|
+
let sent: number;
|
|
589
|
+
if (typeof event.data === "string") sent = ws.send(event.data);
|
|
590
|
+
else if (event.data instanceof ArrayBuffer) sent = ws.send(event.data);
|
|
591
|
+
else if (ArrayBuffer.isView(event.data)) {
|
|
592
|
+
sent = ws.send(event.data.buffer.slice(event.data.byteOffset, event.data.byteOffset + event.data.byteLength));
|
|
593
|
+
} else sent = ws.send(event.data as Buffer);
|
|
594
|
+
if (ws.data.liveMaxSessionMs !== undefined && (sent === 0 || ws.getBufferedAmount() > MAX_WS_FRAME_BYTES)) {
|
|
595
|
+
closeLiveSideband(ws, 1013, "audio client backpressure");
|
|
596
|
+
}
|
|
597
|
+
} catch {
|
|
598
|
+
closeLiveSideband(ws, 1011, "client send failed");
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|