Tome # Contributing to Tome This document is a high level explanation of the architecture and codebase. It won't cover everything right now, but will evolve and expand over time. # Architecture Tome is a [Tauri](https://v2.tauri.app/) app – which means the system/desktop side is written in Rust, while the front-end is a static Typescript application. Specifically, a SvelteKit application. ## Frontend The core of the domain logic lives in the frontend. It's a staticly generated SvelteKit application. It's a little different than one you'd build for the web, since we're in a singleton environment inside of a Tauri application. This mostly effects our data layer, which the [Models](#models) section goes more into. At a glance: - SvelteKit written in Typescript, built with Vite. - Tailwind for CSS **COMPONENTS** are isolated UI elements. They shouldn't hold any domain logic and should, but in some cases may update models directly. **ROUTES** are "pages" or "screens". These should handle most of the orchestration logic. With [global reactivity](#global-reactivity) (described below) there really isn't a need for the Page Load functions you'd normally use in Svelte. In rare occasions you may need to run an async function on page load – this is when a `+page.ts` could be handy. **LIB** If it's not orchestration logic, and it's not a UI component, it lives in `lib`. This is where most of the supporting domain logic resides. ### Database For all intents and purposes, the database and models are managed in the frontend. It's done via a Tauri plugin called `tauri-plugin-sql`. Technically, the plugin just creates a bridge between the frontend and backend through Tauri's `invoke` mechanism. So while the queries are technically being made via Sqlx in Rust, as a developers it's all done via our Typescript models. ### Models Models are a big a strange in this application, due to Svelte's reactivity rules. Models are classes with _ONLY_ `static` functions. Svelte's reactivity doesn't support class instances so we need to always be passing around plain-old JS objects (which do work with reactivity). > [!IMPORTANT] > Because of this, every model function needs to accept the object as it's first > argument. ```ts static async function name(session: ISession): string { return session.other.thing.complicated.path.name; } ``` You can change, reassign, etc. the `session` in this example and Svelte will handle state and reactivity appropriately since we're always working with plain objects. This enabled the [global reactivity](#global-reactivity) explained below. ### Model Interfaces Declaration merging with classes in Typescript is annoying if all you want is a plain JS object in the end (ie. _not_ the functions, etc.). Instead, we create an `I` interface for each model. This is the interface we use throughout the application. So the `Session` model has an accompanying `ISession` interface, for example. ### Global Reactivity Each model has an underlying `repo`, which is a Svelte `$state()` object, meaning it, and it's contents, are deeply reactive. The base model logic handles updating the objects within and keeps them up to date with persisted database changes. When the app loads, we sync all models with their database table, then when you create, update, or delete, a record from the database, we replicate the operation in the `repo`. What this allows us to do is derive our model "instances" in routes and components, from a reactive set of objects that will update in real-time, when changes to them are persisted. The UI will automatically update since we're always working with the same set of reactive objects. You don't need to manage state manually. Just update a model like you're updating a database record and the UI will react appropriately. > [!IMPORTANT] > _ALWAYS_ work with models via `const model = $derived(Model.all())` and _NEVER_ `$state()`. The beauty of working with a singleton static application if that you can have top-level objects like `repo` to track things and not have to worry about isolation between requests, users, etc. ## Backend The backend is a Tauri application written in Rust. It's mostly responsible for anything we can't do from the frontend. For example, process management, MCP server communication, etc. It's a relatively thin layer, but is the backbone of everything in Tome. At a glance: - Tauri application written in Rust - Database connectivity via `tauri-plugin-sql` ### Commands We use commands like you would controllers in an MVC web framework. They're the entrypoint for the frontend to accomplish something only the backend can do. With that, they should be extremely concise. Often, if not always, calling a single function in another module where the logic actually lives ### npx/uvx Tome uses a project from the CashApp folks, called [Hermit](https://github.com/cashapp/hermit), which "manages isolated, self-bootstrapping sets of tools in software projects." `uvx` and `npx` are two pass-through scripts bundled with the app. Each one first checks if we've previously installed the corresponding command and if not, uses Hermit to do so, then runs the original command. This is how we run MCP servers.