sliccy 6.137.0 → 6.138.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/dist/node-server/cdp-proxy/chrome-reconnect.d.ts +141 -0
- package/dist/node-server/cdp-proxy/chrome-reconnect.js +206 -0
- package/dist/node-server/cdp-proxy/client-frame-buffer.d.ts +118 -0
- package/dist/node-server/cdp-proxy/client-frame-buffer.js +141 -0
- package/dist/node-server/cdp-proxy/close-codes.d.ts +28 -0
- package/dist/node-server/cdp-proxy/close-codes.js +28 -0
- package/dist/node-server/index.js +128 -28
- package/dist/ui/.vite/manifest.json +359 -624
- package/dist/ui/assets/SKILL-Cj8ElTxg.js +1 -0
- package/dist/ui/assets/{SKILL-AGJ6NFW_.js → SKILL-DfOG7jCj.js} +22 -19
- package/dist/ui/assets/account-store-CEzlUstH.js +63 -0
- package/dist/ui/assets/account-store-DD8DCyBK.js +1 -0
- package/dist/ui/assets/{adobe-jqcr8RHN.js → adobe-Cdd_E6rB.js} +1 -1
- package/dist/ui/assets/adobe-DhfJgCHq.js +2 -0
- package/dist/ui/assets/{agent-bridge-S8j2812Y.js → agent-bridge-D0DMll5k.js} +10 -9
- package/dist/ui/assets/agent-panels-EBOP0pAa.js +1 -0
- package/dist/ui/assets/{apply-layout-D3gOL_38.js → apply-layout-B2EQevMq.js} +1 -1
- package/dist/ui/assets/{apps-CSQRDxWq.js → apps-BR3cRjE_.js} +2 -2
- package/dist/ui/assets/{azure-openai-CRJdUlny.js → azure-openai-CarvB7O9.js} +1 -1
- package/dist/ui/assets/{azure-openai-CcxSOX33.js → azure-openai-x_uz-FS4.js} +2 -2
- package/dist/ui/assets/bedrock-camp-C8BwH8Mj.js +1 -0
- package/dist/ui/assets/bedrock-camp-D1dak7rV.js +1 -0
- package/dist/ui/assets/bedrock-camp-compat-99DjWBHV.js +1 -0
- package/dist/ui/assets/bedrock-camp-compat-BeCSBUrZ.js +1 -0
- package/dist/ui/assets/capability-CTDMHYYU.js +2 -0
- package/dist/ui/assets/cdp-DseOo5rP.js +881 -0
- package/dist/ui/assets/{cerebras-CWb9p9Rf.js → cerebras-B1AF6wK5.js} +1 -1
- package/dist/ui/assets/{cerebras-DO9w7qJ8.js → cerebras-fR81CWcL.js} +1 -1
- package/dist/ui/assets/{claude-model-version-BJLrDjmH.js → claude-model-version-CSATLzrT.js} +1 -1
- package/dist/ui/assets/claude-model-version-m5AAX9Oe.js +1 -0
- package/dist/ui/assets/composer-speech-B9BMS4Az.js +1 -0
- package/dist/ui/assets/{connect-surface-IBJGJKXd.js → connect-surface-DP45y859.js} +1 -1
- package/dist/ui/assets/{directed-approval-B2nSvGGo.js → directed-approval-CuXuJ8VY.js} +1 -1
- package/dist/ui/assets/dist-CMD3ZuiV.js +1 -0
- package/dist/ui/assets/dist-CWHNqkcj.js +1 -0
- package/dist/ui/assets/dist-CrVgSn_b.js +6 -0
- package/dist/ui/assets/dist-DkG8wgj1.js +23 -0
- package/dist/ui/assets/esp8266-1y0C3qJ1.js +1 -0
- package/dist/ui/assets/esptool-operations-CiccBbBW.js +12 -0
- package/dist/ui/assets/extension-bridge-transport-zMxrmuc2.js +1 -0
- package/dist/ui/assets/{follower-navigate-watcher-JQ0b_cW8.js → follower-navigate-watcher-dqOOG5CZ.js} +1 -1
- package/dist/ui/assets/{fs-BoEnJw4s.js → fs-B8U5EC4S.js} +1 -1
- package/dist/ui/assets/fs-DZiSOeiV.js +2 -0
- package/dist/ui/assets/{github-C0ynjd_v.js → github-BKPC-6V7.js} +1 -1
- package/dist/ui/assets/github-Rph4kR8u.js +2 -0
- package/dist/ui/assets/github-copilot-B45fsbJE.js +2 -0
- package/dist/ui/assets/{github-copilot-Dun7obqf.js → github-copilot-m7FAkLKL.js} +1 -1
- package/dist/ui/assets/{github-install-CCj8_2v5.js → github-install-DEyHFxj7.js} +1 -1
- package/dist/ui/assets/{github-zip-Dy9qcuNt.js → github-zip-D4owU6oz.js} +1 -1
- package/dist/ui/assets/{hear-CNQSuR1y.js → hear-CvLWI56h.js} +1 -1
- package/dist/ui/assets/{help-1e8yOlr-.js → help-C4EiNzBg.js} +1 -1
- package/dist/ui/assets/icons-BXcp4CBT.js +1 -0
- package/dist/ui/assets/{js-realm-worker-rSnHM5iE.js → js-realm-worker-t0tgCFRF.js} +1 -1
- package/dist/ui/assets/{kernel-worker-DkiXdKIm.js → kernel-worker-HXzODg96.js} +602 -601
- package/dist/ui/assets/legacy-api-aliases-bAJ0Klzq.js +1 -0
- package/dist/ui/assets/{lick-ws-bridge-Bo5sH7lf.js → lick-ws-bridge-Bb2eVADZ.js} +1 -1
- package/dist/ui/assets/{local-llm-BG6FnzbS.js → local-llm-BRXVAmOc.js} +1 -1
- package/dist/ui/assets/{local-llm-CHUrcvmR.js → local-llm-CGxqNPHi.js} +1 -1
- package/dist/ui/assets/main-CRJcIlg2.js +22 -0
- package/dist/ui/assets/{main-cherry-Ibu56rin.js → main-cherry-Cz3S2BJy.js} +1 -1
- package/dist/ui/assets/{mount-rNqMCM6y.js → mount-DSBwGVEx.js} +3 -3
- package/dist/ui/assets/{mount-DxsGYJn5.js → mount-jaKXBhl1.js} +1 -1
- package/dist/ui/assets/{mount-recovery-BMIs8Fmy.js → mount-recovery-DL3Skeu-.js} +1 -1
- package/dist/ui/assets/navigation-watcher-COUKtxwQ.js +1 -0
- package/dist/ui/assets/new-session-s3Xs25kA.js +2 -0
- package/dist/ui/assets/oauth-bootstrap-CuNwuZBb.js +2 -0
- package/dist/ui/assets/onboarding-orchestrator-BmeBv4Gp.js +1 -0
- package/dist/ui/assets/{openai-codex-XqP20i_Q.js → openai-codex-C3MfGQX9.js} +1 -1
- package/dist/ui/assets/openai-codex-cu3w-X1S.js +1 -0
- package/dist/ui/assets/openrouter-D4XcSujz.js +1 -0
- package/dist/ui/assets/{openrouter-CeWQMDvQ.js → openrouter-PY89nOyM.js} +1 -1
- package/dist/ui/assets/{openrouter-oauth-BrfPX9xc.js → openrouter-oauth-Bf6qJKa8.js} +1 -1
- package/dist/ui/assets/openrouter-oauth-D26-a5--.js +1 -0
- package/dist/ui/assets/panel-rpc-handlers-DZBKznj3.js +2 -0
- package/dist/ui/assets/panelize-shell-DrwwO2I-.js +4 -0
- package/dist/ui/assets/{provider-BrN0oYcu.js → provider-BiqZhM5h.js} +1 -1
- package/dist/ui/assets/provider-QB1F4Xys.js +2 -0
- package/dist/ui/assets/provider-settings-DaR5wS_z.js +1 -0
- package/dist/ui/assets/provider-store-access-BDFJqulW.js +1 -0
- package/dist/ui/assets/provider-store-access-CYkQdTfa.js +1 -0
- package/dist/ui/assets/{providers-uyqwOc6n.js → providers-DpiQagGT.js} +1 -1
- package/dist/ui/assets/providers-sxOhGKYB.js +2 -0
- package/dist/ui/assets/{quick-llm-KuQledjU.js → quick-llm-CKHfcFuQ.js} +1 -1
- package/dist/ui/assets/{recovery-screen-CEWIaXnj.js → recovery-screen-DmT0rFlT.js} +1 -1
- package/dist/ui/assets/{rest-ops-CLpS_ilI.js → rest-ops-B8ZocTVM.js} +1 -1
- package/dist/ui/assets/{run-t78pCdsT.js → run-BcAa2ols.js} +1 -1
- package/dist/ui/assets/{run-Dt5cxT0F.js → run-BrA8TEwf.js} +1 -1
- package/dist/ui/assets/{run-CIYc2Qb2.js → run-C9g_NP0v.js} +2 -2
- package/dist/ui/assets/{setup-feature-flags-remote-bNwKlj1P.js → setup-feature-flags-remote-CLGA0pxR.js} +1 -1
- package/dist/ui/assets/sidecar-repair-Csokye05.js +1 -0
- package/dist/ui/assets/silent-renew-backoff-CTLMurGM.js +1 -0
- package/dist/ui/assets/{skills-BOdEsYE9.js → skills-CVnzD-KG.js} +1 -1
- package/dist/ui/assets/slicc-editor-D-eO_pjd.js +23 -0
- package/dist/ui/assets/slicc-editor-Sdfe-_x_.js +1 -0
- package/dist/ui/assets/slicc-version-BNbx6DEv.js +1 -0
- package/dist/ui/assets/{snapshot-g-snbDyA.js → snapshot-Cc4QPfvM.js} +1 -1
- package/dist/ui/assets/{snapshot-features-BTV-hsWa.js → snapshot-features-B86A9a5T.js} +1 -1
- package/dist/ui/assets/soundscape-Bq_V_6Yw.js +1 -0
- package/dist/ui/assets/{speak-BxNdNjKi.js → speak-CqbRi6pA.js} +1 -1
- package/dist/ui/assets/sprinkle-manager-CqK5Gzty.js +1 -0
- package/dist/ui/assets/sprinkle-renderer-BGbM2VW8.js +286 -0
- package/dist/ui/assets/{state-Bz2C8x64.js → state-CBPU3yix.js} +1 -1
- package/dist/ui/assets/{store-DB0WokKu.js → store-CfgG6MCL.js} +1 -1
- package/dist/ui/assets/{store-BHve8pmc.js → store-Wcm463vo.js} +1 -1
- package/dist/ui/assets/{sudo-B8TRUg6E.js → sudo-BGnIjc_Q.js} +1 -1
- package/dist/ui/assets/sync-dialog-Cd8n4IiV.js +1 -0
- package/dist/ui/assets/telemetry-BWAgv6Hm.js +3 -0
- package/dist/ui/assets/temperature-support-B2rDQKe1.js +1 -0
- package/dist/ui/assets/temperature-support-LHBt3FMX.js +1 -0
- package/dist/ui/assets/transformers-env-NnCJIbOS.js +1 -0
- package/dist/ui/assets/tray-leader-DaLUK8LB.js +1 -0
- package/dist/ui/assets/tray-sidecar-iqsBxl7p.js +2 -0
- package/dist/ui/assets/{upgrade-detection-BpAx3I8x.js → upgrade-detection-BHqNxLM0.js} +1 -1
- package/dist/ui/assets/voice-reply-wpoJjCii.js +1 -0
- package/dist/ui/assets/wc-attach-BRK3Sw08.js +13 -0
- package/dist/ui/assets/wc-browser-CuZwpwEQ.js +1 -0
- package/dist/ui/assets/wc-extension-DBJold4K.js +2 -0
- package/dist/ui/assets/wc-follower-DGKQnqCu.js +17 -0
- package/dist/ui/assets/wc-follower-oauth-D5QCyPfS.js +5 -0
- package/dist/ui/assets/wc-live-CyqSJGYW.js +400 -0
- package/dist/ui/assets/wc-nav-DhQemEAd.js +2 -0
- package/dist/ui/assets/wc-onboarding-CmGOAIm8.js +2 -0
- package/dist/ui/assets/wc-placeholder-B6IIK-nS.js +4 -0
- package/dist/ui/assets/{wc-settings-C7_YdY0L.js → wc-settings-BvUwaUq0.js} +3 -3
- package/dist/ui/assets/wc-shell-maFgJD5l.js +125 -0
- package/dist/ui/assets/{wc-shortcut-config-2ywqohA1.js → wc-shortcut-config-BJkRa19F.js} +1 -1
- package/dist/ui/assets/{src-hh5Jc_tK.js → wc-shortcuts-BvdDd3KG.js} +236 -186
- package/dist/ui/assets/wc-sprinkles-BzEC5lo2.js +2 -0
- package/dist/ui/assets/wc-transcript-export-D3kvMKNz.js +2 -0
- package/dist/ui/assets/wc-tray-DEXQTMgX.js +5 -0
- package/dist/ui/assets/welcome-detection-CQm-JCYe.js +1 -0
- package/dist/ui/assets/whisper-session-BZBk770W.js +2 -0
- package/dist/ui/assets/xai-grok-BO8zcLws.js +1 -0
- package/dist/ui/assets/{xai-grok-DlChQnzy.js → xai-grok-Kgw4mVWn.js} +1 -1
- package/dist/ui/index.html +7 -12
- package/dist/ui/packages/webapp/index.html +7 -12
- package/dist/ui/slicc-editor.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/SKILL--AFE5RE3.js +0 -1
- package/dist/ui/assets/account-store-BGNETLsm.js +0 -63
- package/dist/ui/assets/account-store-CM0gf4zY.js +0 -1
- package/dist/ui/assets/adobe-DyaLZq-8.js +0 -2
- package/dist/ui/assets/agent-handle-CNeA3sd0.js +0 -1
- package/dist/ui/assets/agent-message-to-chat-Df4an5Ei.js +0 -1
- package/dist/ui/assets/agent-panels-B92KQapO.js +0 -1
- package/dist/ui/assets/bedrock-camp-Djf-aXdl.js +0 -1
- package/dist/ui/assets/bedrock-camp-H3Byazn0.js +0 -1
- package/dist/ui/assets/bedrock-camp-compat-CYnC9BQI.js +0 -1
- package/dist/ui/assets/bedrock-camp-compat-CgBMwna8.js +0 -1
- package/dist/ui/assets/capability-Bxl0SE1s.js +0 -2
- package/dist/ui/assets/cdp-DKhmYptD.js +0 -881
- package/dist/ui/assets/chat-session-store-pWb1-jNp.js +0 -1
- package/dist/ui/assets/claude-model-version-CJiITCDG.js +0 -1
- package/dist/ui/assets/composer-speech-CElQJ4q_.js +0 -1
- package/dist/ui/assets/db-cKsBsMMN.js +0 -1
- package/dist/ui/assets/dictation-priming-CNllGgNq.js +0 -1
- package/dist/ui/assets/dip-DySyh98z.js +0 -233
- package/dist/ui/assets/dist-BamC4SL9.js +0 -1
- package/dist/ui/assets/dist-Bwf-JITi.js +0 -12
- package/dist/ui/assets/dist-CJQsHfm3.js +0 -6
- package/dist/ui/assets/dist-Cre853-J.js +0 -23
- package/dist/ui/assets/dist-D9gC7MoR.js +0 -1
- package/dist/ui/assets/esptool-operations-DEqDNikQ.js +0 -12
- package/dist/ui/assets/export-provider-CqI0pk50.js +0 -1
- package/dist/ui/assets/extension-bridge-transport-DfRNjx8c.js +0 -1
- package/dist/ui/assets/follower-presentation-DqBw0YiV.js +0 -1
- package/dist/ui/assets/fs-Bm2LRyUH.js +0 -2
- package/dist/ui/assets/github-ZxrIXroz.js +0 -2
- package/dist/ui/assets/github-copilot-DK9BvwFj.js +0 -2
- package/dist/ui/assets/host-command-29ain4NA.js +0 -1
- package/dist/ui/assets/icons-D4SDtJW0.js +0 -1
- package/dist/ui/assets/image-markers-BFDmhPy4.js +0 -1
- package/dist/ui/assets/layout-schema-m9_VAwOw.js +0 -1
- package/dist/ui/assets/legacy-api-aliases-45zvHmpJ.js +0 -1
- package/dist/ui/assets/main-L2EM-k_T.js +0 -22
- package/dist/ui/assets/navigation-watcher-C8xRyQBd.js +0 -1
- package/dist/ui/assets/new-session-CZy_BIUd.js +0 -2
- package/dist/ui/assets/oauth-bootstrap-D8Q37TT-.js +0 -2
- package/dist/ui/assets/onboarding-orchestrator-CpVkmcoM.js +0 -1
- package/dist/ui/assets/openai-codex-BoGyqGTb.js +0 -1
- package/dist/ui/assets/openrouter-d0ZBwe7-.js +0 -1
- package/dist/ui/assets/openrouter-oauth-DFBYXCcd.js +0 -1
- package/dist/ui/assets/panel-registry-CJdOibpf.js +0 -1
- package/dist/ui/assets/panel-rpc-handlers-CsyJxXeR.js +0 -2
- package/dist/ui/assets/panelize-shell-C4WoeBi-.js +0 -4
- package/dist/ui/assets/preview-url-r19H6zH1.js +0 -1
- package/dist/ui/assets/provider-bztYEW41.js +0 -2
- package/dist/ui/assets/provider-settings-3-JiPamh.js +0 -1
- package/dist/ui/assets/provider-settings-DT-N1OD6.js +0 -2
- package/dist/ui/assets/provider-store-access-Bch5GC1b.js +0 -2
- package/dist/ui/assets/provider-store-access-KBqWyi3A.js +0 -1
- package/dist/ui/assets/providers-Y6vDOS61.js +0 -2
- package/dist/ui/assets/rest-paths-DJCfupdz.js +0 -1
- package/dist/ui/assets/session-freezer-DWq5xKa-.js +0 -163
- package/dist/ui/assets/setup-standalone-prelude-DU7F98aW.js +0 -2
- package/dist/ui/assets/setup-sudo-kBAwNpgU.js +0 -2
- package/dist/ui/assets/sidecar-repair-431vRT2V.js +0 -1
- package/dist/ui/assets/silent-renew-backoff-YUr3mR2G.js +0 -1
- package/dist/ui/assets/slicc-editor-Bhg4cWBq.js +0 -12
- package/dist/ui/assets/slicc-editor-CL_D1iJB.js +0 -1
- package/dist/ui/assets/slicc-version-BH39IvgC.js +0 -1
- package/dist/ui/assets/soundscape-TvriK11t.js +0 -1
- package/dist/ui/assets/soundscape-cues-5_sT5Un6.js +0 -1
- package/dist/ui/assets/speech-C5U8h4In.js +0 -1
- package/dist/ui/assets/sprinkle-bridge-CeRz-DjR.js +0 -2
- package/dist/ui/assets/sprinkle-manager-jHNM2oxy.js +0 -1
- package/dist/ui/assets/sprinkle-renderer-Ce6YjEek.js +0 -281
- package/dist/ui/assets/surface-fullscreen-4YjALjE7.js +0 -1
- package/dist/ui/assets/sync-dialog-BBo1RfX_.js +0 -1
- package/dist/ui/assets/telemetry-MJ77Fp1a.js +0 -3
- package/dist/ui/assets/temperature-support-BCoY8YuX.js +0 -1
- package/dist/ui/assets/temperature-support-C046iRqM.js +0 -1
- package/dist/ui/assets/timestamp-preference-sKwBl5I8.js +0 -2
- package/dist/ui/assets/transformers-env-BmBEKck6.js +0 -1
- package/dist/ui/assets/tray-leader-DabyaEs-.js +0 -1
- package/dist/ui/assets/tray-sidecar-Zx9F34-1.js +0 -2
- package/dist/ui/assets/tray-sync-protocol-jKTx-dCF.js +0 -1
- package/dist/ui/assets/tray-webrtc-uW6sTyIA.js +0 -1
- package/dist/ui/assets/trusted-layer-ghPA1ENf.js +0 -1
- package/dist/ui/assets/voice-reply-B43Nr1Xr.js +0 -1
- package/dist/ui/assets/wc-attach-Cr1o6HzV.js +0 -13
- package/dist/ui/assets/wc-browser-Ctsf5Y23.js +0 -1
- package/dist/ui/assets/wc-extension-BjK-tKBf.js +0 -2
- package/dist/ui/assets/wc-float-label-BkfYPqdF.js +0 -1
- package/dist/ui/assets/wc-follower-CH9Ey9IG.js +0 -17
- package/dist/ui/assets/wc-follower-oauth-DCz20htV.js +0 -5
- package/dist/ui/assets/wc-live-D_Z9cUh5.js +0 -8
- package/dist/ui/assets/wc-nav-CPhi1Chn.js +0 -2
- package/dist/ui/assets/wc-onboarding-DVK6dIOf.js +0 -2
- package/dist/ui/assets/wc-placeholder-DcsoOquv.js +0 -4
- package/dist/ui/assets/wc-shell-BtcgWp4y.js +0 -125
- package/dist/ui/assets/wc-shortcuts-DESTsoW8.js +0 -41
- package/dist/ui/assets/wc-sprinkles-CDQGgIIW.js +0 -2
- package/dist/ui/assets/wc-transcript-export-DdEuCfi6.js +0 -2
- package/dist/ui/assets/wc-tray-gcTDn_z9.js +0 -5
- package/dist/ui/assets/welcome-detection-Ddf03FcG.js +0 -1
- package/dist/ui/assets/whisper-session-DW7HkDu2.js +0 -2
- package/dist/ui/assets/xai-grok-xFpjw8yf.js +0 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome-leg reconnect supervisor for the node-server `/cdp` proxy.
|
|
3
|
+
*
|
|
4
|
+
* Chrome's browser-level debugger socket drops on its own (`messageTooLarge`,
|
|
5
|
+
* an inbound-queue overflow, a devtools window stealing the single slot). When
|
|
6
|
+
* it does, Chrome discards EVERY CDP session behind it. Until now node-server
|
|
7
|
+
* just cleared `chromeWs` and dropped client frames on the floor until a NEW
|
|
8
|
+
* `/cdp` client connected — i.e. until the SLICC tab was reloaded by hand
|
|
9
|
+
* (issue #2417, `DIAGNOSIS.md` §2.6). swift-server already reconnects
|
|
10
|
+
* (`CDPProxy.scheduleChromeReconnect` / `runChromeReconnectLoop`); this brings
|
|
11
|
+
* node-server to parity and adds the missing half both floats needed: the
|
|
12
|
+
* active client is closed with `CDP_UPSTREAM_RESET_CLOSE_CODE` so the page
|
|
13
|
+
* re-dials and resets its session state instead of issuing commands against
|
|
14
|
+
* sessions Chrome has forgotten.
|
|
15
|
+
*
|
|
16
|
+
* ## Reconnect policy (identical in node-server and swift-server)
|
|
17
|
+
*
|
|
18
|
+
* Retry indefinitely with a 1 s delay between attempts, until shutdown — there
|
|
19
|
+
* is no attempt cap. Close the active client with 4002 `upstream-reset` after
|
|
20
|
+
* the 3rd consecutive failure, so it does not hang on a proxy whose Chrome leg
|
|
21
|
+
* is gone. After a successful reconnect, reset the active client ONLY if it is
|
|
22
|
+
* the same client that held the slot when the Chrome leg went down — it is the
|
|
23
|
+
* one whose sessions Chrome discarded. A client that connected during the
|
|
24
|
+
* outage never had sessions on the dead leg and its buffered frames were just
|
|
25
|
+
* flushed onto the replacement connection, so closing it with 4002 would make
|
|
26
|
+
* the page retry commands that already ran (a sessionless `Target.createTarget`
|
|
27
|
+
* opens a duplicate tab). Never leave a clientless buffer around: buffered
|
|
28
|
+
* frames are dropped whenever the client that wrote them loses the slot.
|
|
29
|
+
*/
|
|
30
|
+
import { type ClientFrameBuffer } from './client-frame-buffer.js';
|
|
31
|
+
/** Delay before each reconnect attempt. Mirrors swift `defaultReconnectDelayNanoseconds`. */
|
|
32
|
+
export declare const CHROME_RECONNECT_DELAY_MS = 1000;
|
|
33
|
+
/**
|
|
34
|
+
* Consecutive failed attempts after which the active client is cut loose with
|
|
35
|
+
* 4002 rather than left hanging on a proxy whose Chrome leg is gone. The loop
|
|
36
|
+
* keeps retrying afterwards. Mirrors swift `upstreamResetFailureThreshold`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CHROME_RECONNECT_FAILURE_THRESHOLD = 3;
|
|
39
|
+
export interface ChromeReconnectDeps {
|
|
40
|
+
/** Re-discover Chrome's browser-level ws URL (via `/json/version`). */
|
|
41
|
+
discoverChromeWsUrl: () => Promise<string>;
|
|
42
|
+
/** Connect the Chrome leg to `url`; rejects when the socket never opens. */
|
|
43
|
+
connectChrome: (url: string) => Promise<void>;
|
|
44
|
+
/** Close the active `/cdp` client with the upstream-reset code, if any. */
|
|
45
|
+
resetClient: (reason: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Id of the client holding the single `/cdp` slot right now, or null when it
|
|
48
|
+
* is empty. Sampled on every leg drop and compared again after a successful
|
|
49
|
+
* reconnect: only the holder that saw the dead leg carries stale sessions.
|
|
50
|
+
*/
|
|
51
|
+
activeClientId: () => number | null;
|
|
52
|
+
/** True once graceful shutdown started — no reconnect, no client reset. */
|
|
53
|
+
isShuttingDown: () => boolean;
|
|
54
|
+
/**
|
|
55
|
+
* True when the Chrome leg is already open again — e.g. a NEW `/cdp` client
|
|
56
|
+
* connected during the delay and `handleCdpClient` rebuilt it. That client
|
|
57
|
+
* has no stale sessions, so the loop must exit WITHOUT evicting it.
|
|
58
|
+
*/
|
|
59
|
+
isChromeLegHealthy?: () => boolean;
|
|
60
|
+
/** Log one `[cdp-proxy]`-prefixed line (caller applies `CliLogDedup`). */
|
|
61
|
+
log: (line: string) => void;
|
|
62
|
+
sleep?: (ms: number) => Promise<void>;
|
|
63
|
+
delayMs?: number;
|
|
64
|
+
failureThreshold?: number;
|
|
65
|
+
}
|
|
66
|
+
/** The proxy state a Chrome-leg drop mutates (a slice of `ServerState`). */
|
|
67
|
+
export interface ChromeLegState<Socket = unknown> {
|
|
68
|
+
chromeWs: Socket | null;
|
|
69
|
+
/** Id of the Chrome connection `chromeWs` refers to (0 = never connected). */
|
|
70
|
+
chromeConnectionId: number;
|
|
71
|
+
/** Id of the client holding the single `/cdp` slot, or null for none. */
|
|
72
|
+
activeClientId: number | null;
|
|
73
|
+
/** Non-null = buffering Client→Chrome frames instead of forwarding them. */
|
|
74
|
+
messageBuffer: ClientFrameBuffer | null;
|
|
75
|
+
shuttingDown: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Record that the Chrome leg went away: clear the socket and start buffering
|
|
79
|
+
* client frames so they survive the gap (node-server used to drop them until a
|
|
80
|
+
* NEW `/cdp` client connected). Returns false — meaning "do not schedule a
|
|
81
|
+
* reconnect" — when the event came from a socket a newer connect already
|
|
82
|
+
* replaced, or when the server is shutting down.
|
|
83
|
+
*
|
|
84
|
+
* The buffer opened here is tagged with the connection that just died, so the
|
|
85
|
+
* flush onto the REPLACEMENT connection discards it: those frames name sessions
|
|
86
|
+
* Chrome dropped with the old socket, and the client is being reset anyway.
|
|
87
|
+
*/
|
|
88
|
+
export declare function markChromeLegDown<Socket>(state: ChromeLegState<Socket>, droppedWs: Socket): boolean;
|
|
89
|
+
/** Minimal `ws` surface needed to evict the active `/cdp` client. */
|
|
90
|
+
export interface ResettableClientSocket {
|
|
91
|
+
readyState: number;
|
|
92
|
+
close(code?: number, reason?: string): void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Close the active `/cdp` client with the upstream-reset code so the page
|
|
96
|
+
* re-dials and rebuilds its (now dead) CDP sessions. Returns true when a close
|
|
97
|
+
* was actually issued.
|
|
98
|
+
*/
|
|
99
|
+
export declare function closeClientForUpstreamReset(client: ResettableClientSocket | null | undefined, reason: string, log: (line: string) => void): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Owns at most one in-flight reconnect loop. `schedule()` is idempotent — a
|
|
102
|
+
* Chrome socket that fires both `error` and `close` schedules once.
|
|
103
|
+
*/
|
|
104
|
+
export declare class ChromeReconnectController {
|
|
105
|
+
private readonly deps;
|
|
106
|
+
private task;
|
|
107
|
+
private cancelled;
|
|
108
|
+
/** Slot holder at the most recent Chrome-leg drop — the one with stale sessions. */
|
|
109
|
+
private slotHolderAtDrop;
|
|
110
|
+
constructor(deps: ChromeReconnectDeps);
|
|
111
|
+
/** True while a reconnect loop is running. */
|
|
112
|
+
get reconnecting(): boolean;
|
|
113
|
+
/** Start the reconnect loop unless one is already running or we're shutting down. */
|
|
114
|
+
schedule(reason: string): void;
|
|
115
|
+
/** Stop the loop (graceful shutdown). Sticky — the controller is not reused. */
|
|
116
|
+
cancel(): void;
|
|
117
|
+
/** Resolves when the current reconnect loop (if any) has finished. For tests. */
|
|
118
|
+
settled(): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Retry until the leg is back or shutdown stops us — no attempt cap, so a
|
|
121
|
+
* Chrome that comes back after a long outage is picked up without needing a
|
|
122
|
+
* fresh client to drive discovery (swift parity).
|
|
123
|
+
*/
|
|
124
|
+
private run;
|
|
125
|
+
/** One discover + connect round. Returns true when the leg is back. */
|
|
126
|
+
private attempt;
|
|
127
|
+
/**
|
|
128
|
+
* Close the client that held the slot when the leg dropped: Chrome discarded
|
|
129
|
+
* every session with the old socket, so that page's cached sessionIds are
|
|
130
|
+
* dead and it must re-dial with a clean slate.
|
|
131
|
+
*
|
|
132
|
+
* A client that connected DURING the outage is left alone. It never had
|
|
133
|
+
* sessions on the dead leg, and the frames it buffered were just flushed onto
|
|
134
|
+
* the replacement connection — closing it with 4002 would make the page retry
|
|
135
|
+
* commands that already ran, and a sessionless `Target.createTarget` among
|
|
136
|
+
* them opens a duplicate tab (issue #2417). The third-failure reset clears
|
|
137
|
+
* the slot, so the comparison below naturally fails for any replacement.
|
|
138
|
+
*/
|
|
139
|
+
private resetStaleSlotHolder;
|
|
140
|
+
private stopped;
|
|
141
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome-leg reconnect supervisor for the node-server `/cdp` proxy.
|
|
3
|
+
*
|
|
4
|
+
* Chrome's browser-level debugger socket drops on its own (`messageTooLarge`,
|
|
5
|
+
* an inbound-queue overflow, a devtools window stealing the single slot). When
|
|
6
|
+
* it does, Chrome discards EVERY CDP session behind it. Until now node-server
|
|
7
|
+
* just cleared `chromeWs` and dropped client frames on the floor until a NEW
|
|
8
|
+
* `/cdp` client connected — i.e. until the SLICC tab was reloaded by hand
|
|
9
|
+
* (issue #2417, `DIAGNOSIS.md` §2.6). swift-server already reconnects
|
|
10
|
+
* (`CDPProxy.scheduleChromeReconnect` / `runChromeReconnectLoop`); this brings
|
|
11
|
+
* node-server to parity and adds the missing half both floats needed: the
|
|
12
|
+
* active client is closed with `CDP_UPSTREAM_RESET_CLOSE_CODE` so the page
|
|
13
|
+
* re-dials and resets its session state instead of issuing commands against
|
|
14
|
+
* sessions Chrome has forgotten.
|
|
15
|
+
*
|
|
16
|
+
* ## Reconnect policy (identical in node-server and swift-server)
|
|
17
|
+
*
|
|
18
|
+
* Retry indefinitely with a 1 s delay between attempts, until shutdown — there
|
|
19
|
+
* is no attempt cap. Close the active client with 4002 `upstream-reset` after
|
|
20
|
+
* the 3rd consecutive failure, so it does not hang on a proxy whose Chrome leg
|
|
21
|
+
* is gone. After a successful reconnect, reset the active client ONLY if it is
|
|
22
|
+
* the same client that held the slot when the Chrome leg went down — it is the
|
|
23
|
+
* one whose sessions Chrome discarded. A client that connected during the
|
|
24
|
+
* outage never had sessions on the dead leg and its buffered frames were just
|
|
25
|
+
* flushed onto the replacement connection, so closing it with 4002 would make
|
|
26
|
+
* the page retry commands that already ran (a sessionless `Target.createTarget`
|
|
27
|
+
* opens a duplicate tab). Never leave a clientless buffer around: buffered
|
|
28
|
+
* frames are dropped whenever the client that wrote them loses the slot.
|
|
29
|
+
*/
|
|
30
|
+
import { createClientFrameBuffer } from './client-frame-buffer.js';
|
|
31
|
+
import { CDP_UPSTREAM_RESET_CLOSE_CODE, CDP_UPSTREAM_RESET_CLOSE_REASON } from './close-codes.js';
|
|
32
|
+
/** Delay before each reconnect attempt. Mirrors swift `defaultReconnectDelayNanoseconds`. */
|
|
33
|
+
export const CHROME_RECONNECT_DELAY_MS = 1000;
|
|
34
|
+
/**
|
|
35
|
+
* Consecutive failed attempts after which the active client is cut loose with
|
|
36
|
+
* 4002 rather than left hanging on a proxy whose Chrome leg is gone. The loop
|
|
37
|
+
* keeps retrying afterwards. Mirrors swift `upstreamResetFailureThreshold`.
|
|
38
|
+
*/
|
|
39
|
+
export const CHROME_RECONNECT_FAILURE_THRESHOLD = 3;
|
|
40
|
+
/**
|
|
41
|
+
* Record that the Chrome leg went away: clear the socket and start buffering
|
|
42
|
+
* client frames so they survive the gap (node-server used to drop them until a
|
|
43
|
+
* NEW `/cdp` client connected). Returns false — meaning "do not schedule a
|
|
44
|
+
* reconnect" — when the event came from a socket a newer connect already
|
|
45
|
+
* replaced, or when the server is shutting down.
|
|
46
|
+
*
|
|
47
|
+
* The buffer opened here is tagged with the connection that just died, so the
|
|
48
|
+
* flush onto the REPLACEMENT connection discards it: those frames name sessions
|
|
49
|
+
* Chrome dropped with the old socket, and the client is being reset anyway.
|
|
50
|
+
*/
|
|
51
|
+
export function markChromeLegDown(state, droppedWs) {
|
|
52
|
+
// swift-server guards the same way with `chromeConnectionID`: a late close
|
|
53
|
+
// from the previous socket must not tear down the live one.
|
|
54
|
+
if (state.chromeWs !== null && state.chromeWs !== droppedWs)
|
|
55
|
+
return false;
|
|
56
|
+
state.chromeWs = null;
|
|
57
|
+
if (state.shuttingDown)
|
|
58
|
+
return false;
|
|
59
|
+
state.messageBuffer ??= createClientFrameBuffer({
|
|
60
|
+
chromeConnectionId: state.chromeConnectionId,
|
|
61
|
+
clientId: state.activeClientId,
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/** `WebSocket.OPEN` — kept local so this module stays free of the `ws` import. */
|
|
66
|
+
const WS_READY_STATE_OPEN = 1;
|
|
67
|
+
/**
|
|
68
|
+
* Close the active `/cdp` client with the upstream-reset code so the page
|
|
69
|
+
* re-dials and rebuilds its (now dead) CDP sessions. Returns true when a close
|
|
70
|
+
* was actually issued.
|
|
71
|
+
*/
|
|
72
|
+
export function closeClientForUpstreamReset(client, reason, log) {
|
|
73
|
+
if (!client || client.readyState !== WS_READY_STATE_OPEN)
|
|
74
|
+
return false;
|
|
75
|
+
log(`[cdp-proxy] Closing client after Chrome-leg reset (${reason})`);
|
|
76
|
+
client.close(CDP_UPSTREAM_RESET_CLOSE_CODE, CDP_UPSTREAM_RESET_CLOSE_REASON);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const defaultSleep = (ms) => new Promise((resolve) => {
|
|
80
|
+
setTimeout(resolve, ms);
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Owns at most one in-flight reconnect loop. `schedule()` is idempotent — a
|
|
84
|
+
* Chrome socket that fires both `error` and `close` schedules once.
|
|
85
|
+
*/
|
|
86
|
+
export class ChromeReconnectController {
|
|
87
|
+
deps;
|
|
88
|
+
task = null;
|
|
89
|
+
cancelled = false;
|
|
90
|
+
/** Slot holder at the most recent Chrome-leg drop — the one with stale sessions. */
|
|
91
|
+
slotHolderAtDrop = null;
|
|
92
|
+
constructor(deps) {
|
|
93
|
+
this.deps = deps;
|
|
94
|
+
}
|
|
95
|
+
/** True while a reconnect loop is running. */
|
|
96
|
+
get reconnecting() {
|
|
97
|
+
return this.task !== null;
|
|
98
|
+
}
|
|
99
|
+
/** Start the reconnect loop unless one is already running or we're shutting down. */
|
|
100
|
+
schedule(reason) {
|
|
101
|
+
if (this.cancelled || this.deps.isShuttingDown()) {
|
|
102
|
+
this.deps.log(`[cdp-proxy] Chrome WS dropped during shutdown — not reconnecting (${reason})`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Sampled on EVERY drop, including one that lands while a loop is already
|
|
106
|
+
// running (a client that connected during the outage rebuilt the leg and
|
|
107
|
+
// then lost it too): the stale-session client is whoever held the slot at
|
|
108
|
+
// the most recent drop.
|
|
109
|
+
this.slotHolderAtDrop = this.deps.activeClientId();
|
|
110
|
+
if (this.task !== null)
|
|
111
|
+
return;
|
|
112
|
+
const delayMs = this.deps.delayMs ?? CHROME_RECONNECT_DELAY_MS;
|
|
113
|
+
this.deps.log(`[cdp-proxy] Scheduling Chrome WS reconnect in ${delayMs}ms (${reason})`);
|
|
114
|
+
const task = this.run(delayMs);
|
|
115
|
+
this.task = task;
|
|
116
|
+
void task.then(() => {
|
|
117
|
+
if (this.task === task)
|
|
118
|
+
this.task = null;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Stop the loop (graceful shutdown). Sticky — the controller is not reused. */
|
|
122
|
+
cancel() {
|
|
123
|
+
this.cancelled = true;
|
|
124
|
+
}
|
|
125
|
+
/** Resolves when the current reconnect loop (if any) has finished. For tests. */
|
|
126
|
+
async settled() {
|
|
127
|
+
await this.task;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Retry until the leg is back or shutdown stops us — no attempt cap, so a
|
|
131
|
+
* Chrome that comes back after a long outage is picked up without needing a
|
|
132
|
+
* fresh client to drive discovery (swift parity).
|
|
133
|
+
*/
|
|
134
|
+
async run(delayMs) {
|
|
135
|
+
const threshold = this.deps.failureThreshold ?? CHROME_RECONNECT_FAILURE_THRESHOLD;
|
|
136
|
+
const sleep = this.deps.sleep ?? defaultSleep;
|
|
137
|
+
let consecutiveFailures = 0;
|
|
138
|
+
let didSignalFailure = false;
|
|
139
|
+
for (;;) {
|
|
140
|
+
await sleep(delayMs);
|
|
141
|
+
if (this.stopped())
|
|
142
|
+
return;
|
|
143
|
+
if (await this.attempt(consecutiveFailures + 1))
|
|
144
|
+
return;
|
|
145
|
+
consecutiveFailures++;
|
|
146
|
+
if (didSignalFailure || consecutiveFailures < threshold)
|
|
147
|
+
continue;
|
|
148
|
+
// Cut the client loose ONCE so it stops waiting on a dead proxy; the loop
|
|
149
|
+
// keeps going and resets whoever holds the slot when Chrome returns.
|
|
150
|
+
didSignalFailure = true;
|
|
151
|
+
this.deps.log(`[cdp-proxy] Chrome WS reconnect failed ${consecutiveFailures}x — resetting client (still retrying)`);
|
|
152
|
+
this.deps.resetClient('reconnect-failed');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** One discover + connect round. Returns true when the leg is back. */
|
|
156
|
+
async attempt(attempt) {
|
|
157
|
+
if (this.deps.isChromeLegHealthy?.() === true) {
|
|
158
|
+
this.deps.log('[cdp-proxy] Chrome WS already re-established — no client reset needed');
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const url = await this.deps.discoverChromeWsUrl();
|
|
163
|
+
if (this.stopped())
|
|
164
|
+
return true;
|
|
165
|
+
await this.deps.connectChrome(url);
|
|
166
|
+
if (this.stopped())
|
|
167
|
+
return true;
|
|
168
|
+
this.deps.log('[cdp-proxy] Chrome WS auto-reconnected');
|
|
169
|
+
this.resetStaleSlotHolder();
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
this.deps.log(`[cdp-proxy] Auto-reconnect attempt ${attempt} failed: ${String(err)}`);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Close the client that held the slot when the leg dropped: Chrome discarded
|
|
179
|
+
* every session with the old socket, so that page's cached sessionIds are
|
|
180
|
+
* dead and it must re-dial with a clean slate.
|
|
181
|
+
*
|
|
182
|
+
* A client that connected DURING the outage is left alone. It never had
|
|
183
|
+
* sessions on the dead leg, and the frames it buffered were just flushed onto
|
|
184
|
+
* the replacement connection — closing it with 4002 would make the page retry
|
|
185
|
+
* commands that already ran, and a sessionless `Target.createTarget` among
|
|
186
|
+
* them opens a duplicate tab (issue #2417). The third-failure reset clears
|
|
187
|
+
* the slot, so the comparison below naturally fails for any replacement.
|
|
188
|
+
*/
|
|
189
|
+
resetStaleSlotHolder() {
|
|
190
|
+
const holder = this.slotHolderAtDrop;
|
|
191
|
+
const current = this.deps.activeClientId();
|
|
192
|
+
if (holder !== null && current === holder) {
|
|
193
|
+
this.deps.resetClient('reconnected');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (current !== null) {
|
|
197
|
+
this.deps.log('[cdp-proxy] Client connected during the outage — no stale sessions, not resetting it');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
stopped() {
|
|
201
|
+
if (!this.cancelled && !this.deps.isShuttingDown())
|
|
202
|
+
return false;
|
|
203
|
+
this.deps.log('[cdp-proxy] Chrome WS reconnect cancelled (shutting down)');
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, generation-tagged Client→Chrome frame buffer for the `/cdp` proxy.
|
|
3
|
+
*
|
|
4
|
+
* Frames a client sends while the Chrome leg is unavailable are held here. The
|
|
5
|
+
* buffer is bounded and drops the OLDEST frame on overflow — same policy and
|
|
6
|
+
* limit as swift-server's `CDPProxy.appendBufferedMessage` (`maxBufferSize =
|
|
7
|
+
* 1_000`). Unbounded growth would turn a Chrome leg that never comes back into
|
|
8
|
+
* a heap leak.
|
|
9
|
+
*
|
|
10
|
+
* The generation tag decides whether the buffer may be flushed at all. A
|
|
11
|
+
* buffered frame is only meaningful on the exact `{chromeConnection, client}`
|
|
12
|
+
* pair it was written for:
|
|
13
|
+
*
|
|
14
|
+
* - Chrome discards EVERY CDP session when its browser-level socket closes,
|
|
15
|
+
* so frames buffered after a leg drop name sessions the replacement
|
|
16
|
+
* connection has never heard of. Worse, a browser-level `Target.createTarget`
|
|
17
|
+
* would still execute — after its caller was already rejected with 4002 —
|
|
18
|
+
* and the caller's retry opens a duplicate tab.
|
|
19
|
+
* - Frames buffered by a client that has since been superseded or closed must
|
|
20
|
+
* not run under whoever holds the slot now.
|
|
21
|
+
*
|
|
22
|
+
* So a buffer is flushed only when both halves of its generation still match at
|
|
23
|
+
* flush time, and dropped (the caller logs the count and the reason) otherwise.
|
|
24
|
+
* The one case that still flushes is the original initial-connect buffering — a
|
|
25
|
+
* client that connected before Chrome was ready lost no sessions, so its
|
|
26
|
+
* `chromeConnectionId` is `null` and any connection will do.
|
|
27
|
+
*
|
|
28
|
+
* The state transitions live here rather than in `index.ts` so the whole policy
|
|
29
|
+
* is unit-testable without booting a server. Mirrors `CDPProxy`'s
|
|
30
|
+
* `clientFrameBufferDropReason` / `discardBufferedMessages` in swift-server.
|
|
31
|
+
*/
|
|
32
|
+
/** Max frames held while the Chrome leg is down. Mirrors swift `maxBufferSize`. */
|
|
33
|
+
export declare const CDP_CLIENT_FRAME_BUFFER_LIMIT = 1000;
|
|
34
|
+
/** The `{chromeConnection, client}` pair a buffer's frames were written for. */
|
|
35
|
+
export interface CdpBufferGeneration {
|
|
36
|
+
/**
|
|
37
|
+
* Id of the Chrome connection that was live when buffering started, or
|
|
38
|
+
* `null` when no leg was live at all (initial connect / a leg that never
|
|
39
|
+
* opened). `null` flushes onto any connection; a concrete id only flushes
|
|
40
|
+
* onto that same connection, which a post-drop replacement never is.
|
|
41
|
+
*/
|
|
42
|
+
chromeConnectionId: number | null;
|
|
43
|
+
/** Id of the client that held the single `/cdp` slot, or `null` for none. */
|
|
44
|
+
clientId: number | null;
|
|
45
|
+
}
|
|
46
|
+
/** Buffered Client→Chrome frames plus the generation they belong to. */
|
|
47
|
+
export interface ClientFrameBuffer {
|
|
48
|
+
readonly generation: CdpBufferGeneration;
|
|
49
|
+
readonly frames: unknown[];
|
|
50
|
+
}
|
|
51
|
+
/** Why a buffer was dropped rather than flushed. Used verbatim in log lines. */
|
|
52
|
+
export type ClientFrameBufferDropReason = 'chrome-leg-reset' | 'client-superseded' | 'client-disconnected' | 'upstream-reset' | 'no-client';
|
|
53
|
+
/** How many frames were thrown away, and why. `null` when nothing was lost. */
|
|
54
|
+
export interface DroppedClientFrames {
|
|
55
|
+
count: number;
|
|
56
|
+
reason: ClientFrameBufferDropReason;
|
|
57
|
+
}
|
|
58
|
+
export interface ClientFrameBufferHost {
|
|
59
|
+
/**
|
|
60
|
+
* The Chrome leg, or null while it is down. A socket that is still
|
|
61
|
+
* CONNECTING (a reconnect attempt in flight) counts as "no live leg": a
|
|
62
|
+
* client joining then has lost nothing and its frames must stay flushable.
|
|
63
|
+
*/
|
|
64
|
+
chromeWs: {
|
|
65
|
+
readyState: number;
|
|
66
|
+
} | null;
|
|
67
|
+
/** Monotonic id of `chromeWs`, assigned when the socket is created. */
|
|
68
|
+
chromeConnectionId: number;
|
|
69
|
+
/** Monotonic id of the client holding the single slot; null when empty. */
|
|
70
|
+
activeClientId: number | null;
|
|
71
|
+
messageBuffer: ClientFrameBuffer | null;
|
|
72
|
+
}
|
|
73
|
+
export declare function createClientFrameBuffer(generation: CdpBufferGeneration): ClientFrameBuffer;
|
|
74
|
+
/**
|
|
75
|
+
* Append `frame` to `frames`, dropping the oldest entry when the limit is
|
|
76
|
+
* reached. Returns true when a frame was dropped, so the caller can log it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function appendBufferedClientFrame(frames: unknown[], frame: unknown, limit?: number): boolean;
|
|
79
|
+
/** The generation a buffer opened right now belongs to. */
|
|
80
|
+
export declare function currentBufferGeneration(state: ClientFrameBufferHost): CdpBufferGeneration;
|
|
81
|
+
/**
|
|
82
|
+
* Decide whether `buffer` may be flushed onto the connection identified by
|
|
83
|
+
* `current`. Returns `null` when the flush is safe, otherwise the drop reason.
|
|
84
|
+
*/
|
|
85
|
+
export declare function clientFrameBufferDropReason(buffer: ClientFrameBuffer, current: CdpBufferGeneration): ClientFrameBufferDropReason | null;
|
|
86
|
+
/** Frames to send now, plus what was thrown away instead of being sent. */
|
|
87
|
+
export interface ClientFrameFlush {
|
|
88
|
+
frames: unknown[];
|
|
89
|
+
dropped: DroppedClientFrames | null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Clear the buffer and report what may be delivered onto `targetConnectionId`.
|
|
93
|
+
* Always leaves `state.messageBuffer` null: either the frames go out now, or
|
|
94
|
+
* they are gone.
|
|
95
|
+
*/
|
|
96
|
+
export declare function takeClientFrameBuffer(state: ClientFrameBufferHost, targetConnectionId: number): ClientFrameFlush;
|
|
97
|
+
/**
|
|
98
|
+
* Whether `clientId` still holds the single `/cdp` slot. A frame from a
|
|
99
|
+
* client that was superseded, closed, or reset with 4002 must not reach
|
|
100
|
+
* Chrome even if it arrives before that client's socket has finished
|
|
101
|
+
* closing — swift-server's `receive` guard, mirrored here.
|
|
102
|
+
*/
|
|
103
|
+
export declare function clientHoldsSlot(state: ClientFrameBufferHost, clientId: number): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Give the single `/cdp` slot to `clientId`. Anything the previous holder
|
|
106
|
+
* buffered belongs to IT — running those frames under the new client is the
|
|
107
|
+
* duplicate-tab bug from issue #2417 — so it is dropped, and a fresh buffer is
|
|
108
|
+
* opened so frames arriving during the Chrome handshake are still captured.
|
|
109
|
+
*/
|
|
110
|
+
export declare function adoptClientSlot(state: ClientFrameBufferHost, clientId: number): DroppedClientFrames | null;
|
|
111
|
+
/**
|
|
112
|
+
* Empty the slot. Whatever the departing client buffered dies with it — a
|
|
113
|
+
* clientless buffer must never reach a Chrome leg, and after a 4002 reset the
|
|
114
|
+
* page re-issues whatever it still needs.
|
|
115
|
+
*/
|
|
116
|
+
export declare function releaseClientSlot(state: ClientFrameBufferHost, reason: Extract<ClientFrameBufferDropReason, 'client-disconnected' | 'upstream-reset'>): DroppedClientFrames | null;
|
|
117
|
+
/** The `[cdp-proxy]` line emitted whenever buffered frames are thrown away. */
|
|
118
|
+
export declare function describeDroppedClientFrames(dropped: DroppedClientFrames): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, generation-tagged Client→Chrome frame buffer for the `/cdp` proxy.
|
|
3
|
+
*
|
|
4
|
+
* Frames a client sends while the Chrome leg is unavailable are held here. The
|
|
5
|
+
* buffer is bounded and drops the OLDEST frame on overflow — same policy and
|
|
6
|
+
* limit as swift-server's `CDPProxy.appendBufferedMessage` (`maxBufferSize =
|
|
7
|
+
* 1_000`). Unbounded growth would turn a Chrome leg that never comes back into
|
|
8
|
+
* a heap leak.
|
|
9
|
+
*
|
|
10
|
+
* The generation tag decides whether the buffer may be flushed at all. A
|
|
11
|
+
* buffered frame is only meaningful on the exact `{chromeConnection, client}`
|
|
12
|
+
* pair it was written for:
|
|
13
|
+
*
|
|
14
|
+
* - Chrome discards EVERY CDP session when its browser-level socket closes,
|
|
15
|
+
* so frames buffered after a leg drop name sessions the replacement
|
|
16
|
+
* connection has never heard of. Worse, a browser-level `Target.createTarget`
|
|
17
|
+
* would still execute — after its caller was already rejected with 4002 —
|
|
18
|
+
* and the caller's retry opens a duplicate tab.
|
|
19
|
+
* - Frames buffered by a client that has since been superseded or closed must
|
|
20
|
+
* not run under whoever holds the slot now.
|
|
21
|
+
*
|
|
22
|
+
* So a buffer is flushed only when both halves of its generation still match at
|
|
23
|
+
* flush time, and dropped (the caller logs the count and the reason) otherwise.
|
|
24
|
+
* The one case that still flushes is the original initial-connect buffering — a
|
|
25
|
+
* client that connected before Chrome was ready lost no sessions, so its
|
|
26
|
+
* `chromeConnectionId` is `null` and any connection will do.
|
|
27
|
+
*
|
|
28
|
+
* The state transitions live here rather than in `index.ts` so the whole policy
|
|
29
|
+
* is unit-testable without booting a server. Mirrors `CDPProxy`'s
|
|
30
|
+
* `clientFrameBufferDropReason` / `discardBufferedMessages` in swift-server.
|
|
31
|
+
*/
|
|
32
|
+
/** Max frames held while the Chrome leg is down. Mirrors swift `maxBufferSize`. */
|
|
33
|
+
export const CDP_CLIENT_FRAME_BUFFER_LIMIT = 1000;
|
|
34
|
+
/** The slice of `ServerState` the buffer lifecycle reads and mutates. */
|
|
35
|
+
/** ws `WebSocket.OPEN`; the only readyState under which frames can actually be delivered. */
|
|
36
|
+
const SOCKET_OPEN = 1;
|
|
37
|
+
export function createClientFrameBuffer(generation) {
|
|
38
|
+
return { generation, frames: [] };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Append `frame` to `frames`, dropping the oldest entry when the limit is
|
|
42
|
+
* reached. Returns true when a frame was dropped, so the caller can log it.
|
|
43
|
+
*/
|
|
44
|
+
export function appendBufferedClientFrame(frames, frame, limit = CDP_CLIENT_FRAME_BUFFER_LIMIT) {
|
|
45
|
+
let dropped = false;
|
|
46
|
+
while (frames.length >= limit) {
|
|
47
|
+
frames.shift();
|
|
48
|
+
dropped = true;
|
|
49
|
+
}
|
|
50
|
+
frames.push(frame);
|
|
51
|
+
return dropped;
|
|
52
|
+
}
|
|
53
|
+
/** The generation a buffer opened right now belongs to. */
|
|
54
|
+
export function currentBufferGeneration(state) {
|
|
55
|
+
return {
|
|
56
|
+
// No live leg = nothing was lost, so the frames may flush onto whatever
|
|
57
|
+
// connection comes up next (the original initial-connect buffering).
|
|
58
|
+
chromeConnectionId: state.chromeWs?.readyState === SOCKET_OPEN ? state.chromeConnectionId : null,
|
|
59
|
+
clientId: state.activeClientId,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Decide whether `buffer` may be flushed onto the connection identified by
|
|
64
|
+
* `current`. Returns `null` when the flush is safe, otherwise the drop reason.
|
|
65
|
+
*/
|
|
66
|
+
export function clientFrameBufferDropReason(buffer, current) {
|
|
67
|
+
const { chromeConnectionId, clientId } = buffer.generation;
|
|
68
|
+
if (chromeConnectionId !== null && chromeConnectionId !== current.chromeConnectionId) {
|
|
69
|
+
return 'chrome-leg-reset';
|
|
70
|
+
}
|
|
71
|
+
if (current.clientId === null)
|
|
72
|
+
return 'no-client';
|
|
73
|
+
if (clientId !== current.clientId)
|
|
74
|
+
return 'client-superseded';
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Clear the buffer and report what may be delivered onto `targetConnectionId`.
|
|
79
|
+
* Always leaves `state.messageBuffer` null: either the frames go out now, or
|
|
80
|
+
* they are gone.
|
|
81
|
+
*/
|
|
82
|
+
export function takeClientFrameBuffer(state, targetConnectionId) {
|
|
83
|
+
const buffer = state.messageBuffer;
|
|
84
|
+
state.messageBuffer = null;
|
|
85
|
+
if (!buffer)
|
|
86
|
+
return { frames: [], dropped: null };
|
|
87
|
+
const reason = clientFrameBufferDropReason(buffer, {
|
|
88
|
+
chromeConnectionId: targetConnectionId,
|
|
89
|
+
clientId: state.activeClientId,
|
|
90
|
+
});
|
|
91
|
+
if (!reason)
|
|
92
|
+
return { frames: buffer.frames, dropped: null };
|
|
93
|
+
return {
|
|
94
|
+
frames: [],
|
|
95
|
+
dropped: buffer.frames.length > 0 ? { count: buffer.frames.length, reason } : null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether `clientId` still holds the single `/cdp` slot. A frame from a
|
|
100
|
+
* client that was superseded, closed, or reset with 4002 must not reach
|
|
101
|
+
* Chrome even if it arrives before that client's socket has finished
|
|
102
|
+
* closing — swift-server's `receive` guard, mirrored here.
|
|
103
|
+
*/
|
|
104
|
+
export function clientHoldsSlot(state, clientId) {
|
|
105
|
+
return state.activeClientId === clientId;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Give the single `/cdp` slot to `clientId`. Anything the previous holder
|
|
109
|
+
* buffered belongs to IT — running those frames under the new client is the
|
|
110
|
+
* duplicate-tab bug from issue #2417 — so it is dropped, and a fresh buffer is
|
|
111
|
+
* opened so frames arriving during the Chrome handshake are still captured.
|
|
112
|
+
*/
|
|
113
|
+
export function adoptClientSlot(state, clientId) {
|
|
114
|
+
state.activeClientId = clientId;
|
|
115
|
+
let dropped = null;
|
|
116
|
+
if (state.messageBuffer && state.messageBuffer.generation.clientId !== clientId) {
|
|
117
|
+
dropped = releaseClientFrameBuffer(state, 'client-superseded');
|
|
118
|
+
}
|
|
119
|
+
state.messageBuffer ??= createClientFrameBuffer(currentBufferGeneration(state));
|
|
120
|
+
return dropped;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Empty the slot. Whatever the departing client buffered dies with it — a
|
|
124
|
+
* clientless buffer must never reach a Chrome leg, and after a 4002 reset the
|
|
125
|
+
* page re-issues whatever it still needs.
|
|
126
|
+
*/
|
|
127
|
+
export function releaseClientSlot(state, reason) {
|
|
128
|
+
state.activeClientId = null;
|
|
129
|
+
return releaseClientFrameBuffer(state, reason);
|
|
130
|
+
}
|
|
131
|
+
function releaseClientFrameBuffer(state, reason) {
|
|
132
|
+
const buffer = state.messageBuffer;
|
|
133
|
+
state.messageBuffer = null;
|
|
134
|
+
if (!buffer || buffer.frames.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
return { count: buffer.frames.length, reason };
|
|
137
|
+
}
|
|
138
|
+
/** The `[cdp-proxy]` line emitted whenever buffered frames are thrown away. */
|
|
139
|
+
export function describeDroppedClientFrames(dropped) {
|
|
140
|
+
return `[cdp-proxy] Dropped ${dropped.count} buffered client frame(s) — ${dropped.reason}`;
|
|
141
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-range (4000–4999) WebSocket close codes the `/cdp` proxy sends
|
|
3
|
+
* to its single client. Both MUST stay in sync with the same-named constants
|
|
4
|
+
* in `packages/webapp/src/cdp/cdp-client.ts` (and their Swift twins in
|
|
5
|
+
* `packages/swift-server/Sources/WebSocket/CDPProxy.swift`), because the
|
|
6
|
+
* page-side `CDPClient` branches on the exact numeric code.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Sent to a CDP client that is evicted because a newer client took the single
|
|
10
|
+
* proxy slot. The page-side `CDPClient` recognises it and stops
|
|
11
|
+
* auto-reconnecting (otherwise two webapp tabs on one instance evict each
|
|
12
|
+
* other forever). MUST stay in sync with `CDP_SUPERSEDED_CLOSE_CODE` in
|
|
13
|
+
* `packages/webapp/src/cdp/cdp-client.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CDP_SUPERSEDED_CLOSE_CODE = 4001;
|
|
16
|
+
/**
|
|
17
|
+
* Sent to the active CDP client after the proxy's Chrome-leg WebSocket dropped
|
|
18
|
+
* and was re-established (or definitively failed to come back). Chrome
|
|
19
|
+
* discards every CDP session when that socket closes, so the page's cached
|
|
20
|
+
* `sessionId`s are dead — but nothing else tells it. The page-side `CDPClient`
|
|
21
|
+
* treats this code as "reconnect and reset session state" (unlike
|
|
22
|
+
* {@link CDP_SUPERSEDED_CLOSE_CODE}, it does NOT latch superseded). MUST stay
|
|
23
|
+
* in sync with `CDP_UPSTREAM_RESET_CLOSE_CODE` in
|
|
24
|
+
* `packages/webapp/src/cdp/cdp-client.ts`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CDP_UPSTREAM_RESET_CLOSE_CODE = 4002;
|
|
27
|
+
/** Close reason paired with {@link CDP_UPSTREAM_RESET_CLOSE_CODE}. */
|
|
28
|
+
export declare const CDP_UPSTREAM_RESET_CLOSE_REASON = "upstream-reset";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-range (4000–4999) WebSocket close codes the `/cdp` proxy sends
|
|
3
|
+
* to its single client. Both MUST stay in sync with the same-named constants
|
|
4
|
+
* in `packages/webapp/src/cdp/cdp-client.ts` (and their Swift twins in
|
|
5
|
+
* `packages/swift-server/Sources/WebSocket/CDPProxy.swift`), because the
|
|
6
|
+
* page-side `CDPClient` branches on the exact numeric code.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Sent to a CDP client that is evicted because a newer client took the single
|
|
10
|
+
* proxy slot. The page-side `CDPClient` recognises it and stops
|
|
11
|
+
* auto-reconnecting (otherwise two webapp tabs on one instance evict each
|
|
12
|
+
* other forever). MUST stay in sync with `CDP_SUPERSEDED_CLOSE_CODE` in
|
|
13
|
+
* `packages/webapp/src/cdp/cdp-client.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export const CDP_SUPERSEDED_CLOSE_CODE = 4001;
|
|
16
|
+
/**
|
|
17
|
+
* Sent to the active CDP client after the proxy's Chrome-leg WebSocket dropped
|
|
18
|
+
* and was re-established (or definitively failed to come back). Chrome
|
|
19
|
+
* discards every CDP session when that socket closes, so the page's cached
|
|
20
|
+
* `sessionId`s are dead — but nothing else tells it. The page-side `CDPClient`
|
|
21
|
+
* treats this code as "reconnect and reset session state" (unlike
|
|
22
|
+
* {@link CDP_SUPERSEDED_CLOSE_CODE}, it does NOT latch superseded). MUST stay
|
|
23
|
+
* in sync with `CDP_UPSTREAM_RESET_CLOSE_CODE` in
|
|
24
|
+
* `packages/webapp/src/cdp/cdp-client.ts`.
|
|
25
|
+
*/
|
|
26
|
+
export const CDP_UPSTREAM_RESET_CLOSE_CODE = 4002;
|
|
27
|
+
/** Close reason paired with {@link CDP_UPSTREAM_RESET_CLOSE_CODE}. */
|
|
28
|
+
export const CDP_UPSTREAM_RESET_CLOSE_REASON = 'upstream-reset';
|