From fa1d7dd295163bc4feb9ea859f6ffc2cfd4aa8a2 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 2 Dec 2025 09:55:59 +0000 Subject: [PATCH] feat: migrate to real Zustand store (Phase 2) --- package-lock.json | 60 ++-- packages/app/package.json | 2 +- packages/app/src/hooks/use-daemon-session.ts | 31 ++- packages/app/src/stores/session-store.ts | 273 ++++++++----------- 4 files changed, 164 insertions(+), 202 deletions(-) diff --git a/package-lock.json b/package-lock.json index b214c2492..4f68762df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -146,6 +146,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1527,6 +1528,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -5067,7 +5069,6 @@ "integrity": "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", @@ -5092,7 +5093,6 @@ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -5107,7 +5107,6 @@ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -5129,7 +5128,6 @@ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -5143,7 +5141,6 @@ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=6.6.0" } @@ -5154,7 +5151,6 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -5198,7 +5194,6 @@ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -5217,7 +5212,6 @@ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 0.8" } @@ -5228,7 +5222,6 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -5242,7 +5235,6 @@ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 0.8" } @@ -5253,7 +5245,6 @@ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" }, @@ -5267,7 +5258,6 @@ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -5281,7 +5271,6 @@ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 0.6" } @@ -5292,7 +5281,6 @@ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "optional": true, - "peer": true, "dependencies": { "side-channel": "^1.1.0" }, @@ -5309,7 +5297,6 @@ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -5333,7 +5320,6 @@ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -5350,7 +5336,6 @@ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -6520,6 +6505,7 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.18.tgz", "integrity": "sha512-DZgd6860dxcq3YX7UzIXeBr6m3UgXvo9acxp5jiJyIZXdR00Br9JwVkO7e0bUeTA2d3Z8dsmtAR84Y86NnH64Q==", "license": "MIT", + "peer": true, "dependencies": { "@react-navigation/core": "^7.12.4", "escape-string-regexp": "^4.0.0", @@ -7209,6 +7195,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -7243,6 +7230,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -7364,6 +7352,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -8052,6 +8041,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8112,6 +8102,7 @@ "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.78.tgz", "integrity": "sha512-ec77fmQwJGLduswMrW4AAUGSOiu8dZaIwMmWHHGKsrMUFFS6ugfkTyx0srtuKYHNRRLRC2dT7cPirnUl98VnxA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@ai-sdk/gateway": "2.0.1", "@ai-sdk/provider": "2.0.0", @@ -9000,6 +8991,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -10484,6 +10476,7 @@ "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -11048,6 +11041,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11244,6 +11238,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -11614,6 +11609,7 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.20.tgz", "integrity": "sha512-mWHky+H63W60P5Oo+VbtqzF2sLvdaoSSwG57H9rlq1DrgIla++QJZuwJkXXo55lYPymVmkVhwG6FjWYKKylwpw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.13", @@ -11786,6 +11782,7 @@ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz", "integrity": "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==", "license": "MIT", + "peer": true, "dependencies": { "@expo/config": "~12.0.10", "@expo/env": "~2.0.7" @@ -11866,6 +11863,7 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz", "integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==", "license": "MIT", + "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -11943,6 +11941,7 @@ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.8.tgz", "integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==", "license": "MIT", + "peer": true, "dependencies": { "expo-constants": "~18.0.8", "invariant": "^2.2.4" @@ -13248,6 +13247,7 @@ "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -14771,6 +14771,7 @@ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "license": "MPL-2.0", + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -17236,6 +17237,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -17276,6 +17278,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -17312,6 +17315,7 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -17401,6 +17405,7 @@ "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.7.0.tgz", "integrity": "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -17420,6 +17425,7 @@ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", "license": "MIT", + "peer": true, "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", @@ -17476,6 +17482,7 @@ "integrity": "sha512-+/uVS7FQwOiKYZQERMIvBRv5/X3CVHrFG6Nr/kIhVfVxGeUimHnBz7cgA97lJKIn7AKDRWL+UjLedW8pGOt0dg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -17502,6 +17509,7 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz", "integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==", "license": "MIT", + "peer": true, "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" @@ -17530,6 +17538,7 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz", "integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -17540,6 +17549,7 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", "license": "MIT", + "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -17555,6 +17565,7 @@ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.14.0.tgz", "integrity": "sha512-B3gYc7WztcOT4N54AtUutbe0Nuqqh/nkresY0fAXzUHYLsWuIu/yGiCCD3DKfAs6GLv5LFtWTu7N333Q+e3bkg==", "license": "MIT", + "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -17597,6 +17608,7 @@ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -17629,6 +17641,7 @@ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", @@ -17744,6 +17757,7 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -19782,7 +19796,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -19941,6 +19954,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20297,6 +20311,7 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -21013,6 +21028,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -21027,9 +21043,9 @@ } }, "node_modules/zustand": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", - "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", + "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", "license": "MIT", "engines": { "node": ">=12.20.0" @@ -21107,7 +21123,7 @@ "react-native-web": "~0.21.0", "react-native-worklets": "0.5.1", "zod": "^3.23.8", - "zustand": "^5.0.8" + "zustand": "^5.0.9" }, "devDependencies": { "@types/react": "~19.2.0", diff --git a/packages/app/package.json b/packages/app/package.json index a47191801..48358ef24 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -63,7 +63,7 @@ "react-native-web": "~0.21.0", "react-native-worklets": "0.5.1", "zod": "^3.23.8", - "zustand": "^5.0.8" + "zustand": "^5.0.9" }, "devDependencies": { "@types/react": "~19.2.0", diff --git a/packages/app/src/hooks/use-daemon-session.ts b/packages/app/src/hooks/use-daemon-session.ts index 442dcad3b..9bfcf9b07 100644 --- a/packages/app/src/hooks/use-daemon-session.ts +++ b/packages/app/src/hooks/use-daemon-session.ts @@ -1,8 +1,8 @@ -import { useRef } from "react"; +import { useCallback, useRef } from "react"; import { Alert } from "react-native"; import type { SessionContextValue } from "@/contexts/session-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context"; -import { useSessionDirectory } from "./use-session-directory"; +import { useSessionStore } from "@/stores/session-store"; export class DaemonSessionUnavailableError extends Error { serverId: string; @@ -14,17 +14,6 @@ export class DaemonSessionUnavailableError extends Error { } } -export function getSessionForServer( - serverId: string, - directory: Map -): SessionContextValue { - const session = directory.get(serverId) ?? null; - if (!session) { - throw new DaemonSessionUnavailableError(serverId); - } - return session; -} - type UseDaemonSessionOptions = { suppressUnavailableAlert?: boolean; allowUnavailable?: boolean; @@ -39,7 +28,16 @@ export function useDaemonSession( options: UseDaemonSessionOptions & { allowUnavailable: true } ): SessionContextValue | null; export function useDaemonSession(serverId?: string | null, options?: UseDaemonSessionOptions) { - const sessionDirectory = useSessionDirectory(); + const selectSession = useCallback( + (state: ReturnType) => { + if (!serverId) { + return null; + } + return state.sessions[serverId] ?? null; + }, + [serverId] + ); + const session = useSessionStore(selectSession); const { connectionStates } = useDaemonConnections(); const alertedDaemonsRef = useRef>(new Set()); const loggedDaemonsRef = useRef>(new Set()); @@ -50,7 +48,10 @@ export function useDaemonSession(serverId?: string | null, options?: UseDaemonSe } try { - return getSessionForServer(serverId, sessionDirectory); + if (!session) { + throw new DaemonSessionUnavailableError(serverId); + } + return session; } catch (error) { if (error instanceof DaemonSessionUnavailableError) { const connection = connectionStates.get(serverId); diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index adea9e0de..f87fd7185 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -1,9 +1,9 @@ -import { useSyncExternalStore } from "react"; +import { create } from "zustand"; +import { subscribeWithSelector } from "zustand/middleware"; import type { SessionContextValue } from "@/contexts/session-context"; import type { AgentDirectoryEntry } from "@/types/agent-directory"; import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf"; -// SessionData mirrors SessionContextValue so consumers can subscribe to a single source of truth. export type SessionData = SessionContextValue; interface SessionStoreState { @@ -21,24 +21,14 @@ interface SessionStore extends SessionStoreState { getAgentDirectory: (serverId: string) => AgentDirectoryEntry[] | undefined; } -type SessionListener = () => void; - -let storeState: SessionStoreState = { sessions: {}, agentDirectory: {} }; -const listeners = new Set(); const SESSION_STORE_LOG_TAG = "[SessionStore]"; let sessionStoreUpdateCount = 0; -const emit = () => { - for (const listener of listeners) { - listener(); - } -}; - -const logSessionStoreUpdate = ( +function logSessionStoreUpdate( type: "setSession" | "updateSession" | "clearSession" | "setAgentDirectory" | "clearAgentDirectory", serverId: string, payload?: unknown -) => { +) { if (!isPerfLoggingEnabled()) { return; } @@ -52,128 +42,7 @@ const logSessionStoreUpdate = ( payloadFieldCount: metrics?.fieldCount ?? 0, timestamp: Date.now(), }); -}; - -const updateStoreState = (updater: (prev: SessionStoreState) => SessionStoreState) => { - const next = updater(storeState); - if (next === storeState) { - return; - } - storeState = next; - emit(); -}; - -const setSession: SessionStore["setSession"] = (serverId, data) => { - updateStoreState((prev) => { - if (prev.sessions[serverId] === data) { - return prev; - } - logSessionStoreUpdate("setSession", serverId, data); - return { - ...prev, - sessions: { - ...prev.sessions, - [serverId]: data, - }, - }; - }); -}; - -const updateSession: SessionStore["updateSession"] = (serverId, partial) => { - updateStoreState((prev) => { - const existing = prev.sessions[serverId]; - const next: SessionData | undefined = existing - ? { ...existing, ...partial, serverId } - : partial.serverId - ? ({ ...(partial as SessionData), serverId }) - : undefined; - - if (!next) { - return prev; - } - - if (existing && shallowEqual(existing, next)) { - return prev; - } - - logSessionStoreUpdate("updateSession", serverId, partial); - return { - ...prev, - sessions: { - ...prev.sessions, - [serverId]: next, - }, - }; - }); -}; - -const clearSession: SessionStore["clearSession"] = (serverId) => { - updateStoreState((prev) => { - if (!(serverId in prev.sessions)) { - return prev; - } - logSessionStoreUpdate("clearSession", serverId); - const nextSessions = { ...prev.sessions }; - delete nextSessions[serverId]; - return { ...prev, sessions: nextSessions }; - }); -}; - -const getSession: SessionStore["getSession"] = (serverId) => { - return storeState.sessions[serverId]; -}; - -const setAgentDirectory: SessionStore["setAgentDirectory"] = (serverId, agents) => { - updateStoreState((prev) => { - const existing = prev.agentDirectory[serverId]; - if (existing && areAgentDirectoriesEqual(existing, agents)) { - return prev; - } - logSessionStoreUpdate("setAgentDirectory", serverId, { agentCount: agents.length }); - return { - ...prev, - agentDirectory: { - ...prev.agentDirectory, - [serverId]: agents, - }, - }; - }); -}; - -const clearAgentDirectory: SessionStore["clearAgentDirectory"] = (serverId) => { - updateStoreState((prev) => { - if (!(serverId in prev.agentDirectory)) { - return prev; - } - logSessionStoreUpdate("clearAgentDirectory", serverId); - const nextDirectory = { ...prev.agentDirectory }; - delete nextDirectory[serverId]; - return { ...prev, agentDirectory: nextDirectory }; - }); -}; - -const getAgentDirectory: SessionStore["getAgentDirectory"] = (serverId) => { - return storeState.agentDirectory[serverId]; -}; - -const subscribe = (listener: SessionListener) => { - listeners.add(listener); - return () => { - listeners.delete(listener); - }; -}; - -const buildSnapshot = (): SessionStore => ({ - sessions: storeState.sessions, - agentDirectory: storeState.agentDirectory, - setSession, - updateSession, - clearSession, - getSession, - setAgentDirectory, - clearAgentDirectory, - getAgentDirectory, -}); +} const shallowEqual = (left: SessionData, right: SessionData): boolean => { if (left === right) { @@ -192,40 +61,116 @@ const shallowEqual = (left: SessionData, right: SessionData): boolean => { return true; }; -export function useSessionStore(selector: (state: SessionStore) => T): T { - return useSyncExternalStore( - subscribe, - () => selector(buildSnapshot()), - () => selector(buildSnapshot()) - ); -} - -function areAgentDirectoriesEqual( - left: AgentDirectoryEntry[] | undefined, - right: AgentDirectoryEntry[] -): boolean { +function areAgentDirectoriesEqual(left: AgentDirectoryEntry[] | undefined, right: AgentDirectoryEntry[]): boolean { if (!left) { return false; } if (left.length !== right.length) { return false; } - const leftById = new Map(left.map((entry) => [entry.id, entry])); - for (const entry of right) { - const previous = leftById.get(entry.id); - if (!previous) { + return left.every((entry, index) => { + const other = right[index]; + if (!other) { return false; } - if ( - previous.serverId !== entry.serverId || - previous.title !== entry.title || - previous.status !== entry.status || - previous.provider !== entry.provider || - previous.cwd !== entry.cwd || - previous.lastActivityAt.getTime() !== entry.lastActivityAt.getTime() - ) { - return false; - } - } - return true; + return ( + entry.id === other.id && + entry.status === other.status && + entry.serverId === other.serverId && + entry.lastActivityAt.getTime() === other.lastActivityAt.getTime() + ); + }); } + +export const useSessionStore = create()( + subscribeWithSelector((set, get) => ({ + sessions: {}, + agentDirectory: {}, + setSession: (serverId, data) => { + set((prev) => { + if (prev.sessions[serverId] === data) { + return prev; + } + logSessionStoreUpdate("setSession", serverId, data); + return { + ...prev, + sessions: { + ...prev.sessions, + [serverId]: data, + }, + }; + }); + }, + updateSession: (serverId, partial) => { + set((prev) => { + const existing = prev.sessions[serverId]; + const next: SessionData | undefined = existing + ? { ...existing, ...partial, serverId } + : partial.serverId + ? ({ ...(partial as SessionData), serverId }) + : undefined; + + if (!next) { + return prev; + } + + if (existing && shallowEqual(existing, next)) { + return prev; + } + + logSessionStoreUpdate("updateSession", serverId, partial); + return { + ...prev, + sessions: { + ...prev.sessions, + [serverId]: next, + }, + }; + }); + }, + clearSession: (serverId) => { + set((prev) => { + if (!(serverId in prev.sessions)) { + return prev; + } + logSessionStoreUpdate("clearSession", serverId); + const nextSessions = { ...prev.sessions }; + delete nextSessions[serverId]; + return { ...prev, sessions: nextSessions }; + }); + }, + getSession: (serverId) => { + return get().sessions[serverId]; + }, + setAgentDirectory: (serverId, agents) => { + set((prev) => { + const existing = prev.agentDirectory[serverId]; + if (existing && areAgentDirectoriesEqual(existing, agents)) { + return prev; + } + logSessionStoreUpdate("setAgentDirectory", serverId, { agentCount: agents.length }); + return { + ...prev, + agentDirectory: { + ...prev.agentDirectory, + [serverId]: agents, + }, + }; + }); + }, + clearAgentDirectory: (serverId) => { + set((prev) => { + if (!(serverId in prev.agentDirectory)) { + return prev; + } + logSessionStoreUpdate("clearAgentDirectory", serverId); + const nextDirectory = { ...prev.agentDirectory }; + delete nextDirectory[serverId]; + return { ...prev, agentDirectory: nextDirectory }; + }); + }, + getAgentDirectory: (serverId) => { + return get().agentDirectory[serverId]; + }, + })) +);