shadow-claw 1.27.0 → 1.28.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/.well-known/ai-catalog.json +11 -0
- package/.well-known/mcp/index.html +19 -0
- package/.well-known/mcp/server-card.json +52 -0
- package/.well-known/mcp/servers.json +15 -0
- package/.well-known/mcp.json +52 -0
- package/.well-known/oauth-protected-resource +10 -0
- package/.well-known/oauth-protected-resource.json +10 -0
- package/bin/assert-version-bump.mjs +61 -3
- package/bin/assert-version-bump.test.mjs +77 -1
- package/bin/build/build.mjs +116 -49
- package/bin/build/build.test.mjs +103 -0
- package/bin/cli.mjs +299 -237
- package/bin/cli.test.mjs +27 -0
- package/bin/commands/mcp.mjs +132 -2
- package/bin/commands/mcp.test.mjs +166 -0
- package/bin/commands/skills-index.mjs +139 -20
- package/bin/commands/skills-index.test.mjs +82 -0
- package/bin/utils/control-client.mjs +17 -0
- package/bin/utils/control-client.test.mjs +46 -0
- package/bin/version.mjs +208 -0
- package/bin/version.test.mjs +138 -0
- package/dist/electron/main.cjs +195 -185
- package/dist/lib/components/index.js +21 -21
- package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/settings/shadow-claw-git.js +1 -1
- package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
- package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
- package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
- package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
- package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw/shadow-claw.d.ts +1 -0
- package/dist/lib/components/shadow-claw/utils/setupAppLifecycle.d.ts +23 -0
- package/dist/lib/components/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/shadow-claw-channels.js +1 -1
- package/dist/lib/components/shadow-claw-chat.js +1 -1
- package/dist/lib/components/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/shadow-claw-conversations.js +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw-files.js +1 -1
- package/dist/lib/components/shadow-claw-git.js +1 -1
- package/dist/lib/components/shadow-claw-llm.js +1 -1
- package/dist/lib/components/shadow-claw-networking.js +1 -1
- package/dist/lib/components/shadow-claw-notifications.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/components/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/shadow-claw-settings.js +1 -1
- package/dist/lib/components/shadow-claw-storage.js +1 -1
- package/dist/lib/components/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/shadow-claw-tasks.js +1 -1
- package/dist/lib/components/shadow-claw-terminal.js +1 -1
- package/dist/lib/components/shadow-claw-tools.js +1 -1
- package/dist/lib/components/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/{configurePeerJs-oODzltaC.js → configurePeerJs-D8Ph0-A9.js} +2 -2
- package/dist/lib/{configurePeerJs-oODzltaC.js.map → configurePeerJs-D8Ph0-A9.js.map} +1 -1
- package/dist/lib/core/control-plane-client.d.ts +2 -0
- package/dist/lib/core/orchestrator/orchestrator.d.ts +10 -0
- package/dist/lib/core/orchestrator/utils/operations/channel.d.ts +2 -2
- package/dist/lib/core/utils/initControlPlane.d.ts +6 -0
- package/dist/lib/index.js +21 -21
- package/dist/lib/{initControlPlane-C2zURVIS.js → initControlPlane-BIrWmpzc.js} +76 -9
- package/dist/lib/initControlPlane-BIrWmpzc.js.map +1 -0
- package/dist/lib/{markdown-C6EHl9zs.js → markdown-DEUoM1c6.js} +2 -2
- package/dist/lib/{markdown-C6EHl9zs.js.map → markdown-DEUoM1c6.js.map} +1 -1
- package/dist/lib/{orchestrator-KRjx0eba.js → orchestrator-BtVj9bBB.js} +11 -10
- package/dist/lib/{orchestrator-KRjx0eba.js.map → orchestrator-BtVj9bBB.js.map} +1 -1
- package/dist/lib/{orchestrator-CACLa_Ro.js → orchestrator-CFCW-l1O.js} +52 -4
- package/dist/lib/orchestrator-CFCW-l1O.js.map +1 -0
- package/dist/lib/{peerjs-DFWOBcXf.js → peerjs-BoBkx9DJ.js} +23 -3
- package/dist/lib/peerjs-BoBkx9DJ.js.map +1 -0
- package/dist/lib/{push-client-_tiWQsCT.js → push-client-D6hbzDNV.js} +10 -4
- package/dist/lib/push-client-D6hbzDNV.js.map +1 -0
- package/dist/lib/{shadow-claw-6UHdiy4a.js → shadow-claw-Cuf2O5bZ.js} +71 -4
- package/dist/lib/shadow-claw-Cuf2O5bZ.js.map +1 -0
- package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js → shadow-claw-channel-config-BDPKt6Bd.js} +3 -3
- package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js.map → shadow-claw-channel-config-BDPKt6Bd.js.map} +1 -1
- package/dist/lib/{shadow-claw-channels-1N6Jl-By.js → shadow-claw-channels-BsP5JABM.js} +2 -2
- package/dist/lib/{shadow-claw-channels-1N6Jl-By.js.map → shadow-claw-channels-BsP5JABM.js.map} +1 -1
- package/dist/lib/{shadow-claw-chat-BMoqQJRK.js → shadow-claw-chat-Cam2lX4k.js} +4 -4
- package/dist/lib/{shadow-claw-chat-BMoqQJRK.js.map → shadow-claw-chat-Cam2lX4k.js.map} +1 -1
- package/dist/lib/{shadow-claw-control-plane-UG0o7pCz.js → shadow-claw-control-plane-DB2qcCFs.js} +20 -10
- package/dist/lib/shadow-claw-control-plane-DB2qcCFs.js.map +1 -0
- package/dist/lib/{shadow-claw-conversations-BwXep3iu.js → shadow-claw-conversations-C9Vf4XGx.js} +2 -2
- package/dist/lib/{shadow-claw-conversations-BwXep3iu.js.map → shadow-claw-conversations-C9Vf4XGx.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js → shadow-claw-file-viewer-B4HYiI0v.js} +3 -3
- package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js.map → shadow-claw-file-viewer-B4HYiI0v.js.map} +1 -1
- package/dist/lib/{shadow-claw-files-DIREEnT2.js → shadow-claw-files-DVROUESO.js} +2 -2
- package/dist/lib/{shadow-claw-files-DIREEnT2.js.map → shadow-claw-files-DVROUESO.js.map} +1 -1
- package/dist/lib/{shadow-claw-git-Bg1SWgsF.js → shadow-claw-git-BqrO7cHJ.js} +3 -3
- package/dist/lib/{shadow-claw-git-Bg1SWgsF.js.map → shadow-claw-git-BqrO7cHJ.js.map} +1 -1
- package/dist/lib/{shadow-claw-llm-CazBrVpM.js → shadow-claw-llm-BexUQdYo.js} +2 -2
- package/dist/lib/{shadow-claw-llm-CazBrVpM.js.map → shadow-claw-llm-BexUQdYo.js.map} +1 -1
- package/dist/lib/{shadow-claw-networking-Df85tCbO.js → shadow-claw-networking-CfXRGpUD.js} +2 -2
- package/dist/lib/{shadow-claw-networking-Df85tCbO.js.map → shadow-claw-networking-CfXRGpUD.js.map} +1 -1
- package/dist/lib/{shadow-claw-notifications-v62FryTc.js → shadow-claw-notifications-C9KPMgx_.js} +2 -2
- package/dist/lib/{shadow-claw-notifications-v62FryTc.js.map → shadow-claw-notifications-C9KPMgx_.js.map} +1 -1
- package/dist/lib/{shadow-claw-pages-BgPyIEZO.js → shadow-claw-pages-CGzGXmrI.js} +3 -3
- package/dist/lib/{shadow-claw-pages-BgPyIEZO.js.map → shadow-claw-pages-CGzGXmrI.js.map} +1 -1
- package/dist/lib/{shadow-claw-peerjs-xkBXILed.js → shadow-claw-peerjs-B0ZB7yhB.js} +3 -3
- package/dist/lib/{shadow-claw-peerjs-xkBXILed.js.map → shadow-claw-peerjs-B0ZB7yhB.js.map} +1 -1
- package/dist/lib/{shadow-claw-settings-CN89xRVU.js → shadow-claw-settings-DBfMZdDS.js} +2 -2
- package/dist/lib/{shadow-claw-settings-CN89xRVU.js.map → shadow-claw-settings-DBfMZdDS.js.map} +1 -1
- package/dist/lib/{shadow-claw-storage-VnhcObCe.js → shadow-claw-storage-CM-w6mgG.js} +2 -2
- package/dist/lib/{shadow-claw-storage-VnhcObCe.js.map → shadow-claw-storage-CM-w6mgG.js.map} +1 -1
- package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js → shadow-claw-task-server-DV4b1g2-.js} +4 -2
- package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js.map → shadow-claw-task-server-DV4b1g2-.js.map} +1 -1
- package/dist/lib/{shadow-claw-tasks-i_evIufy.js → shadow-claw-tasks-K-foOHSE.js} +3 -3
- package/dist/lib/{shadow-claw-tasks-i_evIufy.js.map → shadow-claw-tasks-K-foOHSE.js.map} +1 -1
- package/dist/lib/{shadow-claw-terminal-CwScROlu.js → shadow-claw-terminal-CpAknKyA.js} +2 -2
- package/dist/lib/{shadow-claw-terminal-CwScROlu.js.map → shadow-claw-terminal-CpAknKyA.js.map} +1 -1
- package/dist/lib/{shadow-claw-tools-DD3fyXP4.js → shadow-claw-tools-D98XZ0Ep.js} +3 -3
- package/dist/lib/{shadow-claw-tools-DD3fyXP4.js.map → shadow-claw-tools-D98XZ0Ep.js.map} +1 -1
- package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js → shadow-claw-webvm-DLOb8dMI.js} +2 -2
- package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js.map → shadow-claw-webvm-DLOb8dMI.js.map} +1 -1
- package/dist/lib/subsystems/channels/imessage.d.ts +1 -0
- package/dist/lib/subsystems/channels/peerjs.d.ts +1 -0
- package/dist/lib/subsystems/channels/telegram.d.ts +1 -0
- package/dist/lib/subsystems/channels/types.d.ts +2 -0
- package/dist/lib/subsystems/notifications/push-client.d.ts +1 -1
- package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js → syncWebMcpRegistration-NspKefOI.js} +2 -2
- package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js.map → syncWebMcpRegistration-NspKefOI.js.map} +1 -1
- package/dist/public/.well-known/ai-catalog.json +11 -0
- package/dist/public/.well-known/mcp/index.html +19 -0
- package/dist/public/.well-known/mcp/server-card.json +52 -0
- package/dist/public/.well-known/mcp/servers.json +15 -0
- package/dist/public/.well-known/mcp.json +52 -0
- package/dist/public/.well-known/oauth-protected-resource +10 -0
- package/dist/public/.well-known/oauth-protected-resource.json +10 -0
- package/dist/public/404.html +28 -1
- package/dist/public/AGENTS.md +1 -1
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/{configurePeerJs-BqgeBjP7.js → configurePeerJs-BhBokQlC.js} +1 -1
- package/dist/public/docs/example/article/index.html +3 -3
- package/dist/public/docs/publishing/index.html +3 -3
- package/dist/public/docs/skill-creator/index.html +3 -3
- package/dist/public/docs/subsystems/cli.md +11 -8
- package/dist/public/docs/subsystems/mcp-server.md +1 -0
- package/dist/public/docs/subsystems/notifications.md +10 -0
- package/dist/public/{e2e-bridge-Ceqr5kGc.js → e2e-bridge-DcBoeFvd.js} +1 -1
- package/dist/public/{file-viewer-C-6Jc_tb.js → file-viewer-CKtVC9Ix.js} +1 -1
- package/dist/public/index.css +1 -1
- package/dist/public/index.html +3 -3
- package/dist/public/index.js +1 -1
- package/dist/public/initControlPlane-Bi8ouFEb.js +1 -0
- package/dist/public/llms.txt +36 -1
- package/dist/public/main/index.html +3 -3
- package/dist/public/main/memory/index.html +3 -3
- package/dist/public/{markdown-C3M4OW_A.js → markdown-C-hCVJtc.js} +1 -1
- package/dist/public/orchestrator-8qNGfw8r.js +15 -0
- package/dist/public/{orchestrator-DHjdLtqa.js → orchestrator-BT3Ks5ip.js} +2 -2
- package/dist/public/peerjs-DfhwEiiq.js +1 -0
- package/dist/public/push-client-BVLrE9pK.js +1 -0
- package/dist/public/service-worker/fetch-proxy.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-EAt2ml5Y.js → shadow-claw-DCjxNDCX.js} +3 -3
- package/dist/public/{shadow-claw-channels-XsfuWFF8.js → shadow-claw-channels-CITDrqMl.js} +1 -1
- package/dist/public/{shadow-claw-chat-CbSaOZ4q.js → shadow-claw-chat-BQrSgSKF.js} +1 -1
- package/dist/public/{shadow-claw-control-plane-DHvN3Ukg.js → shadow-claw-control-plane-Cp001OY0.js} +2 -2
- package/dist/public/{shadow-claw-conversations-BWs5VVfs.js → shadow-claw-conversations-EhX4RvIj.js} +1 -1
- package/dist/public/{shadow-claw-file-viewer-BWsoW5QS.js → shadow-claw-file-viewer-DiQXIjZD.js} +1 -1
- package/dist/public/{shadow-claw-files-CG7MqWiV.js → shadow-claw-files-lkTnSJsn.js} +1 -1
- package/dist/public/{shadow-claw-git-DBRPZNav.js → shadow-claw-git-BlcA_z8x.js} +2 -2
- package/dist/public/{shadow-claw-llm-DMvLgKXo.js → shadow-claw-llm-CW16X9Rg.js} +1 -1
- package/dist/public/{shadow-claw-networking-Dd8Rw31D.js → shadow-claw-networking-ySsMBY_i.js} +1 -1
- package/dist/public/{shadow-claw-notifications-Dbevj-lq.js → shadow-claw-notifications-CyV2zCQ8.js} +1 -1
- package/dist/public/{shadow-claw-pages-3_lQAv0t.js → shadow-claw-pages-Cx7cF9ah.js} +1 -1
- package/dist/public/{shadow-claw-settings-t8TIlgrM.js → shadow-claw-settings-zm4PHHGi.js} +2 -2
- package/dist/public/{shadow-claw-storage-CfEXoSIE.js → shadow-claw-storage-dy9JAEGd.js} +1 -1
- package/dist/public/{shadow-claw-task-server-DxQSsgRZ.js → shadow-claw-task-server-Bleww48s.js} +2 -2
- package/dist/public/{shadow-claw-tasks-DNpicQwz.js → shadow-claw-tasks-CTI5oaLN.js} +1 -1
- package/dist/public/{shadow-claw-terminal-DpY6zU_r.js → shadow-claw-terminal-mZ3i4HaQ.js} +1 -1
- package/dist/public/{shadow-claw-tools-WSwWohnk.js → shadow-claw-tools-B_nuk8Zl.js} +1 -1
- package/dist/public/{shadow-claw-webvm-CP6oFjg5.js → shadow-claw-webvm-Vl_jH5YL.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-DOn9gLnD.js → syncWebMcpRegistration-n2vAMJ4w.js} +1 -1
- package/dist/server.js +197 -187
- package/package.json +2 -1
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +6 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +19 -9
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +14 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +12 -0
- package/src/components/shadow-claw/shadow-claw.ts +6 -0
- package/src/components/shadow-claw/utils/bindEventListeners.test.ts +57 -0
- package/src/components/shadow-claw/utils/bindEventListeners.ts +14 -0
- package/src/components/shadow-claw/utils/setupAppLifecycle.test.ts +201 -0
- package/src/components/shadow-claw/utils/setupAppLifecycle.ts +101 -0
- package/src/core/control-plane-client.test.ts +82 -1
- package/src/core/control-plane-client.ts +35 -1
- package/src/core/orchestrator/orchestrator.test.ts +49 -0
- package/src/core/orchestrator/orchestrator.ts +43 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +17 -0
- package/src/core/orchestrator/utils/operations/channel.ts +11 -5
- package/src/core/orchestrator/utils/operations/task.test.ts +26 -0
- package/src/core/orchestrator/utils/operations/task.ts +8 -1
- package/src/core/utils/initControlPlane.test.ts +138 -0
- package/src/core/utils/initControlPlane.ts +82 -0
- package/src/core/utils/initializeApp.test.ts +27 -1
- package/src/core/utils/initializeApp.ts +6 -13
- package/src/server/mcp/mcp-server.test.ts +6 -0
- package/src/server/mcp/mcp-server.ts +2 -1
- package/src/server/mcp/tools/built-in-tools.ts +88 -1
- package/src/server/mcp/tools/client-tool-relay.ts +6 -0
- package/src/server/mcp/tools/mcp-tools.test.ts +128 -0
- package/src/server/openapi/openapi-coverage.test.ts +2 -1
- package/src/server/openapi/openapi-spec.ts +2 -1
- package/src/server/openapi/paths/mcp.ts +104 -0
- package/src/server/openapi/paths/push.ts +3 -0
- package/src/server/routes/mcp.test.ts +92 -0
- package/src/server/routes/mcp.ts +138 -2
- package/src/server/utils/packageVersion.test.ts +10 -0
- package/src/server/utils/packageVersion.ts +32 -0
- package/src/service-worker/fetch-proxy-rules.test.ts +73 -0
- package/src/service-worker/fetch-proxy-rules.ts +44 -0
- package/src/service-worker/fetch-proxy.ts +1 -1
- package/src/subsystems/channels/imessage.test.ts +36 -0
- package/src/subsystems/channels/imessage.ts +11 -0
- package/src/subsystems/channels/peerjs.test.ts +56 -0
- package/src/subsystems/channels/peerjs.ts +29 -0
- package/src/subsystems/channels/telegram.test.ts +38 -0
- package/src/subsystems/channels/telegram.ts +13 -0
- package/src/subsystems/channels/types.ts +2 -0
- package/src/subsystems/notifications/push-client.test.ts +70 -0
- package/src/subsystems/notifications/push-client.ts +28 -3
- package/src/subsystems/notifications/push-routes.test.ts +82 -0
- package/src/subsystems/notifications/push-routes.ts +70 -16
- package/src/subsystems/notifications/push-store.test.ts +59 -0
- package/src/subsystems/notifications/push-store.ts +113 -19
- package/src/testing/jest-setup.ts +15 -0
- package/dist/lib/initControlPlane-C2zURVIS.js.map +0 -1
- package/dist/lib/orchestrator-CACLa_Ro.js.map +0 -1
- package/dist/lib/peerjs-DFWOBcXf.js.map +0 -1
- package/dist/lib/push-client-_tiWQsCT.js.map +0 -1
- package/dist/lib/shadow-claw-6UHdiy4a.js.map +0 -1
- package/dist/lib/shadow-claw-control-plane-UG0o7pCz.js.map +0 -1
- package/dist/public/initControlPlane-BN9cWyO0.js +0 -1
- package/dist/public/orchestrator-CMbCTjBf.js +0 -15
- package/dist/public/peerjs-DUCKH-m9.js +0 -1
- package/dist/public/push-client-D0lkwrK0.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shadow-claw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"description": "Browser-native personal AI assistant.",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"homepage": "https://github.com/xt-ml/shadow-claw#readme",
|
|
57
57
|
"scripts": {
|
|
58
|
+
"version": "node bin/version.mjs",
|
|
58
59
|
"prepack": "npm run build:prod",
|
|
59
60
|
"assert:version": "node bin/assert-version-bump.mjs",
|
|
60
61
|
"build:clean": "node bin/rimraf.mjs dist && node bin/rimraf.mjs dist-electron",
|
|
@@ -60,6 +60,12 @@ describe("shadow-claw-control-plane", () => {
|
|
|
60
60
|
expect(getControlPlaneTargetAddressSpace("https://172.20.0.1:8888")).toBe(
|
|
61
61
|
"private",
|
|
62
62
|
);
|
|
63
|
+
expect(getControlPlaneTargetAddressSpace("https://hostname:8888")).toBe(
|
|
64
|
+
"private",
|
|
65
|
+
);
|
|
66
|
+
expect(
|
|
67
|
+
getControlPlaneTargetAddressSpace("https://hostname.local:8888"),
|
|
68
|
+
).toBe("private");
|
|
63
69
|
expect(
|
|
64
70
|
getControlPlaneTargetAddressSpace("https://my-server.example.com"),
|
|
65
71
|
).toBeUndefined();
|
|
@@ -37,9 +37,13 @@ export function getControlPlaneTargetAddressSpace(
|
|
|
37
37
|
if (isLoopback) {
|
|
38
38
|
return "loopback";
|
|
39
39
|
}
|
|
40
|
-
const isPrivate =
|
|
41
|
-
hostname
|
|
42
|
-
|
|
40
|
+
const isPrivate =
|
|
41
|
+
/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(hostname) ||
|
|
42
|
+
!hostname.includes(".") ||
|
|
43
|
+
hostname.endsWith(".local") ||
|
|
44
|
+
hostname.endsWith(".lan") ||
|
|
45
|
+
hostname.endsWith(".home") ||
|
|
46
|
+
hostname.endsWith(".internal");
|
|
43
47
|
if (isPrivate) {
|
|
44
48
|
return "private";
|
|
45
49
|
}
|
|
@@ -309,7 +313,7 @@ export class ShadowClawControlPlane extends ShadowClawElement {
|
|
|
309
313
|
|
|
310
314
|
const {
|
|
311
315
|
isControlPlaneEnabled,
|
|
312
|
-
|
|
316
|
+
ensureControlPlaneConnected,
|
|
313
317
|
shouldConnectControlPlane,
|
|
314
318
|
} = await import("../../../core/utils/initControlPlane.js");
|
|
315
319
|
|
|
@@ -317,8 +321,10 @@ export class ShadowClawControlPlane extends ShadowClawElement {
|
|
|
317
321
|
(await isControlPlaneEnabled(this.db)) &&
|
|
318
322
|
shouldConnectControlPlane()
|
|
319
323
|
) {
|
|
320
|
-
|
|
324
|
+
await ensureControlPlaneConnected({
|
|
321
325
|
orchestrator: this.orchestrator || undefined,
|
|
326
|
+
db: this.db,
|
|
327
|
+
force: true,
|
|
322
328
|
transport,
|
|
323
329
|
});
|
|
324
330
|
}
|
|
@@ -371,7 +377,7 @@ export class ShadowClawControlPlane extends ShadowClawElement {
|
|
|
371
377
|
|
|
372
378
|
const {
|
|
373
379
|
isControlPlaneEnabled,
|
|
374
|
-
|
|
380
|
+
ensureControlPlaneConnected,
|
|
375
381
|
shouldConnectControlPlane,
|
|
376
382
|
} = await import("../../../core/utils/initControlPlane.js");
|
|
377
383
|
|
|
@@ -379,8 +385,10 @@ export class ShadowClawControlPlane extends ShadowClawElement {
|
|
|
379
385
|
(await isControlPlaneEnabled(this.db)) &&
|
|
380
386
|
shouldConnectControlPlane()
|
|
381
387
|
) {
|
|
382
|
-
|
|
388
|
+
await ensureControlPlaneConnected({
|
|
383
389
|
orchestrator: this.orchestrator || undefined,
|
|
390
|
+
db: this.db,
|
|
391
|
+
force: true,
|
|
384
392
|
});
|
|
385
393
|
}
|
|
386
394
|
|
|
@@ -424,15 +432,17 @@ export class ShadowClawControlPlane extends ShadowClawElement {
|
|
|
424
432
|
}
|
|
425
433
|
|
|
426
434
|
const {
|
|
427
|
-
|
|
435
|
+
ensureControlPlaneConnected,
|
|
428
436
|
stopControlPlaneClient,
|
|
429
437
|
shouldConnectControlPlane,
|
|
430
438
|
} = await import("../../../core/utils/initControlPlane.js");
|
|
431
439
|
|
|
432
440
|
if (enabled) {
|
|
433
441
|
if (shouldConnectControlPlane()) {
|
|
434
|
-
|
|
442
|
+
await ensureControlPlaneConnected({
|
|
435
443
|
orchestrator: this.orchestrator || undefined,
|
|
444
|
+
db: this.db,
|
|
445
|
+
force: true,
|
|
436
446
|
});
|
|
437
447
|
}
|
|
438
448
|
showSuccess("Control plane client enabled and connected", 3000);
|
|
@@ -21,11 +21,15 @@ jest.unstable_mockModule(
|
|
|
21
21
|
}),
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
+
const mockEnsureAllConnections = jest
|
|
25
|
+
.fn()
|
|
26
|
+
.mockResolvedValue(undefined as never);
|
|
24
27
|
jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
|
|
25
28
|
orchestratorStore: {
|
|
26
29
|
orchestrator: {
|
|
27
30
|
taskServerUrl: "/test-schedule",
|
|
28
31
|
taskServerEnabled: true,
|
|
32
|
+
ensureAllConnections: mockEnsureAllConnections,
|
|
29
33
|
},
|
|
30
34
|
ready: true,
|
|
31
35
|
},
|
|
@@ -81,6 +85,7 @@ describe("shadow-claw-task-server", () => {
|
|
|
81
85
|
"https://new-server.com",
|
|
82
86
|
);
|
|
83
87
|
expect(showSuccess).toHaveBeenCalled();
|
|
88
|
+
expect(mockEnsureAllConnections).toHaveBeenCalled();
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
it("should save task server enabled toggle", async () => {
|
|
@@ -95,5 +100,14 @@ describe("shadow-claw-task-server", () => {
|
|
|
95
100
|
expect.anything(),
|
|
96
101
|
false,
|
|
97
102
|
);
|
|
103
|
+
|
|
104
|
+
mockEnsureAllConnections.mockClear();
|
|
105
|
+
await el.saveTaskServerEnabled(true);
|
|
106
|
+
expect(mockSetTaskServerEnabled).toHaveBeenCalledWith(
|
|
107
|
+
orchestratorStore.orchestrator,
|
|
108
|
+
expect.anything(),
|
|
109
|
+
true,
|
|
110
|
+
);
|
|
111
|
+
expect(mockEnsureAllConnections).toHaveBeenCalled();
|
|
98
112
|
});
|
|
99
113
|
});
|
|
@@ -108,6 +108,12 @@ export class ShadowClawTaskServer extends ShadowClawElement {
|
|
|
108
108
|
|
|
109
109
|
try {
|
|
110
110
|
await setTaskServerEnabled(this.orchestrator, this.db, enabled);
|
|
111
|
+
if (
|
|
112
|
+
enabled &&
|
|
113
|
+
typeof this.orchestrator.ensureAllConnections === "function"
|
|
114
|
+
) {
|
|
115
|
+
void this.orchestrator.ensureAllConnections();
|
|
116
|
+
}
|
|
111
117
|
} catch (err) {
|
|
112
118
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
113
119
|
showError("Error saving Task Server setting: " + errorMsg, 6000);
|
|
@@ -135,6 +141,12 @@ export class ShadowClawTaskServer extends ShadowClawElement {
|
|
|
135
141
|
|
|
136
142
|
try {
|
|
137
143
|
await setTaskServerUrl(this.orchestrator, this.db, url || "/schedule");
|
|
144
|
+
if (
|
|
145
|
+
this.orchestrator.taskServerEnabled &&
|
|
146
|
+
typeof this.orchestrator.ensureAllConnections === "function"
|
|
147
|
+
) {
|
|
148
|
+
void this.orchestrator.ensureAllConnections();
|
|
149
|
+
}
|
|
138
150
|
showSuccess("Task Server URL saved", 3000);
|
|
139
151
|
} catch (err) {
|
|
140
152
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -88,6 +88,7 @@ export class ShadowClaw extends ShadowClawElement {
|
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
vmStatusCleanup: (() => void) | null = null;
|
|
91
|
+
appLifecycleCleanup: (() => void) | null = null;
|
|
91
92
|
|
|
92
93
|
constructor() {
|
|
93
94
|
super();
|
|
@@ -251,6 +252,11 @@ export class ShadowClaw extends ShadowClawElement {
|
|
|
251
252
|
this.vmStatusCleanup = null;
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
if (this.appLifecycleCleanup) {
|
|
256
|
+
this.appLifecycleCleanup();
|
|
257
|
+
this.appLifecycleCleanup = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
254
260
|
if (this.terminalPlacementFrame !== null) {
|
|
255
261
|
cancelAnimationFrame(this.terminalPlacementFrame);
|
|
256
262
|
this.terminalPlacementFrame = null;
|
|
@@ -27,6 +27,8 @@ describe("bindEventListeners", () => {
|
|
|
27
27
|
let mockUpdateHeaderMainToggle: jest.Mock<any>;
|
|
28
28
|
let mockUpdateHostTheme: jest.Mock<any>;
|
|
29
29
|
let mockUpdateThemeIcons: jest.Mock<any>;
|
|
30
|
+
let mockSetupAppLifecycle: jest.Mock<any>;
|
|
31
|
+
let mockLifecycleCleanup: jest.Mock<any>;
|
|
30
32
|
|
|
31
33
|
beforeEach(async () => {
|
|
32
34
|
jest.resetModules();
|
|
@@ -54,6 +56,11 @@ describe("bindEventListeners", () => {
|
|
|
54
56
|
jest.unstable_mockModule("./supportsNavigationApi.js", () => ({
|
|
55
57
|
supportsNavigationApi: jest.fn(),
|
|
56
58
|
}));
|
|
59
|
+
mockLifecycleCleanup = jest.fn();
|
|
60
|
+
mockSetupAppLifecycle = jest.fn(() => mockLifecycleCleanup);
|
|
61
|
+
jest.unstable_mockModule("./setupAppLifecycle.js", () => ({
|
|
62
|
+
setupAppLifecycle: mockSetupAppLifecycle,
|
|
63
|
+
}));
|
|
57
64
|
jest.unstable_mockModule(
|
|
58
65
|
"./syncPageHeaderMainVisibilityOverride.js",
|
|
59
66
|
() => ({
|
|
@@ -940,4 +947,54 @@ describe("bindEventListeners", () => {
|
|
|
940
947
|
);
|
|
941
948
|
});
|
|
942
949
|
});
|
|
950
|
+
|
|
951
|
+
describe("app lifecycle", () => {
|
|
952
|
+
it("should set up app lifecycle listener when orchestrator is present", () => {
|
|
953
|
+
shadowClaw.orchestrator = { ensureAllConnections: jest.fn() };
|
|
954
|
+
|
|
955
|
+
bindEventListeners(
|
|
956
|
+
win,
|
|
957
|
+
doc,
|
|
958
|
+
shadow,
|
|
959
|
+
shadowClaw,
|
|
960
|
+
db,
|
|
961
|
+
oStore,
|
|
962
|
+
fStore,
|
|
963
|
+
tStore,
|
|
964
|
+
url,
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
expect(mockSetupAppLifecycle).toHaveBeenCalledWith(
|
|
968
|
+
win,
|
|
969
|
+
doc,
|
|
970
|
+
shadowClaw.orchestrator,
|
|
971
|
+
);
|
|
972
|
+
expect(shadowClaw.appLifecycleCleanup).toBe(mockLifecycleCleanup);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it("should clean up previous app lifecycle listener before setting up new one", () => {
|
|
976
|
+
const prevCleanup = jest.fn();
|
|
977
|
+
shadowClaw.appLifecycleCleanup = prevCleanup;
|
|
978
|
+
shadowClaw.orchestrator = { ensureAllConnections: jest.fn() };
|
|
979
|
+
|
|
980
|
+
bindEventListeners(
|
|
981
|
+
win,
|
|
982
|
+
doc,
|
|
983
|
+
shadow,
|
|
984
|
+
shadowClaw,
|
|
985
|
+
db,
|
|
986
|
+
oStore,
|
|
987
|
+
fStore,
|
|
988
|
+
tStore,
|
|
989
|
+
url,
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
expect(prevCleanup).toHaveBeenCalled();
|
|
993
|
+
expect(mockSetupAppLifecycle).toHaveBeenCalledWith(
|
|
994
|
+
win,
|
|
995
|
+
doc,
|
|
996
|
+
shadowClaw.orchestrator,
|
|
997
|
+
);
|
|
998
|
+
});
|
|
999
|
+
});
|
|
943
1000
|
});
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./setPagesSidebarHidden.js";
|
|
11
11
|
import { showPage } from "./showPage.js";
|
|
12
12
|
import { supportsNavigationApi } from "./supportsNavigationApi.js";
|
|
13
|
+
import { setupAppLifecycle } from "./setupAppLifecycle.js";
|
|
13
14
|
import { syncPageHeaderMainVisibilityOverride } from "./syncPageHeaderMainVisibilityOverride.js";
|
|
14
15
|
|
|
15
16
|
import { toggleActivityLogVisibility } from "./toggleActivityLogVisibility.js";
|
|
@@ -298,4 +299,17 @@ export function bindEventListeners(
|
|
|
298
299
|
const currentTheme = tStore.resolved;
|
|
299
300
|
updateThemeIcons(shadow, currentTheme);
|
|
300
301
|
updateHostTheme(currentTheme, shadowClaw.classList);
|
|
302
|
+
|
|
303
|
+
if (shadowClaw.appLifecycleCleanup) {
|
|
304
|
+
shadowClaw.appLifecycleCleanup();
|
|
305
|
+
shadowClaw.appLifecycleCleanup = null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (shadowClaw.orchestrator) {
|
|
309
|
+
shadowClaw.appLifecycleCleanup = setupAppLifecycle(
|
|
310
|
+
win,
|
|
311
|
+
doc,
|
|
312
|
+
shadowClaw.orchestrator,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
301
315
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { setupAppLifecycle } from "./setupAppLifecycle.js";
|
|
3
|
+
|
|
4
|
+
describe("setupAppLifecycle", () => {
|
|
5
|
+
let win: any;
|
|
6
|
+
let doc: any;
|
|
7
|
+
let orchestrator: any;
|
|
8
|
+
let winListeners: Record<string, Function>;
|
|
9
|
+
let docListeners: Record<string, Function>;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.useFakeTimers();
|
|
13
|
+
winListeners = {};
|
|
14
|
+
docListeners = {};
|
|
15
|
+
|
|
16
|
+
win = {
|
|
17
|
+
addEventListener: jest.fn((event: string, handler: Function) => {
|
|
18
|
+
winListeners[event] = handler;
|
|
19
|
+
}),
|
|
20
|
+
removeEventListener: jest.fn((event: string) => {
|
|
21
|
+
delete winListeners[event];
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
doc = {
|
|
26
|
+
visibilityState: "visible",
|
|
27
|
+
addEventListener: jest.fn((event: string, handler: Function) => {
|
|
28
|
+
docListeners[event] = handler;
|
|
29
|
+
}),
|
|
30
|
+
removeEventListener: jest.fn((event: string) => {
|
|
31
|
+
delete docListeners[event];
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
orchestrator = {
|
|
36
|
+
ensureAllConnections: jest.fn().mockResolvedValue(undefined as never),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
jest.useRealTimers();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("registers event listeners on win and doc", () => {
|
|
45
|
+
const cleanup = setupAppLifecycle(win, doc, orchestrator);
|
|
46
|
+
|
|
47
|
+
expect(doc.addEventListener).toHaveBeenCalledWith(
|
|
48
|
+
"visibilitychange",
|
|
49
|
+
expect.any(Function),
|
|
50
|
+
);
|
|
51
|
+
expect(win.addEventListener).toHaveBeenCalledWith(
|
|
52
|
+
"pageshow",
|
|
53
|
+
expect.any(Function),
|
|
54
|
+
);
|
|
55
|
+
expect(win.addEventListener).toHaveBeenCalledWith(
|
|
56
|
+
"focus",
|
|
57
|
+
expect.any(Function),
|
|
58
|
+
);
|
|
59
|
+
expect(win.addEventListener).toHaveBeenCalledWith(
|
|
60
|
+
"online",
|
|
61
|
+
expect.any(Function),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
cleanup();
|
|
65
|
+
|
|
66
|
+
expect(doc.removeEventListener).toHaveBeenCalledWith(
|
|
67
|
+
"visibilitychange",
|
|
68
|
+
expect.any(Function),
|
|
69
|
+
);
|
|
70
|
+
expect(win.removeEventListener).toHaveBeenCalledWith(
|
|
71
|
+
"pageshow",
|
|
72
|
+
expect.any(Function),
|
|
73
|
+
);
|
|
74
|
+
expect(win.removeEventListener).toHaveBeenCalledWith(
|
|
75
|
+
"focus",
|
|
76
|
+
expect.any(Function),
|
|
77
|
+
);
|
|
78
|
+
expect(win.removeEventListener).toHaveBeenCalledWith(
|
|
79
|
+
"online",
|
|
80
|
+
expect.any(Function),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("triggers ensureAllConnections({ force: true }) on wake up from sleep (> 1000ms hidden)", () => {
|
|
85
|
+
setupAppLifecycle(win, doc, orchestrator, {
|
|
86
|
+
debounceMs: 100,
|
|
87
|
+
minSleepDurationMs: 1000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// App goes to sleep / hidden
|
|
91
|
+
doc.visibilityState = "hidden";
|
|
92
|
+
docListeners["visibilitychange"]();
|
|
93
|
+
|
|
94
|
+
// Advance time by 5 seconds while phone sleeps
|
|
95
|
+
jest.advanceTimersByTime(5000);
|
|
96
|
+
|
|
97
|
+
// App wakes up
|
|
98
|
+
doc.visibilityState = "visible";
|
|
99
|
+
docListeners["visibilitychange"]();
|
|
100
|
+
|
|
101
|
+
// Before debounce timer fires
|
|
102
|
+
expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
|
|
103
|
+
|
|
104
|
+
// Advance past debounce
|
|
105
|
+
jest.advanceTimersByTime(100);
|
|
106
|
+
|
|
107
|
+
expect(orchestrator.ensureAllConnections).toHaveBeenCalledWith({
|
|
108
|
+
force: true,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("triggers ensureAllConnections({ force: false }) on short tab switch (< 1000ms hidden)", () => {
|
|
113
|
+
setupAppLifecycle(win, doc, orchestrator, {
|
|
114
|
+
debounceMs: 100,
|
|
115
|
+
minSleepDurationMs: 1000,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
doc.visibilityState = "hidden";
|
|
119
|
+
docListeners["visibilitychange"]();
|
|
120
|
+
|
|
121
|
+
// Only hidden for 200ms
|
|
122
|
+
jest.advanceTimersByTime(200);
|
|
123
|
+
|
|
124
|
+
doc.visibilityState = "visible";
|
|
125
|
+
docListeners["visibilitychange"]();
|
|
126
|
+
|
|
127
|
+
jest.advanceTimersByTime(100);
|
|
128
|
+
|
|
129
|
+
expect(orchestrator.ensureAllConnections).toHaveBeenCalledWith({
|
|
130
|
+
force: false,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("coalesces rapid resume events (visibilitychange, pageshow, focus, online)", () => {
|
|
135
|
+
setupAppLifecycle(win, doc, orchestrator, {
|
|
136
|
+
debounceMs: 250,
|
|
137
|
+
minSleepDurationMs: 1000,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
doc.visibilityState = "hidden";
|
|
141
|
+
docListeners["visibilitychange"]();
|
|
142
|
+
jest.advanceTimersByTime(2000);
|
|
143
|
+
|
|
144
|
+
// Device wakes up
|
|
145
|
+
doc.visibilityState = "visible";
|
|
146
|
+
docListeners["visibilitychange"]();
|
|
147
|
+
|
|
148
|
+
// Chrome fires pageshow, focus, online right after
|
|
149
|
+
jest.advanceTimersByTime(20);
|
|
150
|
+
winListeners["pageshow"]?.({ persisted: true });
|
|
151
|
+
|
|
152
|
+
jest.advanceTimersByTime(20);
|
|
153
|
+
winListeners["focus"]?.();
|
|
154
|
+
|
|
155
|
+
jest.advanceTimersByTime(20);
|
|
156
|
+
winListeners["online"]?.();
|
|
157
|
+
|
|
158
|
+
// Total 60ms elapsed, still within 250ms debounce
|
|
159
|
+
expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
|
|
160
|
+
|
|
161
|
+
// Advance remaining debounce time
|
|
162
|
+
jest.advanceTimersByTime(250);
|
|
163
|
+
|
|
164
|
+
// Should only be called ONCE
|
|
165
|
+
expect(orchestrator.ensureAllConnections).toHaveBeenCalledTimes(1);
|
|
166
|
+
expect(orchestrator.ensureAllConnections).toHaveBeenCalledWith({
|
|
167
|
+
force: true,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("cancels pending reconnect if app goes hidden again during debounce", () => {
|
|
172
|
+
setupAppLifecycle(win, doc, orchestrator, { debounceMs: 250 });
|
|
173
|
+
|
|
174
|
+
doc.visibilityState = "visible";
|
|
175
|
+
docListeners["visibilitychange"]();
|
|
176
|
+
|
|
177
|
+
// Debounce is pending
|
|
178
|
+
expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
|
|
179
|
+
|
|
180
|
+
// User immediately switches away / phone locks before debounce expires
|
|
181
|
+
doc.visibilityState = "hidden";
|
|
182
|
+
docListeners["visibilitychange"]();
|
|
183
|
+
|
|
184
|
+
// Advance past debounce
|
|
185
|
+
jest.advanceTimersByTime(500);
|
|
186
|
+
|
|
187
|
+
expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("cleanup cancels any pending debounce timer", () => {
|
|
191
|
+
const cleanup = setupAppLifecycle(win, doc, orchestrator, {
|
|
192
|
+
debounceMs: 250,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
winListeners["online"]();
|
|
196
|
+
cleanup();
|
|
197
|
+
|
|
198
|
+
jest.advanceTimersByTime(500);
|
|
199
|
+
expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw — App Lifecycle & Connection Health Management
|
|
3
|
+
*
|
|
4
|
+
* Listens for mobile sleep / wake, tab switching, and network changes:
|
|
5
|
+
* - visibilitychange: detects when the app/tab is backgrounded or brought back to foreground.
|
|
6
|
+
* If the app was backgrounded for longer than minSleepDurationMs (default: 1000ms),
|
|
7
|
+
* it indicates the device went to sleep or user switched away, requiring forced reconnection.
|
|
8
|
+
* - pageshow: handles bfcache restoration and page reveal.
|
|
9
|
+
* - focus: handles window/tab focus.
|
|
10
|
+
* - online: handles network connectivity resumption.
|
|
11
|
+
*
|
|
12
|
+
* Coalesces rapid succession of resume events via a debounce window (default: 250ms).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface AppLifecycleOptions {
|
|
16
|
+
debounceMs?: number;
|
|
17
|
+
minSleepDurationMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MinimalOrchestrator {
|
|
21
|
+
ensureAllConnections(options?: { force?: boolean }): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setupAppLifecycle(
|
|
25
|
+
win: Window,
|
|
26
|
+
doc: Document,
|
|
27
|
+
orchestrator: MinimalOrchestrator,
|
|
28
|
+
options: AppLifecycleOptions = {},
|
|
29
|
+
): () => void {
|
|
30
|
+
const debounceMs = options.debounceMs ?? 250;
|
|
31
|
+
const minSleepDurationMs = options.minSleepDurationMs ?? 1000;
|
|
32
|
+
|
|
33
|
+
let lastHiddenTime = 0;
|
|
34
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
35
|
+
let pendingForce = false;
|
|
36
|
+
|
|
37
|
+
const triggerReconnect = (force: boolean) => {
|
|
38
|
+
if (force) {
|
|
39
|
+
pendingForce = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (debounceTimer !== null) {
|
|
43
|
+
clearTimeout(debounceTimer);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
debounceTimer = setTimeout(() => {
|
|
47
|
+
debounceTimer = null;
|
|
48
|
+
const shouldForce = pendingForce;
|
|
49
|
+
pendingForce = false;
|
|
50
|
+
void orchestrator.ensureAllConnections({ force: shouldForce });
|
|
51
|
+
}, debounceMs);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const cancelPendingReconnect = () => {
|
|
55
|
+
if (debounceTimer !== null) {
|
|
56
|
+
clearTimeout(debounceTimer);
|
|
57
|
+
debounceTimer = null;
|
|
58
|
+
}
|
|
59
|
+
pendingForce = false;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const onVisibilityChange = () => {
|
|
63
|
+
if (doc.visibilityState === "hidden") {
|
|
64
|
+
lastHiddenTime = Date.now();
|
|
65
|
+
cancelPendingReconnect();
|
|
66
|
+
} else if (doc.visibilityState === "visible") {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const wasAsleep =
|
|
69
|
+
lastHiddenTime > 0 && now - lastHiddenTime >= minSleepDurationMs;
|
|
70
|
+
triggerReconnect(wasAsleep);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const onPageShow = (event?: PageTransitionEvent) => {
|
|
75
|
+
const isPersisted = Boolean(event && event.persisted);
|
|
76
|
+
triggerReconnect(isPersisted || doc.visibilityState === "visible");
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const onFocus = () => {
|
|
80
|
+
if (doc.visibilityState === "visible") {
|
|
81
|
+
triggerReconnect(false);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const onOnline = () => {
|
|
86
|
+
triggerReconnect(true);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
doc.addEventListener("visibilitychange", onVisibilityChange);
|
|
90
|
+
win.addEventListener("pageshow", onPageShow);
|
|
91
|
+
win.addEventListener("focus", onFocus);
|
|
92
|
+
win.addEventListener("online", onOnline);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
cancelPendingReconnect();
|
|
96
|
+
doc.removeEventListener("visibilitychange", onVisibilityChange);
|
|
97
|
+
win.removeEventListener("pageshow", onPageShow);
|
|
98
|
+
win.removeEventListener("focus", onFocus);
|
|
99
|
+
win.removeEventListener("online", onOnline);
|
|
100
|
+
};
|
|
101
|
+
}
|