mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 07:10:19 +00:00
add astro fuma docs for website
Signed-off-by: Arvindh <arvindh91@gmail.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# build outputs
|
||||
/dist
|
||||
/.astro
|
||||
/coverage
|
||||
*.tsbuildinfo
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
/.pnp
|
||||
.pnp.js
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env
|
||||
.env*.local
|
||||
.wrangler
|
||||
@@ -0,0 +1,47 @@
|
||||
# FluxMQ Web (Astro + Starlight)
|
||||
|
||||
This `web` folder is now an Astro static site with:
|
||||
|
||||
- Product landing page at `/`
|
||||
- Documentation powered by Starlight at `/docs/`
|
||||
- Built-in docs search (`Ctrl+K`) via Starlight + Pagefind
|
||||
- Tailwind CSS v4 for styling
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open `http://localhost:4321`.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
- `src/pages/index.astro`: product landing page
|
||||
- `src/components/home/*.astro`: landing page components (Astro-only, no React)
|
||||
- `src/content/docs/docs/*`: docs content shown under `/docs/*`
|
||||
- `src/styles/global.css`: Tailwind entry and shared brand/component styles
|
||||
- `src/styles/starlight.css`: docs-theme overrides for Starlight
|
||||
- `astro.config.mjs`: Astro, Tailwind, Starlight, and sitemap config
|
||||
@@ -0,0 +1,38 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
import react from '@astrojs/react';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import {
|
||||
rehypeCode,
|
||||
remarkCodeTab,
|
||||
remarkHeading,
|
||||
remarkNpm,
|
||||
remarkStructure,
|
||||
} from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
const site = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://fluxmq.absmach.eu';
|
||||
|
||||
export default defineConfig({
|
||||
site,
|
||||
base: process.env.FQ_BASE_PATH ?? '/',
|
||||
output: 'static',
|
||||
integrations: [
|
||||
sitemap(),
|
||||
react(),
|
||||
mdx({
|
||||
extendMarkdownConfig: false,
|
||||
syntaxHighlight: false,
|
||||
remarkPlugins: [
|
||||
remarkHeading,
|
||||
remarkCodeTab,
|
||||
remarkNpm,
|
||||
[remarkStructure, { exportAs: 'structuredData' }],
|
||||
],
|
||||
rehypePlugins: [rehypeCode],
|
||||
}),
|
||||
],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
Generated
+10010
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"@astrojs/react": "^4.4.2",
|
||||
"@orama/orama": "^3.1.18",
|
||||
"@astrojs/sitemap": "^3.0.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"astro": "^5.5.0",
|
||||
"fumadocs-core": "^16.5.0",
|
||||
"fumadocs-ui": "^16.5.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,48 @@
|
||||
import { useEffect } from 'react';
|
||||
import { FrameworkProvider } from 'fumadocs-core/framework';
|
||||
import { useSearchContext } from 'fumadocs-ui/contexts/search';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/base';
|
||||
import SearchDialog from '@/components/search';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__fluxmqOpenSearch?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
function HomeSearchBridgeInner() {
|
||||
const { setOpenSearch } = useSearchContext();
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => setOpenSearch(true);
|
||||
window.__fluxmqOpenSearch = handler;
|
||||
window.addEventListener('fluxmq:open-search', handler);
|
||||
return () => {
|
||||
delete window.__fluxmqOpenSearch;
|
||||
window.removeEventListener('fluxmq:open-search', handler);
|
||||
};
|
||||
}, [setOpenSearch]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function HomeSearchBridge() {
|
||||
return (
|
||||
<FrameworkProvider
|
||||
usePathname={() => window.location.pathname}
|
||||
useParams={() => ({})}
|
||||
useRouter={() => ({
|
||||
push(nextPathname) {
|
||||
window.location.assign(nextPathname);
|
||||
},
|
||||
refresh() {
|
||||
window.location.reload();
|
||||
},
|
||||
})}
|
||||
>
|
||||
<RootProvider search={{ SearchDialog }}>
|
||||
<HomeSearchBridgeInner />
|
||||
</RootProvider>
|
||||
</FrameworkProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { DocsPage, type DocsPageProps } from 'fumadocs-ui/layouts/docs/page';
|
||||
import type { Root } from 'fumadocs-core/page-tree';
|
||||
import type { ReactNode } from 'react';
|
||||
import { FrameworkProvider } from 'fumadocs-core/framework';
|
||||
import { navigate } from 'astro:transitions/client';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/base';
|
||||
import SearchDialog from './search';
|
||||
|
||||
export function Docs({
|
||||
tree,
|
||||
children,
|
||||
pathname,
|
||||
params,
|
||||
page,
|
||||
}: {
|
||||
tree: Root;
|
||||
children: ReactNode;
|
||||
pathname: string;
|
||||
params: Record<string, string | string[]>;
|
||||
page?: DocsPageProps;
|
||||
}) {
|
||||
return (
|
||||
<FrameworkProvider
|
||||
usePathname={() => pathname}
|
||||
useParams={() => params}
|
||||
useRouter={() => ({
|
||||
push(nextPathname) {
|
||||
void navigate(nextPathname);
|
||||
},
|
||||
refresh() {
|
||||
window.location.reload();
|
||||
},
|
||||
})}
|
||||
>
|
||||
<RootProvider search={{ SearchDialog }}>
|
||||
<DocsLayout
|
||||
tree={tree}
|
||||
nav={{
|
||||
title: (
|
||||
<span style={{ fontWeight: 800, lineHeight: 1.1, fontSize: '1.9rem' }}>
|
||||
<span style={{ color: 'var(--flux-blue)' }}>Flux</span>
|
||||
<span style={{ color: 'var(--flux-orange)' }}>MQ</span>
|
||||
</span>
|
||||
),
|
||||
url: '/',
|
||||
}}
|
||||
links={[
|
||||
{
|
||||
type: 'icon',
|
||||
label: 'github',
|
||||
text: 'Github',
|
||||
url: 'https://github.com/absmach/fluxmq',
|
||||
external: true,
|
||||
icon: (
|
||||
<svg role="img" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DocsPage {...page}>{children}</DocsPage>
|
||||
</DocsLayout>
|
||||
</RootProvider>
|
||||
</FrameworkProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<section id="architecture" class="border-b-2 border-theme bg-theme-alt py-20">
|
||||
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
|
||||
<h2 class="mb-12 text-4xl font-bold md:text-5xl">
|
||||
<span class="border-l-4 border-[var(--flux-orange)] pl-4">ARCHITECTURE</span>
|
||||
</h2>
|
||||
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="brutalist-border bg-theme overflow-x-auto p-6">
|
||||
<svg viewBox="0 0 880 370" class="min-w-[760px]" role="img" aria-label="FluxMQ architecture diagram">
|
||||
<g font-family="JetBrains Mono, ui-monospace, monospace" font-size="13" font-weight="700">
|
||||
<rect x="335" y="20" width="210" height="48" rx="8" fill="#2f69b3" stroke="#1f1f1f" stroke-width="2"></rect>
|
||||
<text x="440" y="49" text-anchor="middle" fill="#fff">Setup and configuration</text>
|
||||
|
||||
<rect x="70" y="110" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
|
||||
<rect x="330" y="110" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
|
||||
<rect x="590" y="110" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
|
||||
<text x="180" y="142" text-anchor="middle" fill="#101010">TCP/WS/HTTP/CoAP Servers</text>
|
||||
<text x="440" y="142" text-anchor="middle" fill="#101010">AMQP 1.0 Server</text>
|
||||
<text x="700" y="142" text-anchor="middle" fill="#101010">AMQP 0.9.1 Server</text>
|
||||
|
||||
<rect x="70" y="200" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
|
||||
<rect x="330" y="200" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
|
||||
<rect x="590" y="200" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
|
||||
<text x="180" y="232" text-anchor="middle" fill="#101010">MQTT Broker</text>
|
||||
<text x="440" y="232" text-anchor="middle" fill="#101010">AMQP Broker 1.0</text>
|
||||
<text x="700" y="232" text-anchor="middle" fill="#101010">AMQP Broker 0.9.1</text>
|
||||
|
||||
<rect x="315" y="290" width="250" height="52" rx="8" fill="#f9a32a" stroke="#1f1f1f" stroke-width="2"></rect>
|
||||
<text x="440" y="322" text-anchor="middle" fill="#101010">Queue Manager (Bindings + Delivery)</text>
|
||||
|
||||
<path d="M440 70 L440 104" stroke="#2f69b3" stroke-width="2"></path>
|
||||
<path d="M180 162 L180 194M440 162 L440 194M700 162 L700 194" stroke="#2f69b3" stroke-width="2"></path>
|
||||
<path d="M180 252 L180 280 L315 280M440 252 L440 290M700 252 L700 280 L565 280" stroke="#2f69b3" stroke-width="2"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brutalist-border bg-theme mx-auto mt-12 max-w-3xl p-6">
|
||||
<h3 class="mono mb-4 text-lg font-bold">KEY COMPONENTS</h3>
|
||||
<ul class="space-y-3 text-base text-theme-muted">
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-blue)]">▸</span><span><strong>Transport Layer:</strong> Multi-protocol servers (MQTT, AMQP 1.0, AMQP 0.9.1, CoAP, HTTP, WebSocket)</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-blue)]">▸</span><span><strong>Protocol Brokers:</strong> FSM-based protocol handlers with zero-copy parsing</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-blue)]">▸</span><span><strong>Queue Manager:</strong> Durable queue bindings with FIFO, priority, and topic-based delivery</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-blue)]">▸</span><span><strong>Storage:</strong> Persistence for messages and topic indexing</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
interface Props {
|
||||
code: string;
|
||||
}
|
||||
|
||||
const { code } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="code-panel">
|
||||
<button
|
||||
class="absolute top-2 right-2 border-2 border-[#3c3c3c] bg-[#191b1d] px-2 py-1 text-xs font-semibold text-white transition hover:border-[var(--flux-orange)] hover:bg-[var(--flux-orange)] hover:text-black"
|
||||
type="button"
|
||||
data-copy-btn
|
||||
aria-label="Copy command"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<pre><code>{code}</code></pre>
|
||||
</div>
|
||||
@@ -0,0 +1,90 @@
|
||||
<section id="features" class="border-b-2 border-[var(--flux-border)] py-20">
|
||||
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
|
||||
<h2 class="mb-12 text-4xl font-bold md:text-5xl">
|
||||
<span class="border-l-4 border-[var(--flux-orange)] pl-4">FEATURES</span>
|
||||
</h2>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<article class="brutalist-card accent-line p-6 pl-8">
|
||||
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-blue)] text-[var(--flux-blue)]">
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 8h16M4 16h16M8 4v16M16 4v16"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">Multi-Protocol Support</h3>
|
||||
<p class="text-base leading-relaxed text-theme-muted">
|
||||
Full MQTT 3.1.1 and 5.0 over TCP and WebSocket, plus HTTP-MQTT and CoAP bridges. All protocols share the
|
||||
same broker core and messages flow seamlessly across transports.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="brutalist-card accent-line p-6 pl-8">
|
||||
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<ellipse cx="12" cy="6" rx="7" ry="3"></ellipse>
|
||||
<path d="M5 6v6c0 1.7 3.1 3 7 3s7-1.3 7-3V6"></path>
|
||||
<path d="M5 12v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">Durable Queues</h3>
|
||||
<p class="text-base leading-relaxed text-theme-muted">
|
||||
Persistent message queues with consumer groups, ack/nack/reject semantics, dead-letter queues, and retention
|
||||
policies. Raft-based replication with automatic failover.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="brutalist-card accent-line p-6 pl-8">
|
||||
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-blue)] text-[var(--flux-blue)]">
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="6"></rect>
|
||||
<rect x="3" y="14" width="18" height="6"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">Clustering & High Availability</h3>
|
||||
<p class="text-base leading-relaxed text-theme-muted">
|
||||
Embedded etcd for coordination, gRPC-based inter-broker communication with mTLS, automatic session
|
||||
ownership, and graceful shutdown with session transfer.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="brutalist-card accent-line p-6 pl-8">
|
||||
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2L4 14h7l-1 8 10-13h-7l0-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">Performance Optimized</h3>
|
||||
<p class="text-base leading-relaxed text-theme-muted">
|
||||
Zero-copy packet parsing, object pooling, efficient trie-based topic matching, and direct instrumentation.
|
||||
300K-500K msg/s per node with sub-10ms latency.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="brutalist-card accent-line p-6 pl-8">
|
||||
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-blue)] text-[var(--flux-blue)]">
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 3l7 3v6c0 5-3.5 7.5-7 9-3.5-1.5-7-4-7-9V6l7-3z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">Security</h3>
|
||||
<p class="text-base leading-relaxed text-theme-muted">
|
||||
TLS/mTLS for client connections, mTLS for inter-broker gRPC, DTLS options for CoAP, WebSocket origin
|
||||
validation, and per-IP/per-client rate limiting.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="brutalist-card accent-line p-6 pl-8">
|
||||
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M16 18l6-6-6-6M8 6l-6 6 6 6"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">Open-Source & Extensible</h3>
|
||||
<p class="text-base leading-relaxed text-theme-muted">
|
||||
Licensed under Apache 2.0 with a clean layered architecture. Pluggable storage backends, protocol-agnostic
|
||||
domain logic, and easy extensibility.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
<footer class="bg-[var(--flux-bg-alt)] py-14">
|
||||
<div class="mx-auto grid w-[min(100%-2.5rem,1200px)] gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
<section>
|
||||
<h3 class="mb-3 text-lg font-bold uppercase">About</h3>
|
||||
<p class="text-[var(--flux-muted)]">
|
||||
FluxMQ is developed by Abstract Machines, an IoT infrastructure and security company located in Paris.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="mb-3 text-lg font-bold uppercase">Products</h3>
|
||||
<ul class="space-y-2 text-[var(--flux-muted)]">
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="https://magistrala.absmach.eu" target="_blank" rel="noopener noreferrer">Magistrala</a></li>
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="https://absmach.eu/supermq/" target="_blank" rel="noopener noreferrer">SuperMQ</a></li>
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="https://absmach.eu/propeller/" target="_blank" rel="noopener noreferrer">Propeller</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="mb-3 text-lg font-bold uppercase">Resources</h3>
|
||||
<ul class="space-y-2 text-[var(--flux-muted)]">
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="/docs/">Documentation</a></li>
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="https://github.com/absmach/fluxmq" target="_blank" rel="noopener noreferrer">GitHub</a></li>
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="https://absmach.eu/blog/" target="_blank" rel="noopener noreferrer">Blog</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="mb-3 text-lg font-bold uppercase">Contact</h3>
|
||||
<ul class="space-y-2 text-[var(--flux-muted)]">
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="mailto:info@absmach.eu">info@absmach.eu</a></li>
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="https://github.com/absmach" target="_blank" rel="noopener noreferrer">GitHub</a></li>
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="https://twitter.com/absmach" target="_blank" rel="noopener noreferrer">Twitter</a></li>
|
||||
<li><a class="hover:text-[var(--flux-orange)]" href="https://www.linkedin.com/company/abstract-machines" target="_blank" rel="noopener noreferrer">LinkedIn</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto mt-8 w-[min(100%-2.5rem,1200px)] border-t-2 border-[var(--flux-border)] pt-4 text-center text-sm text-[var(--flux-muted)]">
|
||||
© 2026 Abstract Machines. Licensed under Apache 2.0.
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,94 @@
|
||||
<div class="h-full w-full" aria-hidden="true">
|
||||
<svg viewBox="0 0 760 560" class="h-full w-full" role="img">
|
||||
<defs>
|
||||
<marker id="arrow-blue" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L6,3 z" fill="#2f69b3" />
|
||||
</marker>
|
||||
<marker id="arrow-green" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L6,3 z" fill="#2a9c4a" />
|
||||
</marker>
|
||||
<marker id="arrow-orange" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L6,3 z" fill="#f9a32a" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect
|
||||
x="260"
|
||||
y="60"
|
||||
width="220"
|
||||
height="440"
|
||||
rx="10"
|
||||
fill="none"
|
||||
stroke="#f9a32a"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="7 8"
|
||||
opacity="0.8"
|
||||
/>
|
||||
|
||||
<g class="network-dash" style="color:#2f69b3">
|
||||
<line x1="130" y1="110" x2="280" y2="110" marker-end="url(#arrow-blue)" />
|
||||
<line x1="130" y1="230" x2="280" y2="260" marker-end="url(#arrow-blue)" />
|
||||
<line x1="130" y1="350" x2="280" y2="260" marker-end="url(#arrow-blue)" />
|
||||
<line x1="130" y1="470" x2="280" y2="440" marker-end="url(#arrow-blue)" />
|
||||
<line x1="130" y1="110" x2="280" y2="440" marker-end="url(#arrow-blue)" />
|
||||
<line x1="130" y1="230" x2="280" y2="440" marker-end="url(#arrow-blue)" />
|
||||
</g>
|
||||
|
||||
<g class="network-dash" style="color:#f9a32a">
|
||||
<line x1="370" y1="160" x2="370" y2="240" marker-end="url(#arrow-orange)" />
|
||||
<line x1="370" y1="340" x2="370" y2="420" marker-end="url(#arrow-orange)" />
|
||||
<line x1="418" y1="240" x2="418" y2="160" marker-end="url(#arrow-orange)" />
|
||||
<line x1="418" y1="420" x2="418" y2="340" marker-end="url(#arrow-orange)" />
|
||||
</g>
|
||||
|
||||
<g class="network-dash" style="color:#2a9c4a">
|
||||
<line x1="460" y1="110" x2="620" y2="140" marker-end="url(#arrow-green)" />
|
||||
<line x1="460" y1="260" x2="620" y2="280" marker-end="url(#arrow-green)" />
|
||||
<line x1="460" y1="430" x2="620" y2="420" marker-end="url(#arrow-green)" />
|
||||
<line x1="460" y1="110" x2="620" y2="420" marker-end="url(#arrow-green)" />
|
||||
<line x1="460" y1="260" x2="620" y2="140" marker-end="url(#arrow-green)" />
|
||||
</g>
|
||||
|
||||
<g font-family="JetBrains Mono, ui-monospace, monospace" font-size="14" font-weight="700">
|
||||
<rect x="20" y="84" width="112" height="44" rx="8" fill="rgba(47, 105, 179, 0.07)" stroke="#2f69b3" stroke-width="1.3" />
|
||||
<rect x="20" y="204" width="112" height="44" rx="8" fill="rgba(47, 105, 179, 0.07)" stroke="#2f69b3" stroke-width="1.3" />
|
||||
<rect x="20" y="324" width="112" height="44" rx="8" fill="rgba(47, 105, 179, 0.07)" stroke="#2f69b3" stroke-width="1.3" />
|
||||
<rect x="20" y="444" width="112" height="44" rx="8" fill="rgba(47, 105, 179, 0.07)" stroke="#2f69b3" stroke-width="1.3" />
|
||||
<text x="76" y="112" text-anchor="middle" fill="#2f69b3">MQTT</text>
|
||||
<text x="76" y="232" text-anchor="middle" fill="#2f69b3">HTTP</text>
|
||||
<text x="76" y="352" text-anchor="middle" fill="#2f69b3">WebSocket</text>
|
||||
<text x="76" y="472" text-anchor="middle" fill="#2f69b3">AMQP</text>
|
||||
|
||||
<g>
|
||||
<rect x="280" y="80" width="180" height="76" rx="12" fill="rgba(249,163,42,0.11)" stroke="#f9a32a" stroke-width="1.3" />
|
||||
<rect x="298" y="96" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
|
||||
<rect x="298" y="124" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
|
||||
<text x="370" y="112" text-anchor="middle" fill="#2f69b3">FluxMQ</text>
|
||||
<text x="370" y="140" text-anchor="middle" fill="#f9a32a">Durable Queue</text>
|
||||
</g>
|
||||
|
||||
<g>
|
||||
<rect x="280" y="230" width="180" height="76" rx="12" fill="rgba(249,163,42,0.11)" stroke="#f9a32a" stroke-width="1.3" />
|
||||
<rect x="298" y="246" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
|
||||
<rect x="298" y="274" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
|
||||
<text x="370" y="262" text-anchor="middle" fill="#2f69b3">FluxMQ</text>
|
||||
<text x="370" y="290" text-anchor="middle" fill="#f9a32a">Durable Queue</text>
|
||||
</g>
|
||||
|
||||
<g>
|
||||
<rect x="280" y="400" width="180" height="76" rx="12" fill="rgba(249,163,42,0.11)" stroke="#f9a32a" stroke-width="1.3" />
|
||||
<rect x="298" y="416" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
|
||||
<rect x="298" y="444" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
|
||||
<text x="370" y="432" text-anchor="middle" fill="#2f69b3">FluxMQ</text>
|
||||
<text x="370" y="460" text-anchor="middle" fill="#f9a32a">Durable Queue</text>
|
||||
</g>
|
||||
|
||||
<rect x="620" y="118" width="112" height="44" rx="8" fill="rgba(42, 156, 74, 0.09)" stroke="#2a9c4a" stroke-width="1.3" />
|
||||
<rect x="620" y="258" width="112" height="44" rx="8" fill="rgba(42, 156, 74, 0.09)" stroke="#2a9c4a" stroke-width="1.3" />
|
||||
<rect x="620" y="398" width="112" height="44" rx="8" fill="rgba(42, 156, 74, 0.09)" stroke="#2a9c4a" stroke-width="1.3" />
|
||||
<text x="676" y="146" text-anchor="middle" fill="#2a9c4a">Analytics</text>
|
||||
<text x="676" y="286" text-anchor="middle" fill="#2a9c4a">Applications</text>
|
||||
<text x="676" y="426" text-anchor="middle" fill="#2a9c4a">Services</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
import HeroNetwork from './HeroNetwork.astro';
|
||||
---
|
||||
|
||||
<section class="relative h-[90vh] border-b-2 border-[var(--flux-border)] py-20 md:py-20">
|
||||
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
|
||||
<div class="grid items-center gap-12 md:grid-cols-2">
|
||||
<div class="max-w-2xl">
|
||||
<div class="mb-14">
|
||||
<h1 class="animate-fade-in mb-4 text-6xl font-bold md:text-8xl" style="line-height:1.1">
|
||||
<span class="text-[var(--flux-blue)]">Flux</span><span class="text-[var(--flux-orange)]">MQ</span>
|
||||
</h1>
|
||||
<div class="mono animate-slide-up mb-6 border-l-4 border-[var(--flux-orange)] pl-4 text-base text-theme-muted md:text-xl">
|
||||
<div>open-source | high-throughput | low-latency | scalable</div>
|
||||
<div>persistent storage | replication | MQTT v3/v5 | AMQP</div>
|
||||
<div>security | extensibility</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="animate-fade-in mb-14 max-w-3xl text-xl leading-relaxed md:text-2xl">
|
||||
A high-performance, open source, multi-protocol message broker written in Go for scalable IoT and
|
||||
event-driven architectures. Single binary. No external dependencies.
|
||||
</p>
|
||||
|
||||
<div class="animate-slide-up flex flex-wrap gap-4">
|
||||
<a
|
||||
class="brutalist-border inline-block px-6 py-3 font-bold transition-colors hover:bg-[var(--flux-blue)] hover:text-white"
|
||||
href="https://github.com/absmach/fluxmq"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
VIEW ON GITHUB →
|
||||
</a>
|
||||
<a
|
||||
class="brutalist-border inline-block px-6 py-3 font-bold transition-colors hover:bg-[var(--flux-orange)] hover:text-white"
|
||||
href="/docs/"
|
||||
>
|
||||
READ DOCUMENTATION
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden h-[80vh] items-center justify-center md:flex">
|
||||
<HeroNetwork />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,37 @@
|
||||
<section class="border-b-2 border-[var(--flux-border)] bg-[var(--flux-bg-alt)] py-20">
|
||||
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
|
||||
<div class="mx-auto max-w-[700px] text-center">
|
||||
<h2 class="mb-2 text-3xl font-bold uppercase md:text-4xl">Subscribe to Newsletter</h2>
|
||||
<p class="text-[var(--flux-muted)]">Stay updated with the latest FluxMQ news, updates, and announcements.</p>
|
||||
|
||||
<form
|
||||
action="https://absmach.us11.list-manage.com/subscribe/post?u=70b43c7181d005024187bfb31&id=0a319b6b63&f_id=00d816e1f0"
|
||||
method="post"
|
||||
target="_blank"
|
||||
class="mt-6 flex flex-col border-2 border-[var(--flux-border)] bg-white sm:flex-row"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
name="EMAIL"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
class="w-full flex-1 px-4 py-3 text-sm outline-none placeholder:text-[#6b6b6b] sm:text-base"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-[var(--flux-orange)] px-4 py-3 text-sm font-bold uppercase tracking-wide text-black transition hover:bg-[var(--flux-blue)] hover:text-white sm:text-base"
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
<input type="hidden" name="tags" value="8115284" />
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-sm text-[var(--flux-muted)]">
|
||||
By subscribing, you agree to our
|
||||
<a class="underline" href="https://absmach.eu/privacy/" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
|
||||
and
|
||||
<a class="underline" href="https://absmach.eu/terms/" target="_blank" rel="noopener noreferrer">Terms of Service</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,49 @@
|
||||
<section id="performance" class="border-b-2 border-theme bg-theme-alt py-20">
|
||||
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
|
||||
<h2 class="mb-12 text-4xl font-bold md:text-5xl">
|
||||
<span class="border-l-4 border-[var(--flux-orange)] pl-4">PERFORMANCE</span>
|
||||
</h2>
|
||||
|
||||
<div class="max-w-4xl">
|
||||
<table class="metrics-table mono w-full text-sm md:text-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>METRIC</th>
|
||||
<th>VALUE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Concurrent Connections</td>
|
||||
<td class="font-bold">500K+ per node</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Message Throughput</td>
|
||||
<td class="font-bold">300K-500K msg/s per node</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latency (local)</td>
|
||||
<td class="font-bold"><10ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latency (cross-node)</td>
|
||||
<td class="font-bold">~5ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Session Takeover</td>
|
||||
<td class="font-bold"><100ms</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="brutalist-border bg-theme mt-8 p-6">
|
||||
<h3 class="mono mb-4 text-lg font-bold">CLUSTER SCALING</h3>
|
||||
<ul class="mono space-y-2 text-sm">
|
||||
<li class="border-l-4 border-[var(--flux-blue)] pl-4"><strong>3-node cluster:</strong> 1-2M msg/s</li>
|
||||
<li class="border-l-4 border-[var(--flux-blue)] pl-4"><strong>5-node cluster:</strong> 2-4M msg/s</li>
|
||||
<li class="border-l-4 border-[var(--flux-blue)] pl-4"><strong>Scaling:</strong> Linear with topic sharding</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
import CodePanel from './CodePanel.astro';
|
||||
|
||||
const dockerSnippet = `git clone https://github.com/absmach/fluxmq.git
|
||||
cd fluxmq
|
||||
docker compose -f docker/compose.yaml up -d`;
|
||||
|
||||
const mqttSnippet = `# Subscribe to all topics
|
||||
mosquitto_sub -p 1883 -t "test/#" -v
|
||||
|
||||
# Publish a message
|
||||
mosquitto_pub -p 1883 -t "test/hello" -m "Hello FluxMQ"`;
|
||||
|
||||
const localSnippet = `git clone https://github.com/absmach/fluxmq.git
|
||||
cd fluxmq
|
||||
make build
|
||||
./build/fluxmq --config examples/no-cluster.yaml`;
|
||||
---
|
||||
|
||||
<section id="quick-start" class="border-b-2 border-[var(--flux-border)] py-20">
|
||||
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
|
||||
<h2 class="mb-8 text-[clamp(2rem,4vw,3rem)] leading-tight font-bold uppercase tracking-[0.03em]">
|
||||
<span class="border-l-[5px] border-[var(--flux-orange)] pl-3">Quick Start</span>
|
||||
</h2>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">1. Run with Docker</h3>
|
||||
<CodePanel code={dockerSnippet} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">2. Test with MQTT</h3>
|
||||
<CodePanel code={mqttSnippet} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="mono mb-3 text-xl font-bold">3. Or Build Locally</h3>
|
||||
<CodePanel code={localSnippet} />
|
||||
</div>
|
||||
|
||||
<div class="cluster-box">
|
||||
<p class="mono text-sm md:text-base"><strong>Defaults:</strong> MQTT TCP :1883, AMQP 0.9.1 :5682, Data /tmp/fluxmq/data</p>
|
||||
<p class="mono mt-3"><strong>Next Steps:</strong></p>
|
||||
<ul class="mt-2 list-none space-y-2 p-0">
|
||||
<li>
|
||||
<a
|
||||
class="font-bold text-[var(--flux-blue)] hover:text-[var(--flux-orange)]"
|
||||
href="https://github.com/absmach/fluxmq/tree/main/examples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Explore code examples on GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="font-bold text-[var(--flux-blue)] hover:text-[var(--flux-orange)]" href="/docs/">
|
||||
Read the full documentation
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
const copyButtons = document.querySelectorAll('[data-copy-btn]');
|
||||
for (const button of copyButtons) {
|
||||
button.addEventListener('click', async () => {
|
||||
const panel = button.closest('.code-panel');
|
||||
const code = panel?.querySelector('code')?.textContent ?? '';
|
||||
|
||||
if (!code) return;
|
||||
|
||||
await navigator.clipboard.writeText(code);
|
||||
const original = button.textContent;
|
||||
button.textContent = 'Copied';
|
||||
window.setTimeout(() => {
|
||||
button.textContent = original;
|
||||
}, 1600);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</section>
|
||||
@@ -0,0 +1,61 @@
|
||||
<section id="use-cases" class="border-b-2 border-[var(--flux-border)] py-20">
|
||||
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
|
||||
<h2 class="mb-12 text-4xl font-bold md:text-5xl">
|
||||
<span class="border-l-4 border-[var(--flux-orange)] pl-4">USE CASES</span>
|
||||
</h2>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-3">
|
||||
<article class="brutalist-card p-6">
|
||||
<div class="mb-4 inline-flex size-9 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
|
||||
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 12h4l2-6 4 12 2-6h4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mb-4 border-b-2 border-[var(--flux-border)] pb-4">
|
||||
<h3 class="mono text-2xl font-bold">Event-Driven Systems</h3>
|
||||
</div>
|
||||
<ul class="space-y-3 text-base text-theme-muted">
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Decouple microservices with event streams</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Reliable command and event pipelines (CQRS)</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Background jobs and asynchronous workflows</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Real-time data processing pipelines</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="brutalist-card p-6">
|
||||
<div class="mb-4 inline-flex size-9 items-center justify-center rounded-sm border-2 border-[var(--flux-blue)] text-[var(--flux-blue)]">
|
||||
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
||||
<path d="M9 9h6v6H9z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mb-4 border-b-2 border-[var(--flux-border)] pb-4">
|
||||
<h3 class="mono text-2xl font-bold">IoT & Real-Time</h3>
|
||||
</div>
|
||||
<ul class="space-y-3 text-base text-theme-muted">
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>IoT device telemetry ingestion (MQTT)</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Edge deployments with intermittent connectivity</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Live dashboards and browser updates (WebSocket)</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Constrained device messaging via protocol bridges</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="brutalist-card p-6">
|
||||
<div class="mb-4 inline-flex size-9 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
|
||||
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 18h16M6 18V9M12 18V5M18 18v-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mb-4 border-b-2 border-[var(--flux-border)] pb-4">
|
||||
<h3 class="mono text-2xl font-bold">High-Throughput Pipelines</h3>
|
||||
</div>
|
||||
<ul class="space-y-3 text-base text-theme-muted">
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Stream millions of events per second</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Buffer traffic bursts with durable queues</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Decouple ingestion from downstream processing</span></li>
|
||||
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Power analytics and observability data streams</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<ClientRouter />
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const stored = localStorage.getItem('theme');
|
||||
const mode = stored === 'light' || stored === 'dark' ? stored : null;
|
||||
const resolved =
|
||||
mode ?? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
||||
document.documentElement.classList.toggle('dark', resolved === 'dark');
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="flex min-h-screen flex-col" transition:persist transition:animate="none" transition:name="page-layout">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
SearchDialog,
|
||||
SearchDialogClose,
|
||||
SearchDialogContent,
|
||||
SearchDialogHeader,
|
||||
SearchDialogIcon,
|
||||
SearchDialogInput,
|
||||
SearchDialogList,
|
||||
SearchDialogOverlay,
|
||||
type SharedProps,
|
||||
} from 'fumadocs-ui/components/dialog/search';
|
||||
import { useDocsSearch } from 'fumadocs-core/search/client';
|
||||
import { create } from '@orama/orama';
|
||||
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
||||
|
||||
function initOrama() {
|
||||
return create({
|
||||
schema: { _: 'string' },
|
||||
language: 'english',
|
||||
});
|
||||
}
|
||||
|
||||
export default function DefaultSearchDialog(props: SharedProps) {
|
||||
const { locale, locales } = useI18n();
|
||||
const searchLocale = locales && locales.length > 0 ? locale : undefined;
|
||||
|
||||
const { search, setSearch, query } = useDocsSearch({
|
||||
type: 'static',
|
||||
initOrama,
|
||||
locale: searchLocale,
|
||||
});
|
||||
|
||||
return (
|
||||
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
|
||||
<SearchDialogOverlay />
|
||||
<SearchDialogContent>
|
||||
<SearchDialogHeader>
|
||||
<SearchDialogIcon />
|
||||
<SearchDialogInput />
|
||||
<SearchDialogClose />
|
||||
</SearchDialogHeader>
|
||||
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
|
||||
</SearchDialogContent>
|
||||
</SearchDialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { glob } from 'astro/loaders';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const docs = defineCollection({
|
||||
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/docs/docs' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const meta = defineCollection({
|
||||
loader: glob({ pattern: '**/*.{json,yaml}', base: './src/content/docs/docs' }),
|
||||
schema: z.object({
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
pages: z.array(z.string()).optional(),
|
||||
icon: z.string().optional(),
|
||||
root: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
docs,
|
||||
meta,
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: FluxMQ Architecture
|
||||
description: Comprehensive system design overview covering layered architecture, protocol adapters, domain logic, and multi-protocol support
|
||||
---
|
||||
|
||||
# FluxMQ Architecture
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
## Overview
|
||||
|
||||
FluxMQ is a multi-protocol message broker built around a shared queue manager. MQTT transports (TCP, WebSocket, HTTP bridge, CoAP) share one MQTT broker instance, while AMQP 1.0 and AMQP 0.9.1 use dedicated brokers. Durable queues are protocol-agnostic and provide cross-protocol routing and fan-out.
|
||||
|
||||
## High-Level View
|
||||
|
||||
1. **Server wiring**
|
||||
Initializes protocol servers (MQTT TCP/WS/HTTP/CoAP and AMQP), creates the shared queue manager, and wires storage, clustering, metrics, and graceful shutdown.
|
||||
2. **Protocol entry points**
|
||||
MQTT transports feed the MQTT broker, while AMQP 1.0 and AMQP 0.9.1 each feed their own protocol broker.
|
||||
3. **Broker layer**
|
||||
MQTT, AMQP 1.0, and AMQP 0.9.1 brokers process protocol semantics independently.
|
||||
4. **Queue layer**
|
||||
Queue-capable traffic from all brokers converges into the shared **Queue Manager** (durable logs, fan-out, consumer groups).
|
||||
5. **Persistence layer**
|
||||
Queue logs and related durable data are written through the storage backend.
|
||||
|
||||
`Server Wiring -> Protocol Servers -> Protocol Brokers -> Queue Manager -> Log Storage`
|
||||
|
||||
## Key Components (Code Map)
|
||||
|
||||
- **MQTT Broker**: `mqtt/broker/`
|
||||
- Session lifecycle, topic routing, retained messages, wills
|
||||
- Shared subscriptions (MQTT 5.0)
|
||||
- Queue integration for `$queue/` topics and ack topics
|
||||
|
||||
- **AMQP Brokers**:
|
||||
- AMQP 1.0: `amqp1/broker/`
|
||||
- AMQP 0.9.1: `amqp/broker/`
|
||||
- Both integrate with the shared queue manager
|
||||
|
||||
- **Transports and Bridges**: `server/`
|
||||
- `server/tcp`, `server/websocket` for MQTT
|
||||
- `server/http` HTTP publish bridge
|
||||
- `server/coap` CoAP publish bridge
|
||||
- `server/amqp`, `server/amqp1` for AMQP listeners
|
||||
|
||||
- **Queue Manager**: `queue/` and `logstorage/`
|
||||
- Append-only logs with consumer groups
|
||||
- Queue and stream modes
|
||||
- Ack/Nack/Reject support and retention policies
|
||||
|
||||
- **Storage**: `storage/` (BadgerDB and memory backends)
|
||||
- Sessions, subscriptions, retained messages, offline queues
|
||||
|
||||
- **Clustering**: `cluster/`
|
||||
- Embedded etcd metadata, gRPC transport for routing
|
||||
- Session ownership, retained/will coordination
|
||||
|
||||
- **Observability**: `server/otel/`
|
||||
- OpenTelemetry metrics and tracing setup
|
||||
|
||||
- **Webhook Notifier**: `broker/webhook/`
|
||||
- Asynchronous event delivery with retries and circuit breaker
|
||||
|
||||
- **Queue API (Connect/gRPC)**: `server/api/`, `server/queue/`
|
||||
- Programmatic queue operations over HTTP/2 (h2c or TLS)
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `docs/broker.md`
|
||||
- `docs/queue.md`
|
||||
- `docs/configuration.md`
|
||||
- `docs/clustering.md`
|
||||
- `docs/webhooks.md`
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Broker & Message Routing
|
||||
description: Internal MQTT broker architecture, session management, message routing mechanisms, topic matching, QoS handling, and cluster integration
|
||||
---
|
||||
|
||||
# MQTT Broker Internals
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
This document describes the MQTT broker implementation and how it integrates with queues and clustering. The broker code lives in `mqtt/broker/` and uses shared components in `broker/` (router, webhook events, interfaces).
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Manage MQTT sessions (clean start, expiry, inflight tracking, offline queue)
|
||||
- Route messages to local subscribers via the topic router
|
||||
- Persist retained messages and wills
|
||||
- Enforce QoS rules and MaxQoS downgrade
|
||||
- Integrate with the queue manager for `$queue/` topics
|
||||
- Integrate with clustering for cross-node routing and session takeover
|
||||
- Emit webhook events (optional)
|
||||
|
||||
## Session Lifecycle (High Level)
|
||||
|
||||
- CONNECT arrives over a transport (TCP or WebSocket)
|
||||
- Broker creates or resumes session based on `clean_start` and expiry
|
||||
- Session state is restored from storage if needed
|
||||
- In clustered mode, session ownership is acquired and takeover is handled
|
||||
- On disconnect, offline queue is persisted (if session not expired)
|
||||
|
||||
## Message Routing
|
||||
|
||||
- Topic matching uses a trie-based router in `broker/router/`
|
||||
- Shared subscriptions (MQTT 5.0) are handled by the shared subscription manager
|
||||
- Retained messages are stored in the retained store and delivered on subscribe
|
||||
- Queue topics (`$queue/...`) are routed to the queue manager
|
||||
- Queue acks (`$queue/.../$ack|$nack|$reject`) are handled separately and do not enter normal pub/sub routing
|
||||
|
||||
## QoS Handling
|
||||
|
||||
- QoS 0, 1, and 2 are supported
|
||||
- Inflight tracking is persisted for QoS 1/2 sessions
|
||||
- MaxQoS is enforced by downgrading requested QoS when above server limits
|
||||
|
||||
## Cluster Integration
|
||||
|
||||
- Session ownership is coordinated via the cluster layer
|
||||
- Publishes are routed to remote nodes with matching subscriptions
|
||||
- Retained and will messages are backed by cluster-aware stores
|
||||
- Session takeover is supported when a client reconnects to another node
|
||||
|
||||
## Optional Subsystems
|
||||
|
||||
- **Auth/Authz**: pluggable interfaces in `broker/auth.go`
|
||||
- **Rate limiting**: per-IP and per-client limits in `ratelimit/`
|
||||
- **Webhooks**: event delivery via `broker/webhook/`
|
||||
- **OTel metrics/tracing**: optional, configured via `server` settings
|
||||
|
||||
## Configuration Pointers
|
||||
|
||||
- `broker.*` for broker limits (max message size, max QoS, retry policy)
|
||||
- `session.*` for session storage and offline queue limits
|
||||
- `ratelimit.*` for rate limiting
|
||||
- `webhook.*` for webhook delivery
|
||||
|
||||
See `docs/configuration.md` for full details.
|
||||
@@ -0,0 +1,636 @@
|
||||
---
|
||||
title: Client Library
|
||||
description: Pure Go client libraries for MQTT 3.1.1/5.0 and AMQP 0.9.1 with durable queue support, auto-reconnect, and comprehensive features
|
||||
---
|
||||
|
||||
# Client Library
|
||||
|
||||
Pure Go client libraries for MQTT 3.1.1/5.0 and AMQP 0.9.1 with durable queue support.
|
||||
Note: FluxMQ also exposes AMQP 1.0 server support, but this repository does not
|
||||
currently include a dedicated AMQP 1.0 client library.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Protocol Support:** MQTT 3.1.1 (v4) and MQTT 5.0 (v5)
|
||||
- **Auto-Reconnect:** Exponential backoff with configurable limits
|
||||
- **QoS Levels:** Full QoS 0/1/2 support with pluggable in-flight store (memory by default)
|
||||
- **TLS/SSL:** Secure connections with custom certificates
|
||||
- **Session Persistence:** Configurable session expiry
|
||||
- **Durable Queues:** Consumer groups and acknowledgments (DLQ wiring pending)
|
||||
- **MQTT 5.0 Features:** Topic aliases, user properties (publish/receive/will), flow control
|
||||
|
||||
---
|
||||
|
||||
## MQTT Client
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Connection
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"github.com/absmach/fluxmq/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create client with options
|
||||
opts := client.NewOptions().
|
||||
SetServers("localhost:1883").
|
||||
SetClientID("my-client").
|
||||
SetProtocolVersion(5).
|
||||
SetOnMessage(func(topic string, payload []byte, qos byte) {
|
||||
log.Printf("Received: %s -> %s", topic, string(payload))
|
||||
})
|
||||
|
||||
c := client.New(opts)
|
||||
|
||||
// Connect
|
||||
if err := c.Connect(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer c.Disconnect()
|
||||
|
||||
// Subscribe
|
||||
if err := c.SubscribeSingle("sensors/#", 1); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Publish
|
||||
if err := c.Publish("sensors/temp", []byte("22.5"), 1, false); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Keep running
|
||||
select {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Connection Settings
|
||||
|
||||
```go
|
||||
opts := client.NewOptions().
|
||||
SetServers("broker1:1883", "broker2:1883"). // Multiple servers
|
||||
SetClientID("device-001").
|
||||
SetCredentials("user", "password").
|
||||
SetTLSConfig(&tls.Config{...}). // Enable TLS
|
||||
SetConnectTimeout(10 * time.Second).
|
||||
SetKeepAlive(60 * time.Second)
|
||||
```
|
||||
|
||||
### Protocol Version
|
||||
|
||||
```go
|
||||
opts.SetProtocolVersion(4) // MQTT 3.1.1
|
||||
opts.SetProtocolVersion(5) // MQTT 5.0
|
||||
```
|
||||
|
||||
### Session Options
|
||||
|
||||
```go
|
||||
opts.SetCleanSession(true) // Start fresh each connect
|
||||
opts.SetCleanSession(false) // Resume previous session
|
||||
opts.SetSessionExpiry(3600) // Session persists 1 hour after disconnect (v5)
|
||||
```
|
||||
|
||||
### MQTT 5.0 Specific
|
||||
|
||||
```go
|
||||
opts.SetSessionExpiry(86400). // Session expires in 24h
|
||||
SetReceiveMaximum(100). // Max inflight messages to receive
|
||||
SetMaximumPacketSize(1048576). // Max 1MB packets
|
||||
SetTopicAliasMaximum(10). // Enable topic aliases
|
||||
SetRequestResponseInfo(true). // Request response info
|
||||
SetRequestProblemInfo(true) // Get detailed errors
|
||||
```
|
||||
|
||||
### Reconnection
|
||||
|
||||
```go
|
||||
opts.SetAutoReconnect(true) // Enable auto-reconnect
|
||||
opts.ReconnectBackoff = 1 * time.Second // Initial delay
|
||||
opts.MaxReconnectWait = 2 * time.Minute // Max delay
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Publishing Messages
|
||||
|
||||
### Basic Publish
|
||||
|
||||
```go
|
||||
// QoS 0 - Fire and forget
|
||||
c.Publish("topic", []byte("payload"), 0, false)
|
||||
|
||||
// QoS 1 - At least once
|
||||
c.Publish("topic", []byte("payload"), 1, false)
|
||||
|
||||
// QoS 2 - Exactly once
|
||||
c.Publish("topic", []byte("payload"), 2, false)
|
||||
|
||||
// Retained message
|
||||
c.Publish("config/device", []byte("settings"), 1, true)
|
||||
```
|
||||
|
||||
### MQTT 5.0 Publish Properties
|
||||
|
||||
Use `PublishMessage` to set publish properties such as content type, response
|
||||
topic, correlation data, and user properties (MQTT 5.0 only).
|
||||
|
||||
```go
|
||||
msg := &client.Message{
|
||||
Topic: "sensors/temp",
|
||||
Payload: []byte("22.5"),
|
||||
QoS: 1,
|
||||
ContentType: "text/plain",
|
||||
ResponseTopic: "responses/temp",
|
||||
CorrelationData: []byte("req-123"),
|
||||
UserProperties: map[string]string{"unit": "celsius"},
|
||||
}
|
||||
if err := c.PublishMessage(msg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Subscribing to Topics
|
||||
|
||||
### Basic Subscription
|
||||
|
||||
```go
|
||||
// Single topic
|
||||
c.SubscribeSingle("sensors/temp", 1)
|
||||
|
||||
// Multiple topics
|
||||
c.Subscribe(map[string]byte{
|
||||
"sensors/#": 1,
|
||||
"devices/+/status": 2,
|
||||
})
|
||||
```
|
||||
|
||||
### MQTT 5.0 Subscription Options
|
||||
|
||||
```go
|
||||
opts := &client.SubscribeOption{
|
||||
Topic: "sensors/temp",
|
||||
QoS: 1,
|
||||
NoLocal: true, // Don't receive own messages
|
||||
RetainAsPublished: true, // Keep original retain flag
|
||||
RetainHandling: 1, // Only send retained if new sub
|
||||
SubscriptionID: 42, // Track subscription
|
||||
}
|
||||
c.SubscribeWithOptions(opts)
|
||||
```
|
||||
|
||||
### Unsubscribe
|
||||
|
||||
```go
|
||||
c.Unsubscribe("sensors/temp")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message Handling
|
||||
|
||||
### Simple Handler
|
||||
|
||||
```go
|
||||
opts.SetOnMessage(func(topic string, payload []byte, qos byte) {
|
||||
log.Printf("[%s] QoS %d: %s", topic, qos, payload)
|
||||
})
|
||||
```
|
||||
|
||||
### Full Message Context (MQTT 5.0)
|
||||
|
||||
```go
|
||||
opts.SetOnMessageV2(func(msg *client.Message) {
|
||||
log.Printf("Topic: %s", msg.Topic)
|
||||
log.Printf("Payload: %s", msg.Payload)
|
||||
log.Printf("QoS: %d", msg.QoS)
|
||||
log.Printf("Retain: %v", msg.Retain)
|
||||
log.Printf("Properties: %+v", msg.UserProperties)
|
||||
log.Printf("Response Topic: %s", msg.ResponseTopic)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Durable Queues
|
||||
|
||||
The client supports durable queues with consumer groups and message acknowledgment.
|
||||
Reject/DLQ wiring in the broker is pending.
|
||||
MQTT v3 can publish and subscribe to queue topics, but acknowledgments require MQTT v5 user properties.
|
||||
|
||||
**When to use queues instead of regular pub/sub:**
|
||||
- You need at-least-once processing with explicit acknowledgments
|
||||
- Multiple consumers should share the workload (consumer groups)
|
||||
- Failed messages need retry logic or dead-letter handling
|
||||
|
||||
**Key concepts:**
|
||||
- **Queue**: Persistent message buffer with ordered delivery per queue (single log)
|
||||
- **Consumer Group**: Multiple consumers share messages from the same queue
|
||||
- **Acknowledgment**: Confirm success (Ack), request redelivery (Nack), or reject permanently (Reject)
|
||||
|
||||
### Publishing to Queues
|
||||
|
||||
```go
|
||||
// Simple queue publish
|
||||
c.PublishToQueue("orders", []byte(`{"item": "widget"}`))
|
||||
|
||||
// Full control
|
||||
c.PublishToQueueWithOptions(&client.QueuePublishOptions{
|
||||
QueueName: "events",
|
||||
Payload: []byte("event-data"),
|
||||
Properties: map[string]string{"priority": "high"},
|
||||
QoS: 1,
|
||||
})
|
||||
```
|
||||
|
||||
### Subscribing to Queues
|
||||
|
||||
```go
|
||||
// Subscribe with consumer group
|
||||
err := c.SubscribeToQueue("orders", "order-processors", func(msg *client.QueueMessage) {
|
||||
log.Printf("Processing order: %s", msg.Payload)
|
||||
log.Printf("Message ID: %s", msg.MessageID)
|
||||
log.Printf("Group: %s", msg.GroupID)
|
||||
log.Printf("Offset: %d", msg.Offset)
|
||||
|
||||
// Process message...
|
||||
if processedOK {
|
||||
msg.Ack() // Message removed from queue
|
||||
} else if shouldRetry {
|
||||
msg.Nack() // Redelivery eligible (subject to broker delivery/visibility timing)
|
||||
} else {
|
||||
msg.Reject() // Removes from pending; DLQ routing not wired yet
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Message Acknowledgment
|
||||
|
||||
| Method | Effect |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
| `msg.Ack()` | Message processed successfully, removed from queue |
|
||||
| `msg.Nack()` | Processing failed, make eligible for redelivery |
|
||||
| `msg.Reject()` | Remove from pending; DLQ routing not wired yet |
|
||||
|
||||
### Direct Acknowledgment
|
||||
|
||||
```go
|
||||
// Acknowledge by message ID (explicit group)
|
||||
c.AckWithGroup("orders", "msg-12345", "processors")
|
||||
c.NackWithGroup("orders", "msg-12345", "processors")
|
||||
c.RejectWithGroup("orders", "msg-12345", "processors")
|
||||
```
|
||||
Note: MQTT queue acknowledgments require MQTT v5 and the broker expects
|
||||
`message-id` and `group-id` user properties on ack messages. `QueueMessage.Ack()`
|
||||
sends both when they are present on incoming messages.
|
||||
|
||||
### Unsubscribe from Queue
|
||||
|
||||
```go
|
||||
c.UnsubscribeFromQueue("orders")
|
||||
```
|
||||
|
||||
### Queue Code Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"github.com/absmach/fluxmq/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
opts := client.NewOptions().
|
||||
SetServers("localhost:1883").
|
||||
SetClientID("order-processor").
|
||||
SetProtocolVersion(5)
|
||||
|
||||
c := client.New(opts)
|
||||
if err := c.Connect(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer c.Disconnect()
|
||||
|
||||
// Subscribe to order queue with consumer group
|
||||
err := c.SubscribeToQueue("orders", "processors", func(msg *client.QueueMessage) {
|
||||
log.Printf("Order received: %s", msg.Payload)
|
||||
|
||||
// Simulate processing
|
||||
if processOrder(msg.Payload) {
|
||||
if err := msg.Ack(); err != nil {
|
||||
log.Printf("Ack failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
msg.Nack() // Retry later
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Publish some orders
|
||||
for i := 0; i < 10; i++ {
|
||||
c.PublishToQueue("orders", []byte(`{"id": "`+string(rune(i))+`"}`))
|
||||
}
|
||||
|
||||
select {} // Keep running
|
||||
}
|
||||
|
||||
func processOrder(payload []byte) bool {
|
||||
// Process order logic
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
### Callbacks
|
||||
|
||||
```go
|
||||
opts.SetOnConnect(func() {
|
||||
log.Println("Connected!")
|
||||
}).
|
||||
SetOnConnectionLost(func(err error) {
|
||||
log.Printf("Connection lost: %v", err)
|
||||
}).
|
||||
SetOnReconnecting(func(attempt int) {
|
||||
log.Printf("Reconnecting (attempt %d)...", attempt)
|
||||
}).
|
||||
SetOnServerCapabilities(func(caps *client.ServerCapabilities) {
|
||||
log.Printf("Server max QoS: %d", caps.MaximumQoS)
|
||||
log.Printf("Server retain available: %v", caps.RetainAvailable)
|
||||
})
|
||||
```
|
||||
|
||||
### Disconnect
|
||||
|
||||
```go
|
||||
// Normal disconnect
|
||||
c.Disconnect()
|
||||
|
||||
// With reason (MQTT 5.0)
|
||||
c.DisconnectWithReason(0x04, 0, "Going offline")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Will Messages
|
||||
|
||||
Configure a last-will message sent when the client disconnects unexpectedly:
|
||||
|
||||
```go
|
||||
opts.SetWill("clients/device-001/status", []byte("offline"), 1, true)
|
||||
```
|
||||
|
||||
### With MQTT 5.0 Properties
|
||||
|
||||
```go
|
||||
opts.Will = &client.WillMessage{
|
||||
Topic: "clients/device-001/status",
|
||||
Payload: []byte("offline"),
|
||||
QoS: 1,
|
||||
Retain: true,
|
||||
WillDelayInterval: 30, // Wait 30s before sending
|
||||
MessageExpiry: 3600,
|
||||
UserProperties: map[string]string{"reason": "unexpected"},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Cause |
|
||||
| ------------------------- | ------------------------------------------ |
|
||||
| `ErrNotConnected` | Operation attempted while disconnected |
|
||||
| `ErrNoServers` | No broker addresses configured |
|
||||
| `ErrEmptyClientID` | ClientID not set |
|
||||
| `ErrInvalidProtocol` | Protocol version must be 4 or 5 |
|
||||
| `ErrInvalidQoS` | QoS must be 0, 1, or 2 |
|
||||
| `ErrInvalidTopic` | Empty or invalid topic string |
|
||||
| `ErrInvalidMessage` | Message is nil or invalid |
|
||||
| `ErrMaxInflight` | Too many pending messages |
|
||||
| `ErrQueueAckRequiresV5` | Queue acks require MQTT v5 user properties |
|
||||
| `ErrQueueAckMissingGroup` | `group-id` missing for queue ack |
|
||||
|
||||
### Handling Connection Errors
|
||||
|
||||
```go
|
||||
if err := c.Connect(); err != nil {
|
||||
switch err {
|
||||
case client.ErrNoServers:
|
||||
log.Fatal("No brokers configured")
|
||||
case client.ErrEmptyClientID:
|
||||
log.Fatal("ClientID required")
|
||||
default:
|
||||
log.Printf("Connection error: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message Store
|
||||
|
||||
For QoS 1/2 in-flight storage:
|
||||
|
||||
```go
|
||||
store := client.NewMemoryStore()
|
||||
opts.SetStore(store)
|
||||
```
|
||||
|
||||
Built-in stores:
|
||||
- **MemoryStore** (default): In-memory, lost on restart
|
||||
|
||||
You can implement the `MessageStore` interface to persist QoS 1/2 in-flight data.
|
||||
|
||||
---
|
||||
|
||||
## Defaults
|
||||
|
||||
| Option | Default Value |
|
||||
| ---------------- | -------------- |
|
||||
| KeepAlive | 60 seconds |
|
||||
| ConnectTimeout | 10 seconds |
|
||||
| WriteTimeout | 5 seconds |
|
||||
| AckTimeout | 10 seconds |
|
||||
| PingTimeout | 5 seconds |
|
||||
| MaxInflight | 100 |
|
||||
| MessageChanSize | 256 |
|
||||
| AutoReconnect | true |
|
||||
| ReconnectBackoff | 1 second |
|
||||
| MaxReconnectWait | 2 minutes |
|
||||
| ProtocolVersion | 4 (MQTT 3.1.1) |
|
||||
| CleanSession | true |
|
||||
|
||||
---
|
||||
|
||||
## AMQP 0.9.1 Client
|
||||
|
||||
The AMQP 0.9.1 client focuses on durable queue interop with the broker. It uses the same queue naming convention as MQTT: pass the queue name without the `$queue/` prefix.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/absmach/fluxmq/client/amqp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
opts := amqp.NewOptions().
|
||||
SetAddress("localhost:5682").
|
||||
SetCredentials("guest", "guest")
|
||||
|
||||
c, err := amqp.New(opts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Connect(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// Subscribe to a queue with a consumer group
|
||||
err = c.SubscribeToQueue("tasks/orders", "order-shipper", func(msg *amqp.QueueMessage) {
|
||||
log.Printf("Received: %s", string(msg.Body))
|
||||
_ = msg.Ack()
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Publish to the same queue
|
||||
if err := c.PublishToQueue("tasks/orders", []byte("hello")); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
```
|
||||
|
||||
### Queue Semantics
|
||||
|
||||
- `SubscribeToQueue` passes the consumer group via `x-consumer-group` on `basic.consume`.
|
||||
- `Ack`, `Nack`, and `Reject` map to `basic.ack`, `basic.nack`, and `basic.reject`.
|
||||
|
||||
### Stream Queues (RabbitMQ-Compatible)
|
||||
|
||||
Stream queues provide log-style consumption with cursor offsets.
|
||||
Stream queue names follow RabbitMQ conventions (no `$queue/` prefix).
|
||||
Offsets are passed as `x-stream-offset` strings; values like `first`, `last`,
|
||||
`next`, `offset=<n>`, `timestamp=<unix>` are interpreted by the broker.
|
||||
|
||||
```go
|
||||
// Declare a stream queue
|
||||
qName, err := c.DeclareStreamQueue(&amqp.StreamQueueOptions{
|
||||
Name: "events",
|
||||
Durable: true,
|
||||
MaxAge: "7D",
|
||||
MaxLengthBytes: 10 * 1024 * 1024 * 1024,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("stream queue: %s", qName)
|
||||
|
||||
// Consume from the beginning
|
||||
err = c.SubscribeToStream(&amqp.StreamConsumeOptions{
|
||||
QueueName: "events",
|
||||
Offset: "first",
|
||||
}, func(msg *amqp.QueueMessage) {
|
||||
if off, ok := msg.StreamOffset(); ok {
|
||||
log.Printf("offset=%d payload=%s", off, string(msg.Body))
|
||||
}
|
||||
_ = msg.Ack()
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Publish to the stream queue (RabbitMQ-style)
|
||||
if err := c.PublishToStream("events", []byte("hello"), nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
Stream deliveries include:
|
||||
- `x-stream-offset`
|
||||
- `x-stream-timestamp`
|
||||
- `x-work-acked` / `x-work-committed-offset`
|
||||
|
||||
The `x-work-*` fields report the configured primary work group's committed offset.
|
||||
`x-work-acked` is `true` when this message's offset is below the committed offset,
|
||||
which can lag slightly due to auto-commit interval batching.
|
||||
Convenience accessors are available on `QueueMessage`:
|
||||
`StreamOffset()`, `StreamTimestamp()`, `WorkAcked()`, `WorkCommittedOffset()`, `WorkGroup()`.
|
||||
|
||||
### Manual Commit Mode
|
||||
|
||||
By default, stream consumers auto-commit offsets as messages are delivered
|
||||
(similar to Kafka's `enable.auto.commit=true`). For exactly-once processing,
|
||||
disable auto-commit and commit explicitly.
|
||||
|
||||
Auto-commit is rate-limited by the server setting
|
||||
`queue_manager.auto_commit_interval` (default: `5s`).
|
||||
|
||||
Minimal example:
|
||||
|
||||
```go
|
||||
autoCommit := false
|
||||
_ = c.SubscribeToStream(&amqp.StreamConsumeOptions{
|
||||
QueueName: "events",
|
||||
ConsumerGroup: "my-group",
|
||||
AutoCommit: &autoCommit,
|
||||
}, handler)
|
||||
|
||||
_ = c.CommitOffset("events", "my-group", lastProcessedOffset)
|
||||
```
|
||||
|
||||
Use the same consumer group name in both calls.
|
||||
|
||||
With manual commit:
|
||||
- Messages are delivered but the committed offset doesn't advance automatically
|
||||
- On reconnect, delivery resumes from the last committed offset
|
||||
- Use `CommitOffset()` to advance the committed position
|
||||
|
||||
### Pub/Sub
|
||||
|
||||
```go
|
||||
_ = c.Subscribe("sensors/#", func(msg *amqp.Message) {
|
||||
log.Printf("Topic: %s Payload: %s", msg.Topic, string(msg.Body))
|
||||
})
|
||||
|
||||
_ = c.Publish("sensors/temp", []byte("22.5"))
|
||||
```
|
||||
|
||||
### Reconnection
|
||||
|
||||
```go
|
||||
opts.SetAutoReconnect(true).
|
||||
SetReconnectBackoff(1 * time.Second).
|
||||
SetMaxReconnectWait(2 * time.Minute).
|
||||
SetOnConnectionLost(func(err error) { log.Printf("lost: %v", err) }).
|
||||
SetOnReconnecting(func(attempt int) { log.Printf("reconnect attempt %d", attempt) })
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Clustering
|
||||
description: Distributed broker clustering with embedded etcd, gRPC transport, session takeover, and high availability architecture
|
||||
---
|
||||
|
||||
# Clustering
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
FluxMQ supports optional clustering for high availability and cross-node routing. Clustering is embedded and uses etcd for metadata coordination plus gRPC for inter-broker transport.
|
||||
|
||||
## What Clustering Provides
|
||||
|
||||
- **Session ownership** across nodes
|
||||
- **Subscription routing** for cross-node publishes
|
||||
- **Queue consumer registry** for cross-node queue delivery
|
||||
- **Retained/will storage** with a hybrid metadata + payload strategy
|
||||
- **Session takeover** when a client reconnects to a different node
|
||||
|
||||
## Core Components
|
||||
|
||||
- **Embedded etcd**: stores session ownership, subscriptions, queue consumers, retained metadata
|
||||
- **Transport (gRPC)**: routes publishes, queue deliveries, retained/will fetches
|
||||
- **Optional Raft for queues**: configurable replication for queue appends
|
||||
|
||||
## Configuration (Minimal)
|
||||
|
||||
```yaml
|
||||
cluster:
|
||||
enabled: true
|
||||
node_id: "broker-1"
|
||||
|
||||
etcd:
|
||||
data_dir: "/tmp/fluxmq/etcd"
|
||||
bind_addr: "0.0.0.0:2380"
|
||||
client_addr: "0.0.0.0:2379"
|
||||
initial_cluster: "broker-1=http://0.0.0.0:2380"
|
||||
bootstrap: true
|
||||
|
||||
transport:
|
||||
bind_addr: "0.0.0.0:7948"
|
||||
peers: {}
|
||||
```
|
||||
|
||||
For full cluster options (TLS, Raft, hybrid retained settings), see `docs/configuration.md`.
|
||||
|
||||
## Message Routing (High Level)
|
||||
|
||||
- On publish, the broker routes to local subscribers and calls the cluster router to forward to remote nodes with matching subscriptions.
|
||||
- Retained and will messages are stored in a cluster-aware store with metadata in etcd and payloads stored locally for larger messages.
|
||||
- When a client reconnects to a different node, the new node requests session takeover from the previous owner.
|
||||
|
||||
## Queue Delivery Across Nodes
|
||||
|
||||
Queue consumers are registered in the cluster. When a queue publish occurs, the queue manager determines which nodes host matching consumers and forwards delivery to those nodes.
|
||||
|
||||
## Notes
|
||||
|
||||
- Clustering is optional; single-node mode uses in-memory or BadgerDB storage.
|
||||
- Queue replication is optional and controlled via `cluster.raft`.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Comparison
|
||||
description: Comprehensive comparison of FluxMQ against industry solutions including EMQX, HiveMQ, Artemis, Mosquitto, NATS, RabbitMQ, and Kafka
|
||||
---
|
||||
|
||||
# Comparison Guide
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
This document provides a basic, evergreen comparison guide without hard claims about third‑party products. Vendor feature sets change frequently; verify details with official docs before making a decision.
|
||||
|
||||
## FluxMQ Snapshot (from current codebase)
|
||||
|
||||
- **Protocols**: MQTT 3.1.1/5.0 (TCP, WebSocket), HTTP publish bridge, CoAP publish bridge, AMQP 1.0 and AMQP 0.9.1
|
||||
- **Queues**: Durable queues, consumer groups, ack/nack/reject; stream queues supported; retention policies (time/size/message count)
|
||||
- **DLQ**: Handler exists, automatic DLQ routing not wired yet
|
||||
- **Clustering**: Embedded etcd coordination, gRPC routing, session takeover, hybrid retained storage, optional Raft for queue appends
|
||||
- **Storage**: BadgerDB or in‑memory; pluggable interfaces
|
||||
- **Security**: TLS/mTLS, WebSocket origin validation, rate limiting
|
||||
- **Observability**: OpenTelemetry metrics/tracing (OTLP); no native Prometheus endpoint
|
||||
- **Clients**: Go MQTT client, Go AMQP 0.9.1 client (AMQP 1.0 client not provided)
|
||||
|
||||
## How to Compare Systems (by category)
|
||||
|
||||
### MQTT‑Native Brokers
|
||||
Best fit when:
|
||||
- MQTT 5.0 features and device connectivity are the primary focus
|
||||
- Edge/IoT workloads need lightweight clients and topic‑based routing
|
||||
|
||||
### Queue‑Centric Brokers
|
||||
Best fit when:
|
||||
- Work‑queue semantics and explicit acknowledgments are primary
|
||||
- Fine‑grained retry/DLQ handling is critical
|
||||
|
||||
### Log‑Centric Brokers
|
||||
Best fit when:
|
||||
- Long‑term event retention and replay are primary
|
||||
- Stream processing and analytics require strong log semantics
|
||||
|
||||
### Multi‑Protocol Brokers
|
||||
Best fit when:
|
||||
- You need multiple client protocols in a single deployment
|
||||
- Cross‑protocol routing and shared durability are important
|
||||
|
||||
## Decision Checklist
|
||||
|
||||
- Protocols you must support (MQTT, AMQP, HTTP, CoAP)
|
||||
- Delivery guarantees required (QoS 0/1/2, at‑least‑once, exactly‑once)
|
||||
- Durability and retention needs (time/size retention, replay)
|
||||
- Operational complexity you can tolerate (single binary vs external dependencies)
|
||||
- Cluster behavior (session takeover, routing, replication strategy)
|
||||
- Observability requirements (OTLP, Prometheus, tracing)
|
||||
- Client library availability and ecosystem fit
|
||||
|
||||
## Notes
|
||||
|
||||
- FluxMQ is still evolving. For current status of features and limitations, see `docs/queue.md`, `docs/configuration.md`, and `docs/roadmap.md`.
|
||||
- Benchmarking should be done on your target hardware using `benchmarks/`.
|
||||
@@ -0,0 +1,285 @@
|
||||
---
|
||||
title: Configuration Guide
|
||||
description: Comprehensive configuration reference for server transports, broker settings, clustering, storage, security, and operational features
|
||||
---
|
||||
|
||||
# Configuration Guide
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
FluxMQ uses a single YAML configuration file. Start the broker with:
|
||||
|
||||
```bash
|
||||
./build/fluxmq --config /path/to/config.yaml
|
||||
```
|
||||
|
||||
If `--config` is omitted, defaults are used (see `config.Default()` in `config/config.go`).
|
||||
|
||||
## Configuration Overview
|
||||
|
||||
Top-level keys:
|
||||
|
||||
- `server`
|
||||
- `broker`
|
||||
- `session`
|
||||
- `queue_manager`
|
||||
- `queues`
|
||||
- `storage`
|
||||
- `cluster`
|
||||
- `webhook`
|
||||
- `ratelimit`
|
||||
- `log`
|
||||
|
||||
Durations use Go duration strings like `5s`, `1m`, `24h`.
|
||||
|
||||
## Server
|
||||
|
||||
`server` controls network listeners and telemetry endpoints.
|
||||
|
||||
```yaml
|
||||
server:
|
||||
tcp:
|
||||
plain:
|
||||
addr: ":1883"
|
||||
max_connections: 10000
|
||||
read_timeout: "60s"
|
||||
write_timeout: "60s"
|
||||
tls: {}
|
||||
mtls: {}
|
||||
|
||||
websocket:
|
||||
plain:
|
||||
addr: ":8083"
|
||||
path: "/mqtt"
|
||||
allowed_origins: ["https://app.example.com"]
|
||||
tls: {}
|
||||
mtls: {}
|
||||
|
||||
http:
|
||||
plain:
|
||||
addr: ":8080"
|
||||
tls: {}
|
||||
mtls: {}
|
||||
|
||||
coap:
|
||||
plain:
|
||||
addr: ":5683"
|
||||
dtls: {}
|
||||
mdtls: {}
|
||||
|
||||
amqp:
|
||||
plain:
|
||||
addr: ":5672"
|
||||
tls: {}
|
||||
mtls: {}
|
||||
|
||||
amqp091:
|
||||
plain:
|
||||
addr: ":5682"
|
||||
tls: {}
|
||||
mtls: {}
|
||||
|
||||
health_enabled: true
|
||||
health_addr: ":8081"
|
||||
|
||||
metrics_enabled: false
|
||||
metrics_addr: "localhost:4317" # OTLP endpoint
|
||||
|
||||
otel_service_name: "fluxmq"
|
||||
otel_service_version: "dev"
|
||||
otel_metrics_enabled: true
|
||||
otel_traces_enabled: false
|
||||
otel_trace_sample_rate: 0.1
|
||||
|
||||
api_enabled: false
|
||||
api_addr: ":9090" # Queue API (Connect/gRPC)
|
||||
|
||||
shutdown_timeout: "30s"
|
||||
```
|
||||
|
||||
### TLS/DTLS Settings
|
||||
|
||||
TLS fields are shared across `tls`, `mtls`, `dtls`, and `mdtls` blocks via `pkg/tls` config:
|
||||
|
||||
- `cert_file`, `key_file`
|
||||
- `ca_file` (client CA) and `server_ca_file`
|
||||
- `client_auth` (e.g., `require`, `verify_if_given`)
|
||||
- `min_version`, `cipher_suites`, `prefer_server_cipher_suites`
|
||||
- `ocsp`, `crl` (advanced verification)
|
||||
|
||||
## Broker
|
||||
|
||||
```yaml
|
||||
broker:
|
||||
max_message_size: 1048576
|
||||
max_retained_messages: 10000
|
||||
retry_interval: "20s"
|
||||
max_retries: 0
|
||||
max_qos: 2
|
||||
```
|
||||
|
||||
## Session
|
||||
|
||||
```yaml
|
||||
session:
|
||||
max_sessions: 10000
|
||||
default_expiry_interval: 300
|
||||
max_offline_queue_size: 1000
|
||||
max_inflight_messages: 100
|
||||
offline_queue_policy: "evict" # evict or reject
|
||||
```
|
||||
|
||||
## Queue Manager
|
||||
|
||||
```yaml
|
||||
queue_manager:
|
||||
auto_commit_interval: "5s" # Stream groups auto-commit cadence
|
||||
```
|
||||
|
||||
## Queues
|
||||
|
||||
Queue configuration controls durable queues and stream queues.
|
||||
|
||||
```yaml
|
||||
queues:
|
||||
- name: "mqtt"
|
||||
topics: ["$queue/#"]
|
||||
reserved: true
|
||||
type: "classic" # classic or stream
|
||||
primary_group: "" # stream status reporting
|
||||
|
||||
limits:
|
||||
max_message_size: 10485760
|
||||
max_depth: 100000
|
||||
message_ttl: "168h"
|
||||
|
||||
retry:
|
||||
max_retries: 10
|
||||
initial_backoff: "5s"
|
||||
max_backoff: "5m"
|
||||
multiplier: 2.0
|
||||
|
||||
dlq:
|
||||
enabled: true
|
||||
topic: "" # optional override
|
||||
|
||||
retention:
|
||||
max_age: "168h"
|
||||
max_length_bytes: 0
|
||||
max_length_messages: 0
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
```yaml
|
||||
storage:
|
||||
type: "badger" # memory or badger
|
||||
badger_dir: "/tmp/fluxmq/data"
|
||||
sync_writes: false
|
||||
```
|
||||
|
||||
## Cluster
|
||||
|
||||
```yaml
|
||||
cluster:
|
||||
enabled: false
|
||||
node_id: "broker-1"
|
||||
|
||||
etcd:
|
||||
data_dir: "/tmp/fluxmq/etcd"
|
||||
bind_addr: "0.0.0.0:2380"
|
||||
client_addr: "0.0.0.0:2379"
|
||||
initial_cluster: "broker-1=http://0.0.0.0:2380"
|
||||
bootstrap: true
|
||||
hybrid_retained_size_threshold: 1024
|
||||
|
||||
transport:
|
||||
bind_addr: "0.0.0.0:7948"
|
||||
peers: {}
|
||||
tls_enabled: false
|
||||
tls_cert_file: ""
|
||||
tls_key_file: ""
|
||||
tls_ca_file: ""
|
||||
|
||||
raft:
|
||||
enabled: false
|
||||
replication_factor: 3
|
||||
sync_mode: true
|
||||
min_in_sync_replicas: 2
|
||||
ack_timeout: "5s"
|
||||
write_policy: "forward" # local, reject, forward
|
||||
distribution_mode: "replicate" # forward, replicate
|
||||
bind_addr: "127.0.0.1:7100"
|
||||
data_dir: "/tmp/fluxmq/raft"
|
||||
peers: {}
|
||||
heartbeat_timeout: "1s"
|
||||
election_timeout: "3s"
|
||||
snapshot_interval: "5m"
|
||||
snapshot_threshold: 8192
|
||||
```
|
||||
|
||||
## Webhooks
|
||||
|
||||
```yaml
|
||||
webhook:
|
||||
enabled: false
|
||||
queue_size: 10000
|
||||
drop_policy: "oldest" # oldest or newest
|
||||
workers: 5
|
||||
include_payload: false
|
||||
shutdown_timeout: "30s"
|
||||
|
||||
defaults:
|
||||
timeout: "5s"
|
||||
retry:
|
||||
max_attempts: 3
|
||||
initial_interval: "1s"
|
||||
max_interval: "30s"
|
||||
multiplier: 2.0
|
||||
circuit_breaker:
|
||||
failure_threshold: 5
|
||||
reset_timeout: "60s"
|
||||
|
||||
endpoints:
|
||||
- name: "analytics"
|
||||
type: "http"
|
||||
url: "https://example.com/webhook"
|
||||
events: ["message.published"]
|
||||
topic_filters: ["sensors/#"]
|
||||
headers:
|
||||
Authorization: "Bearer token"
|
||||
timeout: "10s"
|
||||
```
|
||||
|
||||
Only `http` endpoints are supported at the moment.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
```yaml
|
||||
ratelimit:
|
||||
enabled: false
|
||||
|
||||
connection:
|
||||
enabled: true
|
||||
rate: 1.6667 # connections per second per IP
|
||||
burst: 20
|
||||
cleanup_interval: "5m"
|
||||
|
||||
message:
|
||||
enabled: true
|
||||
rate: 1000 # messages per second per client
|
||||
burst: 100
|
||||
|
||||
subscribe:
|
||||
enabled: true
|
||||
rate: 100 # subscriptions per second per client
|
||||
burst: 10
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
```yaml
|
||||
log:
|
||||
level: "info" # debug, info, warn, error
|
||||
format: "text" # text or json
|
||||
```
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: FluxMQ Documentation
|
||||
description: Welcome to FluxMQ - a high-performance, multi-protocol message broker with MQTT, WebSocket, HTTP, and CoAP support
|
||||
---
|
||||
|
||||
# FluxMQ
|
||||
|
||||
A high-performance, multi-protocol message broker written in Go designed for scalability, extensibility, and protocol diversity. Supports MQTT 3.1.1 and 5.0 over TCP and WebSocket, plus HTTP-MQTT and CoAP bridges for IoT integration.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Protocol Support** - MQTT 3.1.1/5.0, WebSocket, HTTP-MQTT Bridge, CoAP Bridge
|
||||
- **High Performance** - 300K-500K msg/s per node with zero-copy packet parsing
|
||||
- **Clustering** - Embedded etcd for distributed coordination, no external dependencies
|
||||
- **Durable Queues** - Consumer groups with ack/nack/reject semantics
|
||||
- **Security** - TLS/mTLS, rate limiting, authentication plugins
|
||||
- **Observability** - OpenTelemetry metrics, structured logging, webhook notifications
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Build & Run
|
||||
|
||||
```bash
|
||||
# Clone and build
|
||||
git clone https://github.com/absmach/fluxmq.git
|
||||
cd fluxmq
|
||||
make build
|
||||
|
||||
# Run single node
|
||||
./build/fluxmq
|
||||
|
||||
# Run with configuration
|
||||
./build/fluxmq --config config.yaml
|
||||
```
|
||||
|
||||
### Test with MQTT Client
|
||||
|
||||
```bash
|
||||
# Subscribe
|
||||
mosquitto_sub -p 1883 -t "test/#" -v
|
||||
|
||||
# Publish
|
||||
mosquitto_pub -p 1883 -t "test/hello" -m "Hello FluxMQ"
|
||||
```
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```yaml
|
||||
server:
|
||||
tcp:
|
||||
plain:
|
||||
addr: ":1883"
|
||||
websocket:
|
||||
plain:
|
||||
addr: ":8083"
|
||||
path: "/mqtt"
|
||||
http:
|
||||
plain:
|
||||
addr: ":8080"
|
||||
|
||||
broker:
|
||||
max_message_size: 1048576
|
||||
max_retained_messages: 10000
|
||||
|
||||
storage:
|
||||
type: badger
|
||||
badger_dir: "./data"
|
||||
|
||||
log:
|
||||
level: info
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
FluxMQ uses a clean layered architecture:
|
||||
|
||||
- **Transport Layer** - TCP, WebSocket, HTTP, CoAP servers
|
||||
- **Protocol Layer** - MQTT 3.1.1/5.0 packet handling
|
||||
- **Domain Layer** - Protocol-agnostic broker logic
|
||||
- **Storage Layer** - BadgerDB for persistence, etcd for clustering
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Architecture](/docs/architecture): System design and component overview
|
||||
- [Configuration](/docs/configuration): Complete configuration reference
|
||||
- [Clustering](/docs/clustering): Distributed broker setup
|
||||
- [Client Libraries](/docs/client): Go MQTT and AMQP clients
|
||||
- [Durable Queues](/docs/queue): Queue system with consumer groups
|
||||
- [Webhooks](/docs/webhooks): Event notification system
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
| -------------------------- | ------------------------ |
|
||||
| **Concurrent Connections** | 500K+ per node |
|
||||
| **Message Throughput** | 300K-500K msg/s per node |
|
||||
| **Latency (local)** | \<10ms |
|
||||
| **Session Takeover** | \<100ms |
|
||||
|
||||
## Use Cases
|
||||
|
||||
**Event-Driven Architectures**
|
||||
|
||||
- Event backbone for microservices
|
||||
- CQRS systems with durable queues
|
||||
- Real-time event processing
|
||||
|
||||
**IoT & Real-Time Systems**
|
||||
|
||||
- Device communication via MQTT
|
||||
- Browser clients via WebSocket
|
||||
- Edge computing deployments
|
||||
|
||||
**High-Availability Systems**
|
||||
|
||||
- Clustered deployments (3-5 nodes)
|
||||
- Geographic distribution
|
||||
- Linear scaling
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **GitHub**: [github.com/absmach/fluxmq](https://github.com/absmach/fluxmq)
|
||||
- **Issues**: Report bugs and request features
|
||||
- **Discussions**: Community support and questions
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"root": true,
|
||||
"pages": [
|
||||
"---Overview---",
|
||||
"index",
|
||||
"roadmap",
|
||||
"---Core Concepts---",
|
||||
"architecture",
|
||||
"broker",
|
||||
"queue",
|
||||
"clustering",
|
||||
"---Operations---",
|
||||
"configuration",
|
||||
"scaling",
|
||||
"webhooks",
|
||||
"---Reference---",
|
||||
"client",
|
||||
"competition"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Durable Queues
|
||||
description: Shared queue system for MQTT and AMQP with consumer groups, acknowledgments, stream queues, and append-only log storage
|
||||
---
|
||||
|
||||
# Durable Queues
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
FluxMQ provides durable queues shared across MQTT, AMQP 1.0, and AMQP 0.9.1. The queue manager is append-only with consumer groups and supports both classic work-queue semantics and stream-style consumption.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Queue topics** use the `$queue/` prefix
|
||||
- **Consumer groups** enable load-balanced processing
|
||||
- **Ack/Nack/Reject** are supported across protocols
|
||||
- **Retention** can be configured per queue (time/size/message count)
|
||||
- **Stream queues** are supported via queue type `stream`
|
||||
- **DLQ handler exists**, but automatic DLQ routing is not wired yet
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌───────────────┐
|
||||
│ MQTT Broker │ │ AMQP Broker │ │ AMQP091 Broker│
|
||||
│ (TCP/WS/ │ │ (AMQP 1.0) │ │ (AMQP 0.9.1) │
|
||||
│ HTTP/CoAP) │ │ │ │ │
|
||||
└──────┬───────┘ └───────┬──────┘ └──────┬────────┘
|
||||
│ │ │
|
||||
└──────────────────┼────────────────┘
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Shared Queue Manager │
|
||||
│ - Topic bindings │
|
||||
│ - Consumer groups │
|
||||
│ - Retention loop │
|
||||
└───────────┬─────────────┘
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Log Storage (AOL) │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
## Queue Addressing
|
||||
|
||||
Queue topics use `$queue/<queue-name>/...`.
|
||||
|
||||
Examples:
|
||||
|
||||
- Publish: `$queue/orders`
|
||||
- Subscribe to a pattern: `$queue/orders/#`
|
||||
- Ack: `$queue/orders/$ack`
|
||||
- Nack: `$queue/orders/$nack`
|
||||
- Reject: `$queue/orders/$reject`
|
||||
|
||||
## Consumer Groups
|
||||
|
||||
- **MQTT v5**: provide `consumer-group` as a user property on SUBSCRIBE
|
||||
- **MQTT v3**: consumer group falls back to client ID (acks require MQTT v5)
|
||||
- **AMQP 1.0**: provide `consumer-group` in attach properties
|
||||
- **AMQP 0.9.1**: provide `x-consumer-group` in `basic.consume`
|
||||
|
||||
## Message Properties
|
||||
|
||||
Queue deliveries include these properties:
|
||||
|
||||
- `message-id` (required for ack/nack/reject)
|
||||
- `group-id` (consumer group name)
|
||||
- `queue` (queue name)
|
||||
- `offset` (sequence number)
|
||||
|
||||
Stream deliveries also include:
|
||||
|
||||
- `x-stream-offset`
|
||||
- `x-stream-timestamp` (unix millis)
|
||||
- `x-work-committed-offset` (if primary group is configured)
|
||||
- `x-work-acked` (true when below committed offset)
|
||||
- `x-work-group` (primary work group name)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
### MQTT
|
||||
|
||||
Ack/Nack/Reject are implemented by publishing to `$queue/<queue>/$ack|$nack|$reject` with MQTT v5 user properties:
|
||||
|
||||
- `message-id`
|
||||
- `group-id`
|
||||
|
||||
MQTT v3 can publish and subscribe to queue topics, but acknowledgments require MQTT v5 user properties.
|
||||
|
||||
### AMQP 1.0
|
||||
|
||||
AMQP dispositions are mapped to queue acknowledgments:
|
||||
|
||||
- Accepted → Ack
|
||||
- Released → Nack
|
||||
- Rejected → Reject
|
||||
|
||||
### AMQP 0.9.1
|
||||
|
||||
- `basic.ack`, `basic.nack`, `basic.reject` map to Ack/Nack/Reject
|
||||
|
||||
#### Stream Commit (AMQP 0.9.1)
|
||||
|
||||
Stream consumers can explicitly commit offsets by publishing to:
|
||||
|
||||
- `$queue/<queue>/$commit`
|
||||
|
||||
Headers:
|
||||
|
||||
- `x-group-id`
|
||||
- `x-offset`
|
||||
|
||||
## Queue Types
|
||||
|
||||
### Classic (Work Queue)
|
||||
|
||||
- Ack/Nack/Reject semantics
|
||||
- Pending entry tracking per consumer group
|
||||
- Redelivery uses visibility timeouts and work stealing
|
||||
- Retry backoff settings are accepted in config but not yet enforced in delivery timing
|
||||
|
||||
### Stream
|
||||
|
||||
- Append-only log semantics
|
||||
- Cursor-based consumption
|
||||
- Optional manual commit
|
||||
|
||||
## Retention
|
||||
|
||||
Retention policies can be configured per queue:
|
||||
|
||||
- `max_age` (time-based)
|
||||
- `max_length_bytes`
|
||||
- `max_length_messages`
|
||||
|
||||
A background retention loop truncates logs to the safe offset based on configured limits.
|
||||
|
||||
## DLQ Status
|
||||
|
||||
A DLQ handler exists in `queue/consumer/dlq.go`, but the main delivery path does not automatically move rejected or expired messages into a DLQ yet. `Reject` currently removes the message from the pending list without pushing it to a DLQ.
|
||||
|
||||
## Configuration
|
||||
|
||||
Queues are configured under `queues` in the main config file:
|
||||
|
||||
```yaml
|
||||
queue_manager:
|
||||
auto_commit_interval: "5s"
|
||||
|
||||
queues:
|
||||
- name: "orders"
|
||||
topics: ["$queue/orders/#"]
|
||||
type: "classic" # classic or stream
|
||||
primary_group: "" # optional for stream status
|
||||
|
||||
limits:
|
||||
max_message_size: 10485760
|
||||
max_depth: 100000
|
||||
message_ttl: "168h"
|
||||
|
||||
retry:
|
||||
max_retries: 10
|
||||
initial_backoff: "5s"
|
||||
max_backoff: "5m"
|
||||
multiplier: 2.0
|
||||
|
||||
dlq:
|
||||
enabled: true
|
||||
topic: "" # optional override
|
||||
|
||||
retention:
|
||||
max_age: "168h"
|
||||
max_length_bytes: 0
|
||||
max_length_messages: 0
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Development Roadmap
|
||||
description: Production-ready MQTT broker development roadmap covering queue architecture, security hardening, performance optimization, and clustering
|
||||
---
|
||||
|
||||
# Roadmap
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
*This project is under heavy development and many important features may evolve and change as development progresses.*
|
||||
|
||||
This roadmap highlights near‑term focus areas. Ordering may change as issues are discovered and priorities shift.
|
||||
|
||||
## Focus Areas
|
||||
|
||||
- Tests
|
||||
- Benchmarks
|
||||
- Optimizations
|
||||
- Architecture revision after the above
|
||||
- Scaling and recovery tests
|
||||
- Performance optimization and code cleanup
|
||||
- Dashboards and a basic UI with metrics
|
||||
- Improved and faster logging and telemetry
|
||||
- Extensive storage tests
|
||||
|
||||
For day‑to‑day progress, track open issues and PRs.
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Scaling & Performance
|
||||
description: Comprehensive scaling guide covering capacity analysis, performance optimizations, benchmarks, topic sharding, and architecture for 100M+ clients
|
||||
---
|
||||
|
||||
# Scaling & Performance
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
Performance and scaling are workload-dependent. Use the benchmark suites in `benchmarks/` and validate on your target hardware and network before making production commitments.
|
||||
|
||||
## Benchmarking
|
||||
|
||||
- See `benchmarks/README.md` for available benchmarks and how to run them.
|
||||
- Run benchmarks on the same class of hardware you plan to deploy.
|
||||
- Capture results with `-benchmem` and keep baselines in version control if needed.
|
||||
|
||||
## Practical Tuning Levers
|
||||
|
||||
- `server.tcp.plain.max_connections`: protect the broker from excess concurrent connections
|
||||
- `session.max_sessions`: cap active MQTT sessions
|
||||
- `broker.max_message_size`: limit payload size
|
||||
- `session.max_offline_queue_size` and `session.max_inflight_messages`: control per-client memory usage
|
||||
- `queues.*.limits`: bound queue depth, message size, and TTL
|
||||
|
||||
## OS and Runtime Considerations
|
||||
|
||||
- Ensure file descriptor limits are high enough for your target connection count.
|
||||
- Tune TCP keepalive and timeouts based on your workload (IoT vs low-latency systems).
|
||||
- Measure with production-like TLS settings if TLS is enabled in production.
|
||||
|
||||
## Cluster Scaling
|
||||
|
||||
- Clustering spreads sessions and subscriptions across nodes but requires etcd coordination.
|
||||
- Use multiple nodes to improve availability and distribute connections.
|
||||
- Queue replication is optional and configured via `cluster.raft`.
|
||||
|
||||
For configuration details, see `docs/configuration.md`.
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Webhook System
|
||||
description: Comprehensive webhook system for asynchronous event notifications with circuit breaker, retry logic, and flexible filtering
|
||||
---
|
||||
|
||||
# Webhook System
|
||||
|
||||
**Last Updated:** 2026-02-05
|
||||
|
||||
FluxMQ can emit broker events to external HTTP endpoints using an asynchronous webhook notifier.
|
||||
|
||||
## Overview
|
||||
|
||||
- Asynchronous event queue with worker pool
|
||||
- Retry with exponential backoff
|
||||
- Circuit breaker per endpoint
|
||||
- Filtering by event type and topic pattern
|
||||
- HTTP sender only (gRPC sender not implemented)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Broker Events
|
||||
│
|
||||
▼
|
||||
Webhook Notifier (queue)
|
||||
│ drop_policy: oldest/newest
|
||||
▼
|
||||
Worker Pool
|
||||
│ retry + circuit breaker
|
||||
▼
|
||||
HTTP Sender
|
||||
│
|
||||
▼
|
||||
External Endpoints
|
||||
```
|
||||
|
||||
## Event Types
|
||||
|
||||
Events are defined in `broker/events/events.go`.
|
||||
|
||||
- `client.connected`: `client_id`, `protocol`, `clean_start`, `keep_alive`, `remote_addr`
|
||||
- `client.disconnected`: `client_id`, `reason`, `remote_addr`
|
||||
- `client.session_takeover`: `client_id`, `from_node`, `to_node`
|
||||
- `message.published`: `client_id`, `topic`, `qos`, `retained`, `payload_size`, `payload`
|
||||
- `message.delivered`: `client_id`, `topic`, `qos`, `payload_size`
|
||||
- `message.retained`: `topic`, `payload_size`, `cleared`
|
||||
- `subscription.created`: `client_id`, `topic_filter`, `qos`, `subscription_id`
|
||||
- `subscription.removed`: `client_id`, `topic_filter`
|
||||
- `auth.success`: `client_id`, `remote_addr`
|
||||
- `auth.failure`: `client_id`, `reason`, `remote_addr`
|
||||
- `authz.publish_denied`: `client_id`, `topic`, `reason`
|
||||
- `authz.subscribe_denied`: `client_id`, `topic_filter`, `reason`
|
||||
|
||||
### Payload Notes
|
||||
|
||||
The `message.published` payload field is defined in the event schema (base64-encoded when populated), but payload inclusion is not currently wired in the broker. `webhook.include_payload` is accepted in config, yet payloads are sent as empty strings at the moment.
|
||||
|
||||
## Event Envelope
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "message.published",
|
||||
"event_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"timestamp": "2026-02-05T12:00:00Z",
|
||||
"broker_id": "broker-1",
|
||||
"data": {
|
||||
"client_id": "publisher-1",
|
||||
"topic": "sensors/temperature",
|
||||
"qos": 1,
|
||||
"retained": false,
|
||||
"payload_size": 256
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Filtering
|
||||
|
||||
Each endpoint can filter by:
|
||||
|
||||
- `events`: list of event types
|
||||
- `topic_filters`: MQTT-style patterns (supports `+` and `#`)
|
||||
|
||||
## Retry and Circuit Breaker
|
||||
|
||||
- Retries use exponential backoff (`initial_interval * multiplier^attempt`), capped by `max_interval`
|
||||
- Circuit breaker is per endpoint and trips after `failure_threshold` consecutive failures
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
webhook:
|
||||
enabled: true
|
||||
queue_size: 10000
|
||||
drop_policy: "oldest" # oldest or newest
|
||||
workers: 5
|
||||
include_payload: false
|
||||
shutdown_timeout: "30s"
|
||||
|
||||
defaults:
|
||||
timeout: "5s"
|
||||
retry:
|
||||
max_attempts: 3
|
||||
initial_interval: "1s"
|
||||
max_interval: "30s"
|
||||
multiplier: 2.0
|
||||
circuit_breaker:
|
||||
failure_threshold: 5
|
||||
reset_timeout: "60s"
|
||||
|
||||
endpoints:
|
||||
- name: "analytics"
|
||||
type: "http"
|
||||
url: "https://example.com/webhook"
|
||||
events: ["message.published"]
|
||||
topic_filters: ["sensors/#"]
|
||||
headers:
|
||||
Authorization: "Bearer token"
|
||||
```
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
@@ -0,0 +1,230 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import '../styles/fumadocs-search.css';
|
||||
import HomeSearchBridge from '@/components/HomeSearchBridge';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'FluxMQ - High-Performance Multi-Protocol Message Broker',
|
||||
description =
|
||||
'FluxMQ is a high-performance, multi-protocol message broker written in Go. Supports MQTT, WebSocket, HTTP and CoAP with durable queues and clustering.',
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content="/og-image.png" />
|
||||
<link rel="icon" href="/logo.png" />
|
||||
<link rel="canonical" href="https://absmach.eu/fluxmq/" />
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const THEME_KEY = 'theme';
|
||||
const stored = localStorage.getItem(THEME_KEY);
|
||||
const mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
|
||||
const preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
const resolved = mode === 'system' ? preferred : mode;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
document.documentElement.classList.toggle('dark', resolved === 'dark');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="flex min-h-screen flex-col bg-theme text-[var(--flux-text)]">
|
||||
<HomeSearchBridge client:only="react" />
|
||||
|
||||
<header class="site-header z-50">
|
||||
<nav class="mx-auto flex min-h-[64px] w-[min(100%-2.5rem,1360px)] items-center gap-4" aria-label="Main navigation">
|
||||
<a class="animate-fade-in text-2xl font-bold md:text-[2.45rem]" style="line-height:1.1" href="/" aria-label="FluxMQ home">
|
||||
<span class="text-[var(--flux-blue)]">Flux</span><span class="text-[var(--flux-orange)]">MQ</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
class="inline-flex rounded-md border border-[var(--flux-pill-border)] bg-[var(--flux-pill-bg)] px-3 py-2 text-sm font-semibold text-[var(--flux-text)] md:hidden"
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
aria-controls="site-menu"
|
||||
data-menu-toggle
|
||||
>
|
||||
Menu
|
||||
</button>
|
||||
|
||||
<ul
|
||||
id="site-menu"
|
||||
class="absolute left-0 right-0 top-[63px] hidden flex-col items-start gap-4 border-b border-[var(--flux-header-border)] bg-[var(--flux-header-bg)] px-5 py-4 text-[0.95rem] font-normal md:static md:top-0 md:flex md:min-w-0 md:flex-1 md:flex-row md:items-center md:gap-7 md:border-0 md:bg-transparent md:p-0 md:pl-8"
|
||||
data-menu
|
||||
>
|
||||
<li><a class="nav-link" href="/#features">Features</a></li>
|
||||
<li><a class="nav-link" href="/#performance">Performance</a></li>
|
||||
<li><a class="nav-link" href="/#use-cases">Use Cases</a></li>
|
||||
<li><a class="nav-link" href="/#architecture">Architecture</a></li>
|
||||
<li><a class="nav-link" href="/#quick-start">Quick Start</a></li>
|
||||
<li><a class="nav-link" href="/docs/">Documentation</a></li>
|
||||
<li class="md:hidden"><a class="nav-link" href="https://github.com/absmach/fluxmq" target="_blank" rel="noopener noreferrer">GitHub</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="ml-auto hidden items-center gap-2 md:flex">
|
||||
<button type="button" class="nav-search-pill" aria-label="Search docs" data-search-trigger>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="5.5" stroke="currentColor" stroke-width="1.6"></circle>
|
||||
<path d="M13 13L17 17" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
<span>Search</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<kbd class="kbd-chip" data-shortcut-mod>Ctrl</kbd>
|
||||
<kbd class="kbd-chip">K</kbd>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="theme-toggle-pill" aria-label="Toggle theme" data-theme-toggle>
|
||||
<span class="theme-dot" data-theme-sun aria-hidden="true">
|
||||
<svg class="size-3.5" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="10" r="3.2" stroke="currentColor" stroke-width="1.6"></circle>
|
||||
<path d="M10 2v2.2M10 15.8V18M2 10h2.2M15.8 10H18M4.6 4.6l1.5 1.5M13.9 13.9l1.5 1.5M15.4 4.6l-1.5 1.5M6.1 13.9l-1.5 1.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="theme-dot" data-theme-moon aria-hidden="true">
|
||||
<svg class="size-3.5" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M13.8 13.9A6.2 6.2 0 0 1 6 6.2a6.7 6.7 0 1 0 7.8 7.7Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<a
|
||||
class="nav-icon-link"
|
||||
href="https://github.com/absmach/fluxmq"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="FluxMQ GitHub repository"
|
||||
>
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 .5C5.65.5.5 5.66.5 12.03c0 5.1 3.3 9.43 7.88 10.96.58.11.79-.25.79-.57 0-.28-.01-1.04-.02-2.04-3.2.7-3.88-1.55-3.88-1.55-.53-1.35-1.28-1.7-1.28-1.7-1.05-.72.08-.71.08-.71 1.15.08 1.76 1.2 1.76 1.2 1.03 1.76 2.7 1.25 3.37.96.1-.75.4-1.25.72-1.54-2.56-.29-5.26-1.29-5.26-5.74 0-1.27.46-2.3 1.2-3.11-.12-.29-.52-1.46.12-3.04 0 0 .98-.32 3.2 1.19a11.1 11.1 0 0 1 5.82 0c2.22-1.51 3.2-1.19 3.2-1.19.64 1.58.24 2.75.12 3.04.75.81 1.2 1.84 1.2 3.11 0 4.46-2.7 5.44-5.28 5.73.42.36.78 1.06.78 2.14 0 1.54-.01 2.78-.01 3.16 0 .31.21.68.8.56A11.54 11.54 0 0 0 23.5 12.03C23.5 5.66 18.35.5 12 .5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2 md:hidden">
|
||||
<button type="button" class="theme-toggle-pill" aria-label="Toggle theme" data-theme-toggle>
|
||||
<span class="theme-dot" data-theme-sun aria-hidden="true">
|
||||
<svg class="size-3.5" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="10" r="3.2" stroke="currentColor" stroke-width="1.6"></circle>
|
||||
<path d="M10 2v2.2M10 15.8V18M2 10h2.2M15.8 10H18M4.6 4.6l1.5 1.5M13.9 13.9l1.5 1.5M15.4 4.6l-1.5 1.5M6.1 13.9l-1.5 1.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="theme-dot" data-theme-moon aria-hidden="true">
|
||||
<svg class="size-3.5" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M13.8 13.9A6.2 6.2 0 0 1 6 6.2a6.7 6.7 0 1 0 7.8 7.7Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<a
|
||||
class="nav-icon-link"
|
||||
href="https://github.com/absmach/fluxmq"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="FluxMQ GitHub repository"
|
||||
>
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 .5C5.65.5.5 5.66.5 12.03c0 5.1 3.3 9.43 7.88 10.96.58.11.79-.25.79-.57 0-.28-.01-1.04-.02-2.04-3.2.7-3.88-1.55-3.88-1.55-.53-1.35-1.28-1.7-1.28-1.7-1.05-.72.08-.71.08-.71 1.15.08 1.76 1.2 1.76 1.2 1.03 1.76 2.7 1.25 3.37.96.1-.75.4-1.25.72-1.54-2.56-.29-5.26-1.29-5.26-5.74 0-1.27.46-2.3 1.2-3.11-.12-.29-.52-1.46.12-3.04 0 0 .98-.32 3.2 1.19a11.1 11.1 0 0 1 5.82 0c2.22-1.51 3.2-1.19 3.2-1.19.64 1.58.24 2.75.12 3.04.75.81 1.2 1.84 1.2 3.11 0 4.46-2.7 5.44-5.28 5.73.42.36.78 1.06.78 2.14 0 1.54-.01 2.78-.01 3.16 0 .31.21.68.8.56A11.54 11.54 0 0 0 23.5 12.03C23.5 5.66 18.35.5 12 .5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<script is:inline>
|
||||
const THEME_KEY = 'theme';
|
||||
|
||||
const setTheme = (mode) => {
|
||||
const preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
const resolved = mode === 'system' ? preferred : mode;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
document.documentElement.classList.toggle('dark', resolved === 'dark');
|
||||
localStorage.setItem(THEME_KEY, mode);
|
||||
updateThemeIcons(resolved);
|
||||
};
|
||||
|
||||
const updateThemeIcons = (resolved) => {
|
||||
document.querySelectorAll('[data-theme-sun]').forEach((sun) => {
|
||||
sun.classList.toggle('theme-dot-active', resolved === 'light');
|
||||
});
|
||||
document.querySelectorAll('[data-theme-moon]').forEach((moon) => {
|
||||
moon.classList.toggle('theme-dot-active', resolved === 'dark');
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach((toggleTheme) => {
|
||||
const current = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
|
||||
updateThemeIcons(current);
|
||||
|
||||
toggleTheme.addEventListener('click', () => {
|
||||
const active = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
|
||||
const next = active === 'dark' ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
});
|
||||
});
|
||||
|
||||
const openSearch = () => {
|
||||
if (typeof window.__fluxmqOpenSearch === 'function') {
|
||||
window.__fluxmqOpenSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('fluxmq:open-search'));
|
||||
};
|
||||
|
||||
const searchTrigger = document.querySelector('[data-search-trigger]');
|
||||
if (searchTrigger) {
|
||||
searchTrigger.addEventListener('click', openSearch);
|
||||
}
|
||||
|
||||
const shortcutMod = document.querySelector('[data-shortcut-mod]');
|
||||
if (shortcutMod && /(Mac|iPhone|iPad|iPod)/i.test(navigator.userAgent)) {
|
||||
shortcutMod.textContent = '⌘';
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
|
||||
event.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
});
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
|
||||
const stored = localStorage.getItem(THEME_KEY);
|
||||
if (!stored || stored === 'system') setTheme('system');
|
||||
});
|
||||
|
||||
const toggle = document.querySelector('[data-menu-toggle]');
|
||||
const menu = document.querySelector('[data-menu]');
|
||||
if (toggle && menu) {
|
||||
toggle.addEventListener('click', () => {
|
||||
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
||||
toggle.setAttribute('aria-expanded', String(!expanded));
|
||||
menu.classList.toggle('hidden', expanded);
|
||||
menu.classList.toggle('flex', !expanded);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Source } from 'fumadocs-core/source';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import * as path from 'node:path';
|
||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
export const source = loader({
|
||||
source: await createSource(),
|
||||
baseUrl: '/docs',
|
||||
});
|
||||
|
||||
const docs = import.meta.glob('/src/content/docs/docs/**/*.{md,mdx}');
|
||||
|
||||
export async function getFullExport(entry: CollectionEntry<'docs'>) {
|
||||
return (await docs['/' + entry.filePath!]()) as {
|
||||
structuredData: StructuredData;
|
||||
};
|
||||
}
|
||||
|
||||
async function createSource() {
|
||||
const out: Source<{
|
||||
metaData: CollectionEntry<'meta'>['data'];
|
||||
pageData: CollectionEntry<'docs'>['data'] & {
|
||||
_raw: CollectionEntry<'docs'>;
|
||||
};
|
||||
}> = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
for (const page of await getCollection('docs')) {
|
||||
const virtualPath = path.relative('src/content/docs/docs', page.filePath!);
|
||||
|
||||
out.files.push({
|
||||
type: 'page',
|
||||
path: virtualPath,
|
||||
data: {
|
||||
...page.data,
|
||||
_raw: page,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const meta of await getCollection('meta')) {
|
||||
const virtualPath = path.relative('src/content/docs/docs', meta.filePath!);
|
||||
|
||||
out.files.push({
|
||||
type: 'meta',
|
||||
path: virtualPath,
|
||||
data: meta.data,
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Page Not Found | FluxMQ" description="The page you requested was not found.">
|
||||
<section class="border-b-2 border-[var(--flux-border)] py-24">
|
||||
<div class="mx-auto w-[min(100%-2.5rem,900px)]">
|
||||
<h1 class="text-4xl font-extrabold md:text-6xl">404</h1>
|
||||
<p class="mt-4 text-lg text-[var(--flux-muted)]">The page you requested could not be found.</p>
|
||||
<div class="mt-8 flex flex-wrap gap-3">
|
||||
<a
|
||||
class="inline-block border-2 border-[var(--flux-border)] px-4 py-3 text-sm font-bold uppercase tracking-[0.02em] transition hover:bg-[var(--flux-blue)] hover:text-white"
|
||||
href="/"
|
||||
>
|
||||
Go to Home
|
||||
</a>
|
||||
<a
|
||||
class="inline-block border-2 border-[var(--flux-border)] px-4 py-3 text-sm font-bold uppercase tracking-[0.02em] transition hover:bg-[var(--flux-orange)] hover:text-black"
|
||||
href="/docs/"
|
||||
>
|
||||
Open Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
import { getFullExport, source } from '@/lib/source';
|
||||
import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb';
|
||||
|
||||
const server = createFromSource(source, {
|
||||
async buildIndex(page) {
|
||||
const exported = await getFullExport(page.data._raw);
|
||||
const structuredData = exported.structuredData ?? { headings: [], contents: [] };
|
||||
|
||||
return {
|
||||
id: page.data._raw.id,
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
structuredData,
|
||||
url: page.url,
|
||||
breadcrumbs: getBreadcrumbItems(page.url, source.getPageTree()).map((item) =>
|
||||
String(item.name),
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
return server.staticGET();
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
import { render, type CollectionEntry } from 'astro:content';
|
||||
import { Docs } from '@/components/docs';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import '@/styles/docs.css';
|
||||
import { source } from '@/lib/source';
|
||||
import Layout from '@/components/layout.astro';
|
||||
import type { TOCItemType } from 'fumadocs-core/toc';
|
||||
|
||||
interface Props {
|
||||
page: CollectionEntry<'docs'>;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return source.getPages().map((page) => ({
|
||||
params: { slug: page.slugs.length > 0 ? page.slugs.join('/') : undefined },
|
||||
props: { page: page.data._raw },
|
||||
}));
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
const { Content, headings } = await render(page);
|
||||
|
||||
const toc: TOCItemType[] = headings.map((heading) => ({
|
||||
depth: heading.depth,
|
||||
title: heading.text,
|
||||
url: `#${heading.slug}`,
|
||||
}));
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<title>{page.data.title}</title>
|
||||
<meta name="title" content={page.data.title} />
|
||||
<meta name="description" content={page.data.description} />
|
||||
<Docs
|
||||
tree={source.getPageTree()}
|
||||
pathname={Astro.url.pathname}
|
||||
params={Astro.params as Record<string, string | string[]>}
|
||||
page={{ toc }}
|
||||
client:load
|
||||
>
|
||||
<h1 class="text-3xl font-semibold">{page.data.title}</h1>
|
||||
<p class="mb-8 text-lg text-fd-muted-foreground">{page.data.description}</p>
|
||||
<div class="prose flex-1">
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</div>
|
||||
</Docs>
|
||||
</Layout>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import ArchitectureSection from '../components/home/ArchitectureSection.astro';
|
||||
import FeaturesSection from '../components/home/FeaturesSection.astro';
|
||||
import FooterSection from '../components/home/FooterSection.astro';
|
||||
import HeroSection from '../components/home/HeroSection.astro';
|
||||
import NewsletterSection from '../components/home/NewsletterSection.astro';
|
||||
import PerformanceSection from '../components/home/PerformanceSection.astro';
|
||||
import QuickStartSection from '../components/home/QuickStartSection.astro';
|
||||
import UseCasesSection from '../components/home/UseCasesSection.astro';
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<HeroSection />
|
||||
<FeaturesSection />
|
||||
<PerformanceSection />
|
||||
<UseCasesSection />
|
||||
<ArchitectureSection />
|
||||
<QuickStartSection />
|
||||
<NewsletterSection />
|
||||
<FooterSection />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,268 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
|
||||
:root {
|
||||
--flux-blue: hsl(213.64deg 58.41% 44.31%);
|
||||
--flux-orange: hsl(35.07deg 94.52% 57.06%);
|
||||
--flux-bg: hsl(0 0% 100%);
|
||||
--flux-fg: hsl(0 0% 10%);
|
||||
--flux-border: hsl(0 0% 20%);
|
||||
--flux-grid: hsl(0 0% 90%);
|
||||
--flux-text: hsl(0 0% 10%);
|
||||
--flux-text-muted: hsl(0 0% 40%);
|
||||
--flux-bg-alt: hsl(0 0% 97%);
|
||||
|
||||
--color-fd-background: var(--flux-bg);
|
||||
--color-fd-foreground: var(--flux-text);
|
||||
--color-fd-muted: color-mix(in srgb, var(--flux-bg) 92%, var(--flux-fg));
|
||||
--color-fd-muted-foreground: var(--flux-text-muted);
|
||||
--color-fd-popover: color-mix(in srgb, var(--flux-bg) 96%, var(--flux-fg));
|
||||
--color-fd-popover-foreground: var(--flux-text);
|
||||
--color-fd-card: color-mix(in srgb, var(--flux-bg) 98%, var(--flux-fg));
|
||||
--color-fd-card-foreground: var(--flux-text);
|
||||
--color-fd-border: color-mix(in srgb, var(--flux-border) 35%, transparent);
|
||||
--color-fd-primary: var(--flux-blue);
|
||||
--color-fd-primary-foreground: var(--flux-bg);
|
||||
--color-fd-secondary: color-mix(in srgb, var(--flux-bg) 90%, var(--flux-fg));
|
||||
--color-fd-secondary-foreground: var(--flux-text);
|
||||
--color-fd-accent: color-mix(in srgb, var(--flux-blue) 14%, transparent);
|
||||
--color-fd-accent-foreground: var(--flux-text);
|
||||
--color-fd-ring: color-mix(in srgb, var(--flux-orange) 60%, transparent);
|
||||
}
|
||||
|
||||
:root[class~='dark'],
|
||||
:root[data-theme='dark'] {
|
||||
--flux-blue: hsl(213.64deg 70% 60%);
|
||||
--flux-orange: hsl(35.07deg 94.52% 57.06%);
|
||||
--flux-bg: hsl(0 0% 10%);
|
||||
--flux-fg: hsl(0 0% 95%);
|
||||
--flux-border: hsl(0 0% 30%);
|
||||
--flux-grid: hsl(0 0% 20%);
|
||||
--flux-text: hsl(0 0% 95%);
|
||||
--flux-text-muted: hsl(0 0% 60%);
|
||||
--flux-bg-alt: hsl(0 0% 8%);
|
||||
|
||||
--color-fd-background: var(--flux-bg);
|
||||
--color-fd-foreground: var(--flux-text);
|
||||
--color-fd-muted: color-mix(in srgb, var(--flux-bg) 82%, var(--flux-fg));
|
||||
--color-fd-muted-foreground: var(--flux-text-muted);
|
||||
--color-fd-popover: color-mix(in srgb, var(--flux-bg) 90%, black);
|
||||
--color-fd-popover-foreground: var(--flux-text);
|
||||
--color-fd-card: color-mix(in srgb, var(--flux-bg) 88%, black);
|
||||
--color-fd-card-foreground: var(--flux-text);
|
||||
--color-fd-border: color-mix(in srgb, var(--flux-border) 62%, transparent);
|
||||
--color-fd-primary: var(--flux-orange);
|
||||
--color-fd-primary-foreground: hsl(0 0% 10%);
|
||||
--color-fd-secondary: color-mix(in srgb, var(--flux-bg) 78%, var(--flux-fg));
|
||||
--color-fd-secondary-foreground: var(--flux-text);
|
||||
--color-fd-accent: color-mix(in srgb, var(--flux-blue) 23%, transparent);
|
||||
--color-fd-accent-foreground: var(--flux-text);
|
||||
--color-fd-ring: color-mix(in srgb, var(--flux-orange) 70%, transparent);
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Verdana, Geneva, sans-serif;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.grid-pattern {
|
||||
background-image:
|
||||
linear-gradient(to right, var(--flux-grid) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--flux-grid) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.brutalist-border {
|
||||
border: 2px solid var(--flux-border);
|
||||
}
|
||||
|
||||
.brutalist-card {
|
||||
border: 2px solid var(--flux-border);
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.brutalist-card:hover {
|
||||
border-color: var(--flux-orange);
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background: hsl(0 0% 10%);
|
||||
color: hsl(120 100% 80%);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
border: 2px solid var(--flux-border);
|
||||
}
|
||||
|
||||
:root[class~='dark'] .terminal,
|
||||
:root[data-theme='dark'] .terminal {
|
||||
background: hsl(0 0% 5%);
|
||||
border-color: var(--flux-border);
|
||||
}
|
||||
|
||||
.accent-line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.accent-line::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--flux-orange);
|
||||
}
|
||||
|
||||
.metrics-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.metrics-table th,
|
||||
.metrics-table td {
|
||||
border: 1px solid var(--flux-border);
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.metrics-table th {
|
||||
background: var(--flux-fg);
|
||||
color: var(--flux-bg);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.metrics-table tbody {
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.bg-theme {
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.bg-theme-alt {
|
||||
background: var(--flux-bg-alt);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.bg-theme-inverse {
|
||||
background: var(--flux-fg);
|
||||
color: var(--flux-bg);
|
||||
}
|
||||
|
||||
.text-theme {
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.text-theme-muted {
|
||||
color: var(--flux-text-muted);
|
||||
}
|
||||
|
||||
.border-theme {
|
||||
border-color: var(--flux-border);
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--flux-orange);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.8s ease-out;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
|
||||
.animate-slide-up:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.animate-slide-up:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-fade-in,
|
||||
.animate-slide-up {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
#nd-sidebar [data-active='true'] {
|
||||
color: var(--flux-orange) !important;
|
||||
background-color: rgb(249 163 42 / 10%) !important;
|
||||
border-left: 3px solid var(--flux-orange) !important;
|
||||
}
|
||||
|
||||
#nd-sidebar a:hover {
|
||||
color: var(--flux-blue) !important;
|
||||
}
|
||||
|
||||
#nd-sidebar [data-search-full] {
|
||||
border-color: color-mix(in srgb, var(--flux-border) 35%, transparent);
|
||||
background: color-mix(in srgb, var(--flux-bg) 88%, var(--flux-fg));
|
||||
}
|
||||
|
||||
#nd-sidebar [data-search-full] kbd {
|
||||
border-color: color-mix(in srgb, var(--flux-border) 32%, transparent);
|
||||
background: var(--flux-bg);
|
||||
}
|
||||
|
||||
#nd-sidebar > div:last-child {
|
||||
border-top-color: color-mix(in srgb, var(--flux-border) 30%, transparent);
|
||||
}
|
||||
|
||||
#nd-toc {
|
||||
border-left: 1px solid color-mix(in srgb, var(--flux-border) 24%, transparent);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
#nd-docs-layout {
|
||||
--fd-sidebar-width: 312px;
|
||||
--fd-toc-width: 272px;
|
||||
}
|
||||
|
||||
#nd-subnav {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
@@ -0,0 +1,341 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
:root {
|
||||
--flux-blue: hsl(213.64deg 58.41% 44.31%);
|
||||
--flux-orange: hsl(35.07deg 94.52% 57.06%);
|
||||
--flux-bg: hsl(0 0% 100%);
|
||||
--flux-fg: hsl(0 0% 10%);
|
||||
--flux-border: hsl(0 0% 20%);
|
||||
--flux-grid: hsl(0 0% 90%);
|
||||
--flux-text: hsl(0 0% 10%);
|
||||
--flux-text-muted: hsl(0 0% 40%);
|
||||
--flux-bg-alt: hsl(0 0% 97%);
|
||||
--flux-muted: var(--flux-text-muted);
|
||||
--flux-header-bg: hsl(0 0% 97%);
|
||||
--flux-header-border: hsl(0 0% 85%);
|
||||
--flux-pill-bg: hsl(0 0% 96%);
|
||||
--flux-pill-border: hsl(0 0% 82%);
|
||||
--flux-pill-text: hsl(0 0% 45%);
|
||||
--flux-icon: hsl(0 0% 15%);
|
||||
--flux-focus-ring: color-mix(in srgb, var(--flux-orange) 65%, transparent);
|
||||
}
|
||||
|
||||
:root[class~='dark'],
|
||||
:root[data-theme='dark'] {
|
||||
--flux-blue: hsl(213.64deg 70% 60%);
|
||||
--flux-orange: hsl(35.07deg 94.52% 57.06%);
|
||||
--flux-bg: hsl(0 0% 10%);
|
||||
--flux-fg: hsl(0 0% 95%);
|
||||
--flux-border: hsl(0 0% 30%);
|
||||
--flux-grid: hsl(0 0% 20%);
|
||||
--flux-text: hsl(0 0% 95%);
|
||||
--flux-text-muted: hsl(0 0% 60%);
|
||||
--flux-bg-alt: hsl(0 0% 8%);
|
||||
--flux-muted: var(--flux-text-muted);
|
||||
--flux-header-bg: hsl(0 0% 8%);
|
||||
--flux-header-border: hsl(0 0% 18%);
|
||||
--flux-pill-bg: hsl(0 0% 10%);
|
||||
--flux-pill-border: hsl(0 0% 20%);
|
||||
--flux-pill-text: hsl(0 0% 70%);
|
||||
--flux-icon: hsl(0 0% 92%);
|
||||
--flux-focus-ring: color-mix(in srgb, var(--flux-orange) 70%, transparent);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Verdana, Geneva, sans-serif;
|
||||
color: var(--flux-text);
|
||||
background: var(--flux-bg);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.grid-pattern {
|
||||
background-image:
|
||||
linear-gradient(to right, var(--flux-grid) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--flux-grid) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.brutalist-border {
|
||||
border: 2px solid var(--flux-border);
|
||||
}
|
||||
|
||||
.brutalist-card {
|
||||
border: 2px solid var(--flux-border);
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.brutalist-card:hover {
|
||||
border-color: var(--flux-orange);
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background: hsl(0 0% 10%);
|
||||
color: hsl(120 100% 80%);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
border: 2px solid var(--flux-border);
|
||||
}
|
||||
|
||||
:root[class~='dark'] .terminal,
|
||||
:root[data-theme='dark'] .terminal {
|
||||
background: hsl(0 0% 5%);
|
||||
}
|
||||
|
||||
.accent-line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.accent-line::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--flux-orange);
|
||||
}
|
||||
|
||||
.metrics-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.metrics-table th,
|
||||
.metrics-table td {
|
||||
border: 1px solid var(--flux-border);
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.metrics-table th {
|
||||
background: var(--flux-fg);
|
||||
color: var(--flux-bg);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metrics-table tbody {
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.bg-theme {
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.bg-theme-alt {
|
||||
background: var(--flux-bg-alt);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.text-theme-muted {
|
||||
color: var(--flux-text-muted);
|
||||
}
|
||||
|
||||
.border-theme {
|
||||
border-color: var(--flux-border);
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.site-header {
|
||||
border-top: 6px solid hsl(0 0% 22%);
|
||||
border-bottom: 1px solid var(--flux-header-border);
|
||||
background: var(--flux-header-bg);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@apply inline-flex text-[1.03rem] font-medium transition-colors;
|
||||
color: color-mix(in srgb, var(--flux-text) 65%, transparent);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.nav-search-pill {
|
||||
@apply inline-flex min-w-[252px] items-center justify-between rounded-full border px-4 py-2 text-[1.05rem] leading-none transition;
|
||||
border-color: var(--flux-pill-border);
|
||||
background: var(--flux-pill-bg);
|
||||
color: var(--flux-pill-text);
|
||||
}
|
||||
|
||||
.nav-search-pill:hover {
|
||||
border-color: color-mix(in srgb, var(--flux-blue) 35%, var(--flux-pill-border));
|
||||
color: color-mix(in srgb, var(--flux-text) 75%, transparent);
|
||||
}
|
||||
|
||||
.kbd-chip {
|
||||
@apply inline-flex items-center justify-center rounded-md border px-1.5 py-[0.14rem] text-[0.72rem] font-semibold;
|
||||
border-color: var(--flux-pill-border);
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-pill-text);
|
||||
}
|
||||
|
||||
.theme-toggle-pill {
|
||||
@apply inline-flex items-center gap-0.5 rounded-full border p-[0.2rem];
|
||||
border-color: var(--flux-pill-border);
|
||||
background: var(--flux-pill-bg);
|
||||
}
|
||||
|
||||
.theme-toggle-pill:hover {
|
||||
border-color: color-mix(in srgb, var(--flux-orange) 45%, var(--flux-pill-border));
|
||||
}
|
||||
|
||||
.theme-dot {
|
||||
@apply inline-flex size-6 items-center justify-center rounded-full transition-colors;
|
||||
color: var(--flux-pill-text);
|
||||
}
|
||||
|
||||
.theme-dot-active {
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-icon);
|
||||
box-shadow: 0 0 0 1px var(--flux-pill-border);
|
||||
}
|
||||
|
||||
.nav-icon-link {
|
||||
@apply inline-flex items-center justify-center rounded-full p-1 transition-colors;
|
||||
color: var(--flux-icon);
|
||||
}
|
||||
|
||||
.nav-icon-link:hover {
|
||||
color: var(--flux-orange);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
@apply border-2 bg-[var(--flux-bg)] p-5;
|
||||
border-color: var(--flux-border);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: var(--flux-orange);
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
@apply mb-3 text-xl font-bold;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: var(--flux-text-muted);
|
||||
}
|
||||
|
||||
.bullet-list {
|
||||
list-style-type: square;
|
||||
@apply space-y-1.5 pl-5;
|
||||
color: var(--flux-text-muted);
|
||||
}
|
||||
|
||||
.cluster-box {
|
||||
@apply border-2 p-5;
|
||||
border-color: var(--flux-border);
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.metric-head {
|
||||
@apply border px-3 py-3 text-left text-sm font-bold uppercase md:text-base;
|
||||
border-color: var(--flux-border);
|
||||
background: var(--flux-fg);
|
||||
color: var(--flux-bg);
|
||||
}
|
||||
|
||||
.metric-cell {
|
||||
@apply border px-3 py-3 text-sm md:text-base;
|
||||
border-color: var(--flux-border);
|
||||
}
|
||||
|
||||
.code-panel {
|
||||
@apply relative border-2;
|
||||
border-color: var(--flux-border);
|
||||
background: hsl(0 0% 10%);
|
||||
color: hsl(120 100% 80%);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.code-panel pre {
|
||||
@apply m-0 overflow-x-auto p-4;
|
||||
}
|
||||
|
||||
.code-panel code {
|
||||
@apply text-[0.95rem] whitespace-pre;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.network-dash line {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 8 10;
|
||||
animation: network-dash 10s linear infinite;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.8s ease-out;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
|
||||
.animate-slide-up:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.animate-slide-up:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.network-dash line,
|
||||
.animate-fade-in,
|
||||
.animate-slide-up {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes network-dash {
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: -420;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--flux-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
:root,
|
||||
:root[data-theme='light'] {
|
||||
--flux-blue: hsl(213.64deg 58.41% 44.31%);
|
||||
--flux-orange: hsl(35.07deg 94.52% 57.06%);
|
||||
--flux-bg: hsl(0 0% 100%);
|
||||
--flux-fg: hsl(0 0% 10%);
|
||||
--flux-border: hsl(0 0% 20%);
|
||||
--flux-text: hsl(0 0% 10%);
|
||||
--flux-text-muted: hsl(0 0% 40%);
|
||||
--flux-bg-alt: hsl(0 0% 97%);
|
||||
|
||||
--sl-font: Verdana, Geneva, sans-serif;
|
||||
--sl-font-mono: 'JetBrains Mono', 'Courier New', monospace;
|
||||
|
||||
--sl-color-accent-low: color-mix(in srgb, var(--flux-blue) 18%, white);
|
||||
--sl-color-accent: var(--flux-blue);
|
||||
--sl-color-accent-high: color-mix(in srgb, var(--flux-blue) 70%, black);
|
||||
|
||||
--sl-color-text: var(--flux-text);
|
||||
--sl-color-text-accent: var(--flux-blue);
|
||||
--sl-color-text-invert: var(--flux-bg);
|
||||
--sl-color-bg: var(--flux-bg);
|
||||
--sl-color-bg-nav: var(--flux-bg);
|
||||
--sl-color-bg-sidebar: var(--flux-bg);
|
||||
--sl-color-bg-inline-code: color-mix(in srgb, var(--flux-fg) 8%, white);
|
||||
--sl-color-bg-accent: var(--flux-blue);
|
||||
--sl-color-hairline-light: color-mix(in srgb, var(--flux-border) 30%, white);
|
||||
--sl-color-hairline: color-mix(in srgb, var(--flux-border) 45%, white);
|
||||
--sl-color-hairline-shade: color-mix(in srgb, var(--flux-border) 60%, white);
|
||||
--sl-sidebar-width: 21rem;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--flux-blue: hsl(213.64deg 70% 60%);
|
||||
--flux-orange: hsl(35.07deg 94.52% 57.06%);
|
||||
--flux-bg: hsl(0 0% 10%);
|
||||
--flux-fg: hsl(0 0% 95%);
|
||||
--flux-border: hsl(0 0% 30%);
|
||||
--flux-text: hsl(0 0% 95%);
|
||||
--flux-text-muted: hsl(0 0% 60%);
|
||||
--flux-bg-alt: hsl(0 0% 8%);
|
||||
|
||||
--sl-font: Verdana, Geneva, sans-serif;
|
||||
--sl-font-mono: 'JetBrains Mono', 'Courier New', monospace;
|
||||
|
||||
--sl-color-accent-low: color-mix(in srgb, var(--flux-blue) 28%, black);
|
||||
--sl-color-accent: var(--flux-blue);
|
||||
--sl-color-accent-high: color-mix(in srgb, var(--flux-blue) 70%, white);
|
||||
|
||||
--sl-color-text: var(--flux-text);
|
||||
--sl-color-text-accent: var(--flux-blue);
|
||||
--sl-color-text-invert: var(--flux-bg);
|
||||
--sl-color-bg: var(--flux-bg);
|
||||
--sl-color-bg-nav: var(--flux-bg);
|
||||
--sl-color-bg-sidebar: var(--flux-bg-alt);
|
||||
--sl-color-bg-inline-code: color-mix(in srgb, var(--flux-fg) 12%, black);
|
||||
--sl-color-bg-accent: var(--flux-blue);
|
||||
--sl-color-hairline-light: color-mix(in srgb, var(--flux-border) 60%, black);
|
||||
--sl-color-hairline: color-mix(in srgb, var(--flux-border) 70%, black);
|
||||
--sl-color-hairline-shade: color-mix(in srgb, var(--flux-border) 80%, black);
|
||||
--sl-sidebar-width: 21rem;
|
||||
}
|
||||
|
||||
.page > .header {
|
||||
border-bottom: 2px solid var(--flux-border);
|
||||
}
|
||||
|
||||
.sidebar-pane {
|
||||
border-right: 2px solid color-mix(in srgb, var(--flux-border) 28%, transparent);
|
||||
}
|
||||
|
||||
.sidebar-content a[aria-current='page'] {
|
||||
color: var(--flux-orange) !important;
|
||||
background-color: color-mix(in srgb, var(--flux-orange) 12%, transparent) !important;
|
||||
border-left: 3px solid var(--flux-orange) !important;
|
||||
}
|
||||
|
||||
.sidebar-content a:hover {
|
||||
color: var(--flux-blue) !important;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.site-title img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-title span {
|
||||
position: relative;
|
||||
font-size: 0;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.site-title span::before {
|
||||
content: 'Flux';
|
||||
font-size: 1.55rem;
|
||||
font-weight: 800;
|
||||
color: var(--flux-blue);
|
||||
}
|
||||
|
||||
.site-title span::after {
|
||||
content: 'MQ';
|
||||
font-size: 1.55rem;
|
||||
font-weight: 800;
|
||||
color: var(--flux-orange);
|
||||
}
|
||||
|
||||
.sl-markdown-content p {
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.sl-markdown-content table th,
|
||||
.sl-markdown-content table td {
|
||||
border: 1px solid var(--flux-border);
|
||||
}
|
||||
|
||||
.sl-markdown-content table th {
|
||||
background: var(--flux-fg);
|
||||
color: var(--flux-bg);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (min-width: 50rem) {
|
||||
:root,
|
||||
:root[data-theme='light'],
|
||||
:root[data-theme='dark'] {
|
||||
--sl-nav-height: 7.2rem;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: grid !important;
|
||||
grid-template-columns: var(--sl-sidebar-width) minmax(0, 1fr);
|
||||
grid-template-rows: var(--sl-nav-height) minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page > .header {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
width: var(--sl-sidebar-width) !important;
|
||||
border-inline-end: 1px solid var(--sl-color-hairline);
|
||||
background: var(--sl-color-bg-sidebar);
|
||||
}
|
||||
|
||||
.page > .header > .header {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.7rem;
|
||||
padding-inline: 0.8rem;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.page > .header .title-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page > .header site-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page > .header site-search button[data-open-modal] {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--sl-color-hairline-light);
|
||||
border-radius: 0.6rem;
|
||||
background: color-mix(in srgb, var(--sl-color-bg) 75%, var(--sl-color-bg-sidebar));
|
||||
}
|
||||
|
||||
.page > .header site-search button[data-open-modal] kbd {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
.page > .header .right-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.page > .sidebar {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.page > .sidebar .sidebar-pane {
|
||||
inset-block-start: var(--sl-nav-height);
|
||||
width: var(--sl-sidebar-width);
|
||||
}
|
||||
|
||||
.page > .sidebar .sidebar-content {
|
||||
min-height: calc(100vh - var(--sl-nav-height));
|
||||
}
|
||||
|
||||
.page > .sidebar .sidebar-content > .md\:sl-hidden {
|
||||
display: block !important;
|
||||
margin-top: auto;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--sl-color-hairline-light);
|
||||
}
|
||||
|
||||
.page > .sidebar .mobile-preferences {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 0;
|
||||
padding-top: 0.3rem;
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.page > .sidebar .mobile-preferences .social-icons {
|
||||
margin-inline-end: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page > .sidebar .mobile-preferences starlight-theme-select label {
|
||||
--sl-select-width: 2.1rem;
|
||||
border: 1px solid var(--sl-color-hairline-light);
|
||||
border-radius: 999px;
|
||||
padding: 0.35rem;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.page > .sidebar .mobile-preferences starlight-theme-select .caret {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page > .sidebar .mobile-preferences starlight-theme-select select {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.page > .main-frame {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 2;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,13 @@ function initOrama() {
|
||||
}
|
||||
|
||||
export default function DefaultSearchDialog(props: SharedProps) {
|
||||
const { locale } = useI18n(); // (optional) for i18n
|
||||
const { locale, locales } = useI18n(); // (optional) for i18n
|
||||
const searchLocale = locales && locales.length > 0 ? locale : undefined;
|
||||
|
||||
const { search, setSearch, query } = useDocsSearch({
|
||||
type: 'static',
|
||||
initOrama,
|
||||
locale,
|
||||
locale: searchLocale,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user