Add template
This commit is contained in:
parent
9aa3caa0c2
commit
5800a5ebbf
27 changed files with 7866 additions and 1 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
.direnv/
|
||||||
|
result
|
||||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
apps/web/dist/
|
||||||
|
apps/web/src/routeTree.gen.ts
|
||||||
|
node_modules/
|
||||||
|
result
|
||||||
15
LICENSE
Normal file
15
LICENSE
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Copyright (c) 2025 Methanium
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
No part of this software, source code, documentation, or
|
||||||
|
associated materials may be copied, reproduced, modified,
|
||||||
|
distributed, published, sublicensed, sold, or used to create
|
||||||
|
derivative works without prior written permission from the
|
||||||
|
copyright holder.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EXPRESS OR IMPLIED. TO THE MAXIMUM
|
||||||
|
EXTENT PERMITTED BY LAW, THE COPYRIGHT HOLDER SHALL
|
||||||
|
NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
||||||
|
LIABILITY ARISING FROM THE SOFTWARE OR ITS USE.
|
||||||
51
README.md
51
README.md
|
|
@ -1,2 +1,51 @@
|
||||||
# template
|
# Methanium Template
|
||||||
|
|
||||||
|
A pnpm React template with Vite, Tailwind CSS, Methanium UI, MTP, TanStack Router, and a base nix flake.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix develop
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure MTP
|
||||||
|
|
||||||
|
1. Define the host's communication and data types in `apps/web/type-maps.yaml`.
|
||||||
|
2. Set the connection options in `apps/web/src/mtp-options.ts`.
|
||||||
|
|
||||||
|
The reusable provider and `useMTP` hook are in `packages/mtp/`.
|
||||||
|
|
||||||
|
## Nix
|
||||||
|
|
||||||
|
Build the static frontend:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix build
|
||||||
|
```
|
||||||
|
|
||||||
|
To serve it with nginx on NixOS, import the included module:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.methanium-template.url =
|
||||||
|
"git+https://git.methanium.net/methanium/template";
|
||||||
|
|
||||||
|
outputs = {nixpkgs, methanium-template, ...}: {
|
||||||
|
nixosConfigurations.host = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
methanium-template.nixosModules.default
|
||||||
|
{
|
||||||
|
services.methanium-template = {
|
||||||
|
enable = true;
|
||||||
|
hostName = "app.example.com";
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
||||||
38
apps/web/index.html
Normal file
38
apps/web/index.html
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark" data-theme="methanium">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#171e26" />
|
||||||
|
<title>Methanium Vite Template</title>
|
||||||
|
<style>
|
||||||
|
html.dark,
|
||||||
|
html.dark body {
|
||||||
|
background-color: #171e26;
|
||||||
|
color: #d2e0f0;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light,
|
||||||
|
html.light body {
|
||||||
|
background-color: #edf1f4;
|
||||||
|
color: #10161d;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
const savedTheme = localStorage.getItem("theme");
|
||||||
|
const theme =
|
||||||
|
savedTheme === "light" ||
|
||||||
|
(savedTheme === "system" &&
|
||||||
|
matchMedia("(prefers-color-scheme: light)").matches)
|
||||||
|
? "light"
|
||||||
|
: "dark";
|
||||||
|
document.documentElement.classList.replace("dark", theme);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
apps/web/package.json
Normal file
28
apps/web/package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "@methanium/template-web",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@methanium/template-mtp": "workspace:*",
|
||||||
|
"@methanium/ui": "*",
|
||||||
|
"@tanstack/react-router": "^1.131.35",
|
||||||
|
"lucide-react": "^1.21.0",
|
||||||
|
"mtp": "*",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
|
"@tanstack/router-plugin": "^1.131.35",
|
||||||
|
"@types/react": "^19.2.0",
|
||||||
|
"@types/react-dom": "^19.2.0",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"tailwindcss": "^4.3.1",
|
||||||
|
"vite": "^7.1.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
apps/web/public/methanium.svg
Normal file
27
apps/web/public/methanium.svg
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="4441.4" height="6468.8" shape-rendering="geometricPrecision" viewBox="0 0 4441 6469">
|
||||||
|
<path fill="#331c4e" d="m1714 6033-33-248-5-676 69-1110-247-2069-388-302 196 3614Z"/>
|
||||||
|
<path fill="#6a438a" d="m1864 2038-214 3093 31 640 820-1486 125-2262Z"/>
|
||||||
|
<path fill="#291641" d="m3130 1586-504 437-125 2262 305 315Z"/>
|
||||||
|
<path fill="#0e0619" d="m2501 4285 305 315-120 1018-384 830h-56Z"/>
|
||||||
|
<path fill="#211136" d="m1762 6196 247 254 237-2 255-2163-822 1476 17 221Z"/>
|
||||||
|
<path fill="#3f245b" d="m740 4907 255-267 719 1393 48 163-340-112Z"/>
|
||||||
|
<path fill="#05020d" d="m1407 6080-147 198 414 172h335l-247-254Z"/>
|
||||||
|
<path fill="#030106" d="m1273 6283-85 173 486-6Z"/>
|
||||||
|
<path fill="#0a0413" d="m10 5229 297-41 966 1095-85 173-100 3Z"/>
|
||||||
|
<path fill="#5b3878" d="m740 4907 682 1177-149 199-966-1095Z"/>
|
||||||
|
<path fill="#794f9a" d="M159 4369 10 5229l297-41Z"/>
|
||||||
|
<path fill="#b984c5" d="m307 5188 433-281-581-538Z"/>
|
||||||
|
<path fill="#d4abda" d="m159 4369 581 538 255-267Z"/>
|
||||||
|
<path d="m2686 5604 312 564-179 263-517 17Z"/>
|
||||||
|
<path fill="#130922" d="m2686 5604 294-421 588-268 48 443-618 810Z"/>
|
||||||
|
<path fill="#975dac" d="m2686 5604 1115-1590-379-274-666 1279Z"/>
|
||||||
|
<path fill="#6a438a" d="m2980 5183 588-268 511-814-429-152Z"/>
|
||||||
|
<path fill="#190c2b" d="m4079 4101 352 111-815 1146-48-443Z"/>
|
||||||
|
<path fill="#f0d7f2" d="m2045 11-547 1919 366 108 762-15 70-59Z"/>
|
||||||
|
<path fill="#a558aa" d="m2045 11 651 1953 434-378Z"/>
|
||||||
|
<path fill="#b06ab6" d="m2045 11-547 1919-388-302Z"/>
|
||||||
|
<path fill="#885ca8" d="m1498 1930 238 1992 128-1884Z"/>
|
||||||
|
<path fill="#c698d0" d="m3650 3949 429 152 273-790Z"/>
|
||||||
|
<path fill="#4c2d68" d="m4079 4101 352 111-79-901Z"/>
|
||||||
|
<path fill="#e2c1e6" d="m3422 3740 228 209 702-638Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
57
apps/web/src/components/layout/Header.tsx
Normal file
57
apps/web/src/components/layout/Header.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
|
||||||
|
import { Button, useTheme } from "@methanium/ui";
|
||||||
|
|
||||||
|
const homepageUrl = "https://methanium.net";
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const { resolvedPolarity, setTheme } = useTheme();
|
||||||
|
const isDark = resolvedPolarity === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-40 h-12 border-b border-sidebar-border bg-sidebar text-sidebar-foreground shadow-sm">
|
||||||
|
<div className="flex h-full items-center justify-between overflow-x-auto px-[10px]">
|
||||||
|
<Button
|
||||||
|
nativeButton={false}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="mr-1 shrink-0 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
style={{ width: "46.59px", height: "36px", padding: "3px 13px" }}
|
||||||
|
render={<a href="/" aria-label="Methanium home" />}
|
||||||
|
>
|
||||||
|
<img src="/methanium.svg" alt="" className="h-8 w-6 object-contain" />
|
||||||
|
</Button>
|
||||||
|
<nav
|
||||||
|
className="mr-auto flex h-full shrink-0 items-center"
|
||||||
|
aria-label="Primary"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
nativeButton={false}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
render={<a href={homepageUrl} />}
|
||||||
|
>
|
||||||
|
Homepage
|
||||||
|
</Button>
|
||||||
|
</nav>
|
||||||
|
<div className="flex h-full items-center gap-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
||||||
|
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||||
|
>
|
||||||
|
{isDark ? (
|
||||||
|
<Sun className="size-3.5" />
|
||||||
|
) : (
|
||||||
|
<Moon className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
apps/web/src/main.tsx
Normal file
32
apps/web/src/main.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { BUILT_IN_THEMES, ThemeProvider } from "@methanium/ui";
|
||||||
|
import { Provider as MTPProvider } from "@methanium/template-mtp";
|
||||||
|
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import { routeTree } from "./routeTree.gen";
|
||||||
|
import { mtpOptions } from "./mtp-options.ts";
|
||||||
|
|
||||||
|
const router = createRouter({ routeTree });
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ThemeProvider
|
||||||
|
defaultTheme="dark"
|
||||||
|
themes={BUILT_IN_THEMES}
|
||||||
|
defaultParentThemeId="methanium"
|
||||||
|
>
|
||||||
|
<MTPProvider options={mtpOptions}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</MTPProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
13
apps/web/src/mtp-options.ts
Normal file
13
apps/web/src/mtp-options.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { MTPClientOptions } from "mtp";
|
||||||
|
|
||||||
|
// Replace null with your application's stable MTP connection configuration.
|
||||||
|
export const mtpOptions: MTPClientOptions | null = null;
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// export const mtpOptions: MTPClientOptions = {
|
||||||
|
// url: "https://mtp.example.com:4433",
|
||||||
|
// credentials: { clientId: 1, keyring: new Uint8Array([...]) },
|
||||||
|
// hostPublicKey: "...",
|
||||||
|
// descriptor: "web",
|
||||||
|
// pings: true,
|
||||||
|
// };
|
||||||
59
apps/web/src/routeTree.gen.ts
Normal file
59
apps/web/src/routeTree.gen.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
|
||||||
|
const IndexRoute = IndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRouteImport
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
}
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths: '/'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to: '/'
|
||||||
|
id: '__root__' | '/'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
}
|
||||||
|
export const routeTree = rootRouteImport
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
17
apps/web/src/routes/__root.tsx
Normal file
17
apps/web/src/routes/__root.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Header } from "../components/layout/Header";
|
||||||
|
|
||||||
|
const EmptyPage = () => (
|
||||||
|
<main className="min-h-[calc(100vh-3rem)] bg-background" />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
component: () => (
|
||||||
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
|
<Header />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
notFoundComponent: EmptyPage,
|
||||||
|
});
|
||||||
7
apps/web/src/routes/index.tsx
Normal file
7
apps/web/src/routes/index.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({ component: Home });
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
return <main className="min-h-[calc(100vh-3rem)] bg-background" />;
|
||||||
|
}
|
||||||
2
apps/web/src/styles.css
Normal file
2
apps/web/src/styles.css
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "@methanium/ui/index.css";
|
||||||
10
apps/web/type-maps.yaml
Normal file
10
apps/web/type-maps.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
protocol_version: "1.0"
|
||||||
|
|
||||||
|
type_maps:
|
||||||
|
"1.0":
|
||||||
|
CommunicationTypes:
|
||||||
|
ExampleRequest: 32
|
||||||
|
ExampleResponse: 33
|
||||||
|
DataTypes:
|
||||||
|
Message: 32
|
||||||
|
ReceivedAt: 33
|
||||||
28
apps/web/vite.config.ts
Normal file
28
apps/web/vite.config.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { methaniumUi } from "@methanium/ui/vite";
|
||||||
|
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { mtp } from "mtp/vite";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { defineConfig, type Plugin } from "vite";
|
||||||
|
|
||||||
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
tanstackRouter({ target: "react", autoCodeSplitting: true }),
|
||||||
|
tailwindcss(),
|
||||||
|
react(),
|
||||||
|
methaniumUi({ defaultThemeId: "methanium" }),
|
||||||
|
mtp({ typeMaps: "./type-maps.yaml" }) as Plugin,
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@methanium/template-mtp": path.resolve(
|
||||||
|
root,
|
||||||
|
"../../packages/mtp/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
62
flake.lock
generated
Normal file
62
flake.lock
generated
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1785090369,
|
||||||
|
"narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "624af665418d3c65d544145b4d34ad696439570e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744536153,
|
||||||
|
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1785302874,
|
||||||
|
"narHash": "sha256-fpKEww3TJoo1ANHO2q918ei+ayOrp0YEQAO1DuBLOB4=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "b99d48435bc3e34309d2c7ae6f7d45e77a156c38",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
211
flake.nix
Normal file
211
flake.nix
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
{
|
||||||
|
description = "Methanium Template";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs@{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
rust-overlay,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
systems = [
|
||||||
|
"aarch64-darwin"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"x86_64-linux"
|
||||||
|
];
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||||
|
pkgsFor =
|
||||||
|
system:
|
||||||
|
import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ rust-overlay.overlays.default ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
root = toString ./.;
|
||||||
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
|
targets = [ "wasm32-unknown-unknown" ];
|
||||||
|
};
|
||||||
|
source = pkgs.lib.cleanSourceWith {
|
||||||
|
src = ./.;
|
||||||
|
filter =
|
||||||
|
path: _type:
|
||||||
|
let
|
||||||
|
relative = pkgs.lib.removePrefix "${root}/" (toString path);
|
||||||
|
in
|
||||||
|
!(pkgs.lib.hasPrefix "node_modules/" relative)
|
||||||
|
&& !(pkgs.lib.hasPrefix "apps/web/node_modules/" relative)
|
||||||
|
&& !(pkgs.lib.hasPrefix "apps/web/dist/" relative)
|
||||||
|
&& relative != "node_modules"
|
||||||
|
&& relative != "apps/web/node_modules"
|
||||||
|
&& relative != "apps/web/dist"
|
||||||
|
&& relative != "result";
|
||||||
|
};
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.cacert
|
||||||
|
pkgs.git
|
||||||
|
pkgs.nodejs
|
||||||
|
pkgs.pnpm
|
||||||
|
pkgs.wasm-pack
|
||||||
|
rustToolchain
|
||||||
|
];
|
||||||
|
webDeps = pkgs.stdenvNoCC.mkDerivation {
|
||||||
|
pname = "methanium-template-web-deps";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = source;
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.cacert
|
||||||
|
pkgs.git
|
||||||
|
pkgs.nodejs
|
||||||
|
pkgs.pnpm
|
||||||
|
pkgs.wasm-pack
|
||||||
|
rustToolchain
|
||||||
|
];
|
||||||
|
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHashMode = "recursive";
|
||||||
|
outputHash = "sha256-ihavd0hJDptdyEtjWjVgghwAl/rn6aUdQ9CT3B4if3E=";
|
||||||
|
dontFixup = true;
|
||||||
|
__structuredAttrs = true;
|
||||||
|
unsafeDiscardReferences.out = true;
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
export HOME="$TMPDIR/home"
|
||||||
|
export CARGO_HOME="$TMPDIR/cargo"
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
patchShebangs node_modules
|
||||||
|
pnpm build
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p "$out/apps/web"
|
||||||
|
cp -a node_modules "$out/node_modules"
|
||||||
|
cp -a apps/web/node_modules "$out/apps/web/node_modules"
|
||||||
|
cp -a "$CARGO_HOME" "$out/cargo"
|
||||||
|
cp -a "$HOME/.cache/.wasm-pack" "$out/wasm-pack"
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
web = pkgs.stdenvNoCC.mkDerivation {
|
||||||
|
pname = "methanium-template-web";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = source;
|
||||||
|
inherit nativeBuildInputs;
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
cp -a ${webDeps}/node_modules .
|
||||||
|
cp -a ${webDeps}/apps/web/node_modules apps/web/node_modules
|
||||||
|
cp -a ${webDeps}/cargo "$TMPDIR/cargo"
|
||||||
|
chmod -R u+w node_modules apps/web/node_modules "$TMPDIR/cargo"
|
||||||
|
export HOME="$TMPDIR/home"
|
||||||
|
export CARGO_HOME="$TMPDIR/cargo"
|
||||||
|
export CARGO_NET_OFFLINE=true
|
||||||
|
mkdir -p "$HOME/.cache"
|
||||||
|
cp -a ${webDeps}/wasm-pack "$HOME/.cache/.wasm-pack"
|
||||||
|
chmod -R u+w "$HOME/.cache/.wasm-pack"
|
||||||
|
pnpm build
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
cp -r apps/web/dist "$out"
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = web;
|
||||||
|
web = web;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
devShells = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
|
extensions = [ "rust-src" ];
|
||||||
|
targets = [ "wasm32-unknown-unknown" ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
pkgs.nodejs
|
||||||
|
pkgs.pnpm
|
||||||
|
pkgs.wasm-pack
|
||||||
|
rustToolchain
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
checks = forAllSystems (system: {
|
||||||
|
inherit (self.packages.${system}) web;
|
||||||
|
});
|
||||||
|
|
||||||
|
nixosModules.default =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.services.methanium-template;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.methanium-template = {
|
||||||
|
enable = lib.mkEnableOption "the Methanium frontend";
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
|
defaultText = lib.literalExpression "self.packages.${pkgs.stdenv.hostPlatform.system}.default";
|
||||||
|
description = "Static frontend package to serve.";
|
||||||
|
};
|
||||||
|
|
||||||
|
hostName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "localhost";
|
||||||
|
description = "nginx virtual host name.";
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to open TCP port 80 in the firewall.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts.${cfg.hostName} = {
|
||||||
|
root = cfg.package;
|
||||||
|
locations."/".tryFiles = "$uri $uri/ /index.html";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ 80 ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "methanium-template",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@11.17.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "pnpm --filter @methanium/template-web dev",
|
||||||
|
"build": "pnpm --filter @methanium/template-web build",
|
||||||
|
"check": "tsc -p tsconfig.json",
|
||||||
|
"lint": "oxlint apps/web/src packages/mtp",
|
||||||
|
"fmt": "prettier --write .",
|
||||||
|
"fmt:check": "prettier --check ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"oxlint": "^1.28.0",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"@types/node": "^24.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
357
packages/mtp/context.tsx
Normal file
357
packages/mtp/context.tsx
Normal file
|
|
@ -0,0 +1,357 @@
|
||||||
|
import { toast } from "@methanium/ui";
|
||||||
|
import {
|
||||||
|
ConnectionState,
|
||||||
|
MTPClient,
|
||||||
|
type MTPClientOptions,
|
||||||
|
type MTPRequestOptions,
|
||||||
|
} from "mtp";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type JSX,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import { RECONNECT_RESET, RECONNECT_TRIES, RETRY_INTERVAL } from "./values.ts";
|
||||||
|
|
||||||
|
export type ProtocolMessage = {
|
||||||
|
id?: number;
|
||||||
|
type: string;
|
||||||
|
data: unknown;
|
||||||
|
sender?: bigint;
|
||||||
|
receiver?: bigint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BoundSendFn = (
|
||||||
|
type: string,
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
options?: MTPRequestOptions,
|
||||||
|
) => Promise<ProtocolMessage>;
|
||||||
|
|
||||||
|
export type PushHandler = (message: ProtocolMessage) => void;
|
||||||
|
|
||||||
|
export type MTPExchange = {
|
||||||
|
type: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
response: ProtocolMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MTPInterceptor = (exchange: MTPExchange) => void | Promise<void>;
|
||||||
|
|
||||||
|
type Subscription = {
|
||||||
|
type: string;
|
||||||
|
handler: PushHandler;
|
||||||
|
unsubscribe?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ClientWaiter = {
|
||||||
|
resolve: (client: MTPClient) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContextType = {
|
||||||
|
send: BoundSendFn;
|
||||||
|
subscribe: (type: string, handler: PushHandler) => () => void;
|
||||||
|
addInterceptor: (interceptor: MTPInterceptor) => () => void;
|
||||||
|
reconnect: () => void;
|
||||||
|
readyState: number;
|
||||||
|
connected: boolean;
|
||||||
|
authenticated: boolean;
|
||||||
|
contextReady: boolean;
|
||||||
|
loadingDescription: string;
|
||||||
|
error: Error | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MTPContext = createContext<ContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export type MTPProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
options: MTPClientOptions | null;
|
||||||
|
authenticate?: boolean;
|
||||||
|
blockConnection?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Provider({
|
||||||
|
children,
|
||||||
|
options,
|
||||||
|
authenticate = true,
|
||||||
|
blockConnection = false,
|
||||||
|
}: MTPProviderProps): JSX.Element {
|
||||||
|
const [readyState, setReadyState] = useState<number>(
|
||||||
|
ConnectionState.Disconnected,
|
||||||
|
);
|
||||||
|
const [authenticated, setAuthenticated] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [reconnectGeneration, setReconnectGeneration] = useState(0);
|
||||||
|
const clientRef = useRef<MTPClient | null>(null);
|
||||||
|
const readyRef = useRef(false);
|
||||||
|
const waitersRef = useRef(new Set<ClientWaiter>());
|
||||||
|
const subscriptionsRef = useRef(new Set<Subscription>());
|
||||||
|
const interceptorsRef = useRef(new Set<MTPInterceptor>());
|
||||||
|
|
||||||
|
const unbindSubscriptions = useCallback(() => {
|
||||||
|
for (const subscription of subscriptionsRef.current) {
|
||||||
|
subscription.unsubscribe?.();
|
||||||
|
subscription.unsubscribe = undefined;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const bindSubscriptions = useCallback((client: MTPClient) => {
|
||||||
|
for (const subscription of subscriptionsRef.current) {
|
||||||
|
subscription.unsubscribe?.();
|
||||||
|
subscription.unsubscribe = client.subscribe(
|
||||||
|
subscription.type,
|
||||||
|
(message) => subscription.handler(message as ProtocolMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!options || blockConnection) {
|
||||||
|
readyRef.current = false;
|
||||||
|
setReadyState(ConnectionState.Disconnected);
|
||||||
|
setAuthenticated(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const connectionOptions = options;
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let reconnectResetTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let attempts = 0;
|
||||||
|
let connectionGeneration = 0;
|
||||||
|
|
||||||
|
const disconnectClient = () => {
|
||||||
|
readyRef.current = false;
|
||||||
|
unbindSubscriptions();
|
||||||
|
const activeClient = clientRef.current;
|
||||||
|
clientRef.current = null;
|
||||||
|
activeClient?.disconnect();
|
||||||
|
setReadyState(ConnectionState.Disconnected);
|
||||||
|
setAuthenticated(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleReconnect = (connectionError: Error) => {
|
||||||
|
if (disposed || reconnectTimer) return;
|
||||||
|
setError(connectionError);
|
||||||
|
|
||||||
|
if (attempts >= RECONNECT_TRIES) {
|
||||||
|
toast.error("MTP connection failed", {
|
||||||
|
id: "mtp-connection",
|
||||||
|
description: connectionError.message,
|
||||||
|
duration: Infinity,
|
||||||
|
closeButton: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts += 1;
|
||||||
|
toast.loading(`Reconnecting to MTP (${attempts}/${RECONNECT_TRIES})`, {
|
||||||
|
id: "mtp-connection",
|
||||||
|
});
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
reconnectTimer = null;
|
||||||
|
void connect();
|
||||||
|
}, RETRY_INTERVAL);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
if (disposed) return;
|
||||||
|
|
||||||
|
const generation = ++connectionGeneration;
|
||||||
|
let client: MTPClient | null = null;
|
||||||
|
try {
|
||||||
|
disconnectClient();
|
||||||
|
setError(null);
|
||||||
|
setReadyState(ConnectionState.Connecting);
|
||||||
|
|
||||||
|
await MTPClient.init(connectionOptions.wasm);
|
||||||
|
client = await MTPClient.create({
|
||||||
|
...connectionOptions,
|
||||||
|
logger: (event) => {
|
||||||
|
connectionOptions.logger?.(event);
|
||||||
|
if (disposed || generation !== connectionGeneration) return;
|
||||||
|
if ((event as { type: string }).type !== "state") return;
|
||||||
|
|
||||||
|
const nextState = client?.state ?? ConnectionState.Disconnected;
|
||||||
|
setReadyState(nextState);
|
||||||
|
if (
|
||||||
|
client !== null &&
|
||||||
|
nextState === ConnectionState.Disconnected &&
|
||||||
|
clientRef.current === client
|
||||||
|
) {
|
||||||
|
disconnectClient();
|
||||||
|
scheduleReconnect(new Error("MTP connection lost"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (disposed || generation !== connectionGeneration) {
|
||||||
|
client.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clientRef.current = client;
|
||||||
|
setReadyState(client.state);
|
||||||
|
|
||||||
|
if (authenticate) {
|
||||||
|
if (!client.credentials || client.credentials.clientId === null) {
|
||||||
|
throw new Error("MTP credentials are required for authentication");
|
||||||
|
}
|
||||||
|
await client.auth();
|
||||||
|
} else {
|
||||||
|
await client.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disposed || clientRef.current !== client) return;
|
||||||
|
|
||||||
|
readyRef.current = true;
|
||||||
|
setReadyState(client.state);
|
||||||
|
setAuthenticated(true);
|
||||||
|
setError(null);
|
||||||
|
bindSubscriptions(client);
|
||||||
|
for (const waiter of waitersRef.current) waiter.resolve(client);
|
||||||
|
waitersRef.current.clear();
|
||||||
|
toast.dismiss("mtp-connection");
|
||||||
|
|
||||||
|
if (reconnectResetTimer) clearTimeout(reconnectResetTimer);
|
||||||
|
reconnectResetTimer = setTimeout(() => {
|
||||||
|
attempts = 0;
|
||||||
|
reconnectResetTimer = null;
|
||||||
|
}, RECONNECT_RESET * 1_000);
|
||||||
|
} catch (cause) {
|
||||||
|
if (disposed || generation !== connectionGeneration) {
|
||||||
|
client?.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectClient();
|
||||||
|
scheduleReconnect(
|
||||||
|
cause instanceof Error ? cause : new Error(String(cause)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void connect();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
connectionGeneration += 1;
|
||||||
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||||
|
if (reconnectResetTimer) clearTimeout(reconnectResetTimer);
|
||||||
|
disconnectClient();
|
||||||
|
toast.dismiss("mtp-connection");
|
||||||
|
|
||||||
|
const cancellation = new Error("MTP provider was disconnected");
|
||||||
|
for (const waiter of waitersRef.current) waiter.reject(cancellation);
|
||||||
|
waitersRef.current.clear();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
authenticate,
|
||||||
|
bindSubscriptions,
|
||||||
|
blockConnection,
|
||||||
|
options,
|
||||||
|
reconnectGeneration,
|
||||||
|
unbindSubscriptions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getClient = useCallback(() => {
|
||||||
|
if (readyRef.current && clientRef.current) {
|
||||||
|
return Promise.resolve(clientRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options || blockConnection) {
|
||||||
|
return Promise.reject(new Error("MTP connection is not configured"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<MTPClient>((resolve, reject) => {
|
||||||
|
waitersRef.current.add({ resolve, reject });
|
||||||
|
});
|
||||||
|
}, [blockConnection, options]);
|
||||||
|
|
||||||
|
const send = useCallback<BoundSendFn>(
|
||||||
|
async (type, data = {}, requestOptions) => {
|
||||||
|
const client = await getClient();
|
||||||
|
const response = await client.request(type, data, requestOptions);
|
||||||
|
const message = response as ProtocolMessage;
|
||||||
|
|
||||||
|
for (const interceptor of interceptorsRef.current) {
|
||||||
|
void Promise.resolve(
|
||||||
|
interceptor({ type, data, response: message }),
|
||||||
|
).catch((interceptorError) => {
|
||||||
|
console.error("MTP interceptor failed", interceptorError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
[getClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
const subscribe = useCallback((type: string, handler: PushHandler) => {
|
||||||
|
const subscription: Subscription = { type, handler };
|
||||||
|
subscriptionsRef.current.add(subscription);
|
||||||
|
|
||||||
|
if (readyRef.current && clientRef.current) {
|
||||||
|
subscription.unsubscribe = clientRef.current.subscribe(type, (message) =>
|
||||||
|
handler(message as ProtocolMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.unsubscribe?.();
|
||||||
|
subscriptionsRef.current.delete(subscription);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addInterceptor = useCallback((interceptor: MTPInterceptor) => {
|
||||||
|
interceptorsRef.current.add(interceptor);
|
||||||
|
return () => interceptorsRef.current.delete(interceptor);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reconnect = useCallback(() => {
|
||||||
|
setReconnectGeneration((generation) => generation + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const connected = readyState === ConnectionState.Connected;
|
||||||
|
const contextReady = connected && authenticated;
|
||||||
|
const loadingDescription = useMemo(() => {
|
||||||
|
if (!options) return "Waiting for connection configuration";
|
||||||
|
if (error) return error.message;
|
||||||
|
if (readyState === ConnectionState.Connecting || !connected) {
|
||||||
|
return "Establishing transport channel";
|
||||||
|
}
|
||||||
|
if (!authenticated) return "Waiting for authenticated session";
|
||||||
|
return "Connected";
|
||||||
|
}, [authenticated, connected, error, options, readyState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MTPContext.Provider
|
||||||
|
value={{
|
||||||
|
send,
|
||||||
|
subscribe,
|
||||||
|
addInterceptor,
|
||||||
|
reconnect,
|
||||||
|
readyState,
|
||||||
|
connected,
|
||||||
|
authenticated,
|
||||||
|
contextReady,
|
||||||
|
loadingDescription,
|
||||||
|
error,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MTPContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMTP(): ContextType {
|
||||||
|
const context = useContext(MTPContext);
|
||||||
|
if (!context) throw new Error("useMTP must be used within an MTP Provider");
|
||||||
|
return context;
|
||||||
|
}
|
||||||
10
packages/mtp/index.ts
Normal file
10
packages/mtp/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export { Provider, useMTP } from "./context.tsx";
|
||||||
|
export type {
|
||||||
|
BoundSendFn,
|
||||||
|
MTPExchange,
|
||||||
|
MTPInterceptor,
|
||||||
|
MTPProviderProps,
|
||||||
|
ProtocolMessage,
|
||||||
|
PushHandler,
|
||||||
|
} from "./context.tsx";
|
||||||
|
export { RECONNECT_RESET, RECONNECT_TRIES, RETRY_INTERVAL } from "./values.ts";
|
||||||
18
packages/mtp/package.json
Normal file
18
packages/mtp/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "@methanium/template-mtp",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@methanium/ui": "*",
|
||||||
|
"mtp": "*",
|
||||||
|
"react": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@methanium/ui": "*",
|
||||||
|
"mtp": "*",
|
||||||
|
"react": "^19.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/mtp/values.ts
Normal file
3
packages/mtp/values.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const RETRY_INTERVAL = 3_000;
|
||||||
|
export const RECONNECT_TRIES = 3;
|
||||||
|
export const RECONNECT_RESET = 6;
|
||||||
6759
pnpm-lock.yaml
generated
Normal file
6759
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
12
pnpm-workspace.yaml
Normal file
12
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
packages:
|
||||||
|
- apps/*
|
||||||
|
- packages/*
|
||||||
|
|
||||||
|
nodeLinker: hoisted
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
"@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.12/methanium-ui.tgz"
|
||||||
|
mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz"
|
||||||
|
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"noEmit": true,
|
||||||
|
"paths": {
|
||||||
|
"@methanium/template-mtp": ["./packages/mtp/index.ts"],
|
||||||
|
"mtp/raw": ["./apps/web/node_modules/.vite/mtp/mtp_wasm.d.ts"],
|
||||||
|
"mtp/type-map": ["./apps/web/node_modules/.vite/mtp/mtp_type_map.d.ts"]
|
||||||
|
},
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"types": ["node"],
|
||||||
|
"useDefineForClassFields": true
|
||||||
|
},
|
||||||
|
"include": ["apps/web/src", "apps/web/vite.config.ts", "packages/mtp"]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue