Model store hooked up

This commit is contained in:
Gregory Schier
2026-03-08 15:42:18 -07:00
parent 0a616eb5e2
commit 96a22c68f2
10 changed files with 254 additions and 154 deletions

View File

@@ -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",

View File

@@ -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<number | null>(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 (
<div className={classNames('h-full w-full grid grid-rows-[auto_1fr]', osType === 'linux' && 'border border-border-subtle')}>
<div
className={classNames(
"h-full w-full grid grid-rows-[auto_1fr]",
osType === "linux" && "border border-border-subtle",
)}
>
<HeaderSize
size="lg"
osType={osType}
@@ -56,41 +71,54 @@ function App() {
>
<div
data-tauri-drag-region
className="flex items-center h-full px-2 text-sm font-semibold text-text-subtle"
className="flex items-center px-2 text-sm font-semibold text-text-subtle"
>
Yaak Proxy
</div>
</HeaderSize>
<main className="overflow-auto p-6">
<section className="flex items-start">
<div className="flex w-full max-w-xl flex-col gap-4">
<div>
<p className="text-sm text-text-subtle">Status: {status}</p>
<p className="mt-1 text-sm text-text-subtle">
Port: {port ?? "Not running"}
</p>
</div>
<main className="overflow-auto p-4">
<div className="flex items-center gap-3 mb-4">
<Button disabled={busy} onClick={startProxy} size="sm" tone="primary">
Start Proxy
</Button>
<Button
disabled={busy}
onClick={stopProxy}
size="sm"
variant="border"
>
Stop Proxy
</Button>
<span className="text-xs text-text-subtlest">
{status}
{port != null && ` · :${port}`}
</span>
</div>
<div className="flex flex-wrap gap-3">
<Button
disabled={busy}
onClick={startProxy}
size="sm"
tone="primary"
>
Start Proxy
</Button>
<Button
disabled={busy}
onClick={stopProxy}
size="sm"
variant="border"
>
Stop Proxy
</Button>
</div>
</div>
</section>
<div className="text-xs font-mono">
{exchanges.length === 0 ? (
<p className="text-text-subtlest">No traffic yet</p>
) : (
<table className="w-full text-left">
<thead>
<tr className="text-text-subtlest border-b border-border-subtle">
<th className="py-1 pr-3 font-medium">Method</th>
<th className="py-1 pr-3 font-medium">URL</th>
<th className="py-1 pr-3 font-medium">Status</th>
</tr>
</thead>
<tbody>
{exchanges.map((ex) => (
<tr key={ex.id} className="border-b border-border-subtle">
<td className="py-1 pr-3">{ex.method}</td>
<td className="py-1 pr-3 truncate max-w-md">{ex.url}</td>
<td className="py-1 pr-3">{ex.resStatus ?? "—"}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</main>
</div>
);
@@ -99,7 +127,9 @@ function App() {
createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<Provider store={jotaiStore}>
<App />
</Provider>
</QueryClientProvider>
</StrictMode>,
);

View File

@@ -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"
},

11
apps/yaak-proxy/store.ts Normal file
View File

@@ -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<HttpExchange>();
export const httpExchangesAtom = orderedListAtom(
"http_exchange",
"createdAt",
"desc",
);

View File

@@ -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" };

View File

@@ -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<ProxyHeader>, reqBody: Array<number> | null, resStatus: number | null, resHeaders: Array<ProxyHeader>, resBody: Array<number> | 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<ProxyHeader>, reqBody: Array<number> | null, resStatus: number | null, resHeaders: Array<ProxyHeader>, resBody: Array<number> | null, error: string | null, };
export type ModelPayload = { model: HttpExchange, change: ModelChangeEvent, };
export type ProxyHeader = { name: string, value: string, };

198
package-lock.json generated
View File

@@ -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"
}

View File

@@ -10,6 +10,7 @@
"packages/ui",
"packages/theme",
"packages/tailwind-config",
"packages/model-store",
"packages/common-lib",
"packages/plugin-runtime",
"packages/plugin-runtime-types",

View File

@@ -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"
}
}

View File

@@ -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<M extends BaseModel> = Record<string, Record<string, M>>;
export type ModelChange = { type: "upsert" } | { type: "delete" };
export function createModelStore<M extends BaseModel>() {
const dataAtom = atom<StoreData<M>>({});
/** Apply a single upsert or delete to the store. */
function applyChange(
prev: StoreData<M>,
modelType: string,
model: M,
change: ModelChange,
): StoreData<M> {
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<K extends keyof M>(
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<T>(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;
}