diff --git a/apps/yaak-client/package.json b/apps/yaak-client/package.json index ee2c69dd..ad088725 100644 --- a/apps/yaak-client/package.json +++ b/apps/yaak-client/package.json @@ -50,7 +50,7 @@ "fuzzbunny": "^1.0.1", "hexy": "^0.3.5", "history": "^5.3.0", - "jotai": "^2.12.2", + "jotai": "^2.18.0", "js-md5": "^0.8.3", "lucide-react": "^0.525.0", "mime": "^4.0.4", diff --git a/apps/yaak-proxy/main.tsx b/apps/yaak-proxy/main.tsx index 1755f27c..a8b05384 100644 --- a/apps/yaak-proxy/main.tsx +++ b/apps/yaak-proxy/main.tsx @@ -1,20 +1,30 @@ -import "./main.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { type } from "@tauri-apps/plugin-os"; import { Button, HeaderSize } from "@yaakapp-internal/ui"; import classNames from "classnames"; -import { StrictMode } from "react"; -import { useState } from "react"; +import { createStore, Provider, useAtomValue } from "jotai"; +import { StrictMode, useState } from "react"; import { createRoot } from "react-dom/client"; -import { rpc } from "./rpc"; +import "./main.css"; +import { listen, rpc } from "./rpc"; +import { applyChange, dataAtom, httpExchangesAtom } from "./store"; const queryClient = new QueryClient(); +const jotaiStore = createStore(); + +// Subscribe to model change events from the backend +listen("model_write", (payload) => { + jotaiStore.set(dataAtom, (prev) => + applyChange(prev, "http_exchange", payload.model, payload.change), + ); +}); function App() { const [status, setStatus] = useState("Idle"); const [port, setPort] = useState(null); const [busy, setBusy] = useState(false); const osType = type(); + const exchanges = useAtomValue(httpExchangesAtom); async function startProxy() { setBusy(true); @@ -45,7 +55,12 @@ function App() { } return ( -
+
Yaak Proxy
-
-
-
-
-

Status: {status}

-

- Port: {port ?? "Not running"} -

-
+
+
+ + + + {status} + {port != null && ` ยท :${port}`} + +
-
- - -
-
-
+
+ {exchanges.length === 0 ? ( +

No traffic yet

+ ) : ( + + + + + + + + + + {exchanges.map((ex) => ( + + + + + + ))} + +
MethodURLStatus
{ex.method}{ex.url}{ex.resStatus ?? "โ€”"}
+ )} +
); @@ -99,7 +127,9 @@ function App() { createRoot(document.getElementById("root") as HTMLElement).render( - + + + , ); diff --git a/apps/yaak-proxy/package.json b/apps/yaak-proxy/package.json index 728ad836..7abad0b0 100644 --- a/apps/yaak-proxy/package.json +++ b/apps/yaak-proxy/package.json @@ -13,8 +13,10 @@ "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-os": "^2.3.2", "@yaakapp-internal/theme": "^1.0.0", + "@yaakapp-internal/model-store": "^1.0.0", "@yaakapp-internal/ui": "^1.0.0", "classnames": "^2.5.1", + "jotai": "^2.18.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/apps/yaak-proxy/store.ts b/apps/yaak-proxy/store.ts new file mode 100644 index 00000000..0ae5d252 --- /dev/null +++ b/apps/yaak-proxy/store.ts @@ -0,0 +1,11 @@ +import { createModelStore } from "@yaakapp-internal/model-store"; +import type { HttpExchange } from "../../crates-proxy/yaak-proxy-lib/bindings/gen_models"; + +export const { dataAtom, applyChange, listAtom, orderedListAtom } = + createModelStore(); + +export const httpExchangesAtom = orderedListAtom( + "http_exchange", + "createdAt", + "desc", +); diff --git a/crates-proxy/yaak-proxy-lib/bindings/ModelChangeEvent.ts b/crates-proxy/yaak-proxy-lib/bindings/ModelChangeEvent.ts new file mode 100644 index 00000000..36c8397c --- /dev/null +++ b/crates-proxy/yaak-proxy-lib/bindings/ModelChangeEvent.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelChangeEvent = { "type": "upsert", created: boolean, } | { "type": "delete" }; diff --git a/crates-proxy/yaak-proxy-lib/bindings/gen_models.ts b/crates-proxy/yaak-proxy-lib/bindings/gen_models.ts index a1e4941f..ba452b1c 100644 --- a/crates-proxy/yaak-proxy-lib/bindings/gen_models.ts +++ b/crates-proxy/yaak-proxy-lib/bindings/gen_models.ts @@ -1,9 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ModelChangeEvent } from "./ModelChangeEvent"; -export type ModelChangeEvent = { "type": "upsert" } | { "type": "delete" }; +export type HttpExchange = { id: string, createdAt: string, updatedAt: string, url: string, method: string, reqHeaders: Array, reqBody: Array | null, resStatus: number | null, resHeaders: Array, resBody: Array | null, error: string | null, }; -export type ModelPayload = { model: ProxyEntry, change: ModelChangeEvent, }; - -export type ProxyEntry = { id: string, createdAt: string, updatedAt: string, url: string, method: string, reqHeaders: Array, reqBody: Array | null, resStatus: number | null, resHeaders: Array, resBody: Array | null, error: string | null, }; +export type ModelPayload = { model: HttpExchange, change: ModelChangeEvent, }; export type ProxyHeader = { name: string, value: string, }; diff --git a/package-lock.json b/package-lock.json index 7494b34d..1146f997 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "packages/ui", "packages/theme", "packages/tailwind-config", + "packages/model-store", "packages/common-lib", "packages/plugin-runtime", "packages/plugin-runtime-types", @@ -132,7 +133,7 @@ "fuzzbunny": "^1.0.1", "hexy": "^0.3.5", "history": "^5.3.0", - "jotai": "^2.12.2", + "jotai": "^2.18.0", "js-md5": "^0.8.3", "lucide-react": "^0.525.0", "mime": "^4.0.4", @@ -186,9 +187,10 @@ } }, "apps/yaak-client/node_modules/@types/node": { - "version": "24.10.4", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } @@ -227,8 +229,11 @@ "@tanstack/react-query": "^5.90.5", "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-os": "^2.3.2", + "@yaakapp-internal/model-store": "^1.0.0", "@yaakapp-internal/theme": "^1.0.0", "@yaakapp-internal/ui": "^1.0.0", + "classnames": "^2.5.1", + "jotai": "^2.18.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -3976,15 +3981,20 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "dev": true, - "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true + }, "node_modules/@types/openapi-to-postmanv2": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/openapi-to-postmanv2/-/openapi-to-postmanv2-5.0.0.tgz", @@ -4419,6 +4429,10 @@ "resolved": "crates-tauri/yaak-mac-window", "link": true }, + "node_modules/@yaakapp-internal/model-store": { + "resolved": "packages/model-store", + "link": true + }, "node_modules/@yaakapp-internal/models": { "resolved": "crates/yaak-models", "link": true @@ -5669,10 +5683,9 @@ } }, "node_modules/codemirror-json-schema/node_modules/@types/node": { - "version": "20.19.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", - "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", - "license": "MIT", + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "dependencies": { "undici-types": "~6.21.0" } @@ -5957,11 +5970,10 @@ } }, "node_modules/cpx2/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -6562,11 +6574,10 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -7486,15 +7497,6 @@ "node": ">=10.0" } }, - "node_modules/format-graphql/node_modules/graphql": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz", - "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", - "license": "MIT", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7812,11 +7814,10 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -7928,13 +7929,11 @@ } }, "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", - "license": "MIT", - "peer": true, + "version": "15.10.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz", + "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + "node": ">= 10.x" } }, "node_modules/graphql-language-service": { @@ -9090,10 +9089,9 @@ } }, "node_modules/jotai": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.16.1.tgz", - "integrity": "sha512-vrHcAbo3P7Br37C8Bv6JshMtlKMPqqmx0DDREtTjT4nf3QChDrYdbH+4ik/9V0cXA57dK28RkJ5dctYvavcIlg==", - "license": "MIT", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.18.0.tgz", + "integrity": "sha512-XI38kGWAvtxAZ+cwHcTgJsd+kJOJGf3OfL4XYaXWZMZ7IIY8e53abpIHvtVn1eAgJ5dlgwlGFnP4psrZ/vZbtA==", "engines": { "node": ">=12.20.0" }, @@ -9229,18 +9227,6 @@ "valid-url": "^1.0.9" } }, - "node_modules/json-schema-library/node_modules/smtp-address-parser": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.0.10.tgz", - "integrity": "sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg==", - "license": "MIT", - "dependencies": { - "nearley": "^2.20.1" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/json-schema-merge-allof": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", @@ -9334,10 +9320,9 @@ } }, "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "bin": { "semver": "bin/semver.js" }, @@ -10731,11 +10716,10 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -11943,28 +11927,26 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, - "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -13129,11 +13111,10 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -13142,18 +13123,17 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -13777,10 +13757,9 @@ } }, "node_modules/smtp-address-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.1.0.tgz", - "integrity": "sha512-Gz11jbNU0plrReU9Sj7fmshSBxxJ9ShdD2q4ktHIHo/rpTH6lFyQoYHYKINPJtPe8aHFnsbtW46Ls0tCCBsIZg==", - "license": "MIT", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.0.10.tgz", + "integrity": "sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg==", "dependencies": { "nearley": "^2.20.1" }, @@ -14149,13 +14128,12 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -16110,6 +16088,13 @@ "name": "@yaakapp-internal/lib", "version": "1.0.0" }, + "packages/model-store": { + "name": "@yaakapp-internal/model-store", + "version": "1.0.0", + "peerDependencies": { + "jotai": "^2.18.0" + } + }, "packages/plugin-runtime": { "name": "@yaakapp-internal/plugin-runtime", "dependencies": { @@ -16130,8 +16115,9 @@ } }, "packages/plugin-runtime-types/node_modules/@types/node": { - "version": "24.10.4", - "license": "MIT", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dependencies": { "undici-types": "~7.16.0" } @@ -16155,20 +16141,7 @@ }, "packages/ui": { "name": "@yaakapp-internal/ui", - "version": "1.0.0", - "dependencies": { - "@tanstack/react-query": "^5.90.5", - "@tauri-apps/api": "^2.9.1", - "@yaakapp-internal/lib": "^1.0.0", - "classnames": "^2.5.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-use": "^17.6.0" - }, - "peerDependencies": { - "react": "^19.1.0", - "react-dom": "^19.1.0" - } + "version": "1.0.0" }, "plugins-external/faker": { "name": "@yaak/faker", @@ -16209,11 +16182,10 @@ } }, "plugins-external/httpsnippet/node_modules/@types/node": { - "version": "22.19.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", - "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } diff --git a/package.json b/package.json index 3de33e94..dfbe6b42 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "packages/ui", "packages/theme", "packages/tailwind-config", + "packages/model-store", "packages/common-lib", "packages/plugin-runtime", "packages/plugin-runtime-types", diff --git a/packages/model-store/package.json b/packages/model-store/package.json new file mode 100644 index 00000000..5176d5fc --- /dev/null +++ b/packages/model-store/package.json @@ -0,0 +1,11 @@ +{ + "name": "@yaakapp-internal/model-store", + "private": true, + "version": "1.0.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "peerDependencies": { + "jotai": "^2.18.0" + } +} diff --git a/packages/model-store/src/index.ts b/packages/model-store/src/index.ts new file mode 100644 index 00000000..3d24cb96 --- /dev/null +++ b/packages/model-store/src/index.ts @@ -0,0 +1,71 @@ +import { atom } from "jotai"; +import { selectAtom } from "jotai/utils"; + +/** Any model must at least have an id. */ +type BaseModel = { id: string }; + +/** The raw store shape: model type string โ†’ id โ†’ model instance. */ +type StoreData = Record>; + +export type ModelChange = { type: "upsert" } | { type: "delete" }; + +export function createModelStore() { + const dataAtom = atom>({}); + + /** Apply a single upsert or delete to the store. */ + function applyChange( + prev: StoreData, + modelType: string, + model: M, + change: ModelChange, + ): StoreData { + if (change.type === "upsert") { + return { + ...prev, + [modelType]: { ...prev[modelType], [model.id]: model }, + }; + } else { + const bucket = { ...prev[modelType] }; + delete bucket[model.id]; + return { ...prev, [modelType]: bucket }; + } + } + + /** Atom that selects all models of a given type as an array. */ + function listAtom(modelType: string) { + return selectAtom( + dataAtom, + (data) => Object.values(data[modelType] ?? {}), + shallowEqual, + ); + } + + /** Atom that selects all models of a given type, sorted by a field. */ + function orderedListAtom( + modelType: string, + field: K, + order: "asc" | "desc", + ) { + return selectAtom( + dataAtom, + (data) => { + const vals = Object.values(data[modelType] ?? {}); + return vals.sort((a, b) => { + const n = a[field] > b[field] ? 1 : -1; + return order === "desc" ? -n : n; + }); + }, + shallowEqual, + ); + } + + return { dataAtom, applyChange, listAtom, orderedListAtom }; +} + +function shallowEqual(a: T[], b: T[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +}