Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ lerna-debug.log*
test-results/
playwright-report/
playwright/.cache/

# VitePress
docs/.vitepress/cache/
docs/.vitepress/dist/
4 changes: 3 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
"packages/playground/public/tabmesh-worker.js",
"packages/playground/public/tabmesh-sw.js",
"test-results",
"playwright-report"
"playwright-report",
"docs/.vitepress/cache",
"docs/.vitepress/dist"
]
}
}
111 changes: 111 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { defineConfig } from 'vitepress';

// Site config for tabmesh.dev. Layout: landing at /, deep content under
// /guide, /reference, /recipes, /adr, /roadmap, and the playground iframe
// at /playground. ADRs are picked up directly from docs/adr/ — no
// duplication between repo-internal architecture decisions and the
// public site.
export default defineConfig({
lang: 'en-US',
title: 'TabMesh',
description:
'One backend connection, every browser tab. SharedWorker-primary event mesh with elected-leader fallback.',
cleanUrls: true,
lastUpdated: true,
head: [
['meta', { name: 'theme-color', content: '#646cff' }],
['meta', { property: 'og:title', content: 'TabMesh' }],
[
'meta',
{
property: 'og:description',
content: 'One backend connection, every browser tab.',
},
],
],
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide/getting-started' },
{ text: 'Reference', link: '/reference/tabmesh-class' },
{ text: 'Recipes', link: '/recipes/' },
{ text: 'Roadmap', link: '/roadmap' },
{ text: 'Playground', link: '/playground' },
{
text: 'GitHub',
link: 'https://github.com/CodeThicket/tabmesh',
},
],
sidebar: {
'/guide/': [
{
text: 'Guide',
items: [
{ text: 'What is TabMesh?', link: '/guide/what-is-tabmesh' },
{ text: 'Getting Started', link: '/guide/getting-started' },
{ text: 'Architecture', link: '/guide/architecture' },
{ text: 'Gotchas', link: '/guide/gotchas' },
{ text: 'React', link: '/guide/react' },
],
},
],
'/reference/': [
{
text: 'API Reference',
items: [
{ text: 'TabMesh class', link: '/reference/tabmesh-class' },
{ text: 'Configuration', link: '/reference/config' },
{ text: 'System events', link: '/reference/system-events' },
{ text: 'Types', link: '/reference/types' },
],
},
],
'/recipes/': [
{
text: 'Recipes',
items: [
{ text: 'Index', link: '/recipes/' },
{ text: 'Auth & logout', link: '/recipes/auth-and-logout' },
{ text: 'Custom transport', link: '/recipes/custom-transport' },
{ text: 'Service Worker handoff', link: '/recipes/service-worker-handoff' },
],
},
],
'/adr/': [
{
text: 'Architecture Decisions',
items: [
{ text: 'Index', link: '/adr/' },
{
text: '0001 — Write-through Outbox',
link: '/adr/0001-write-through-outbox',
},
{
text: '0002 — SharedWorker primary Hub',
link: '/adr/0002-sharedworker-primary-hub',
},
{
text: '0003 — Pre-built worker bundles',
link: '/adr/0003-distribute-prebuilt-worker-bundles',
},
{
text: '0004 — VitePress single site',
link: '/adr/0004-vitepress-single-site-at-tabmesh-dev',
},
],
},
],
},
socialLinks: [{ icon: 'github', link: 'https://github.com/CodeThicket/tabmesh' }],
search: {
provider: 'local',
},
editLink: {
pattern: 'https://github.com/CodeThicket/tabmesh/edit/main/docs/:path',
text: 'Suggest changes to this page',
},
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2026-present TabMesh contributors',
},
},
});
20 changes: 20 additions & 0 deletions docs/adr/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Architecture Decisions

Records of architectural decisions taken during TabMesh's development. Each ADR is short — usually 1–3 paragraphs explaining what was decided and why, plus the rejected alternatives when those are worth remembering.

## Index

- [0001 — Write-through Outbox for all outbound events](./0001-write-through-outbox)
- [0002 — SharedWorker as the primary Hub implementation](./0002-sharedworker-primary-hub)
- [0003 — Distribute pre-built worker bundles inside `@tabmesh/core`](./0003-distribute-prebuilt-worker-bundles)
- [0004 — Single VitePress site at `tabmesh.dev` for docs, roadmap, and playground](./0004-vitepress-single-site-at-tabmesh-dev)

## When we add an ADR

A decision warrants an ADR when **all three** are true:

1. **Hard to reverse** — the cost of changing your mind later is meaningful.
2. **Surprising without context** — a future reader will look at the code and wonder why.
3. **The result of a real trade-off** — there were genuine alternatives.

Routine choices ("we use TypeScript", "we use pnpm workspaces") don't get an ADR. Only the surprising or load-bearing ones.
14 changes: 14 additions & 0 deletions docs/guide/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Architecture

::: warning Migrating
This page is being migrated from the [README architecture section](https://github.com/CodeThicket/tabmesh#architecture). The diagrams and full explainer arrive in the next docs PR.
:::

For now, the short version:

- **Primary mode** uses a `SharedWorker` shared across all tabs of the same origin. The worker holds the transport and the IndexedDB outbox. Tabs talk to it over `MessagePort`.
- **Fallback mode** elects a leader tab via Web Locks API → BroadcastChannel heartbeat → IndexedDB heartbeat. Used when SharedWorker isn't available (some mobile browsers).
- **Outbox** is IndexedDB-backed with TTL, priority ordering, and an in-memory degraded fallback.
- **Service Worker** (optional) takes over outbox draining after the last tab closes, via Background Sync.

→ See [ADR-0001](/adr/0001-write-through-outbox) and [ADR-0002](/adr/0002-sharedworker-primary-hub) for the design rationale.
15 changes: 15 additions & 0 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Getting Started

::: warning Migrating
This page is being migrated from the [project README](https://github.com/CodeThicket/tabmesh#readme). The full quickstart, install instructions, and first-event walkthrough land in the next docs PR.
:::

For now: see the [README quickstart](https://github.com/CodeThicket/tabmesh#quick-start).

## What's here next

- Install commands for `@tabmesh/core` + `@tabmesh/transport-websocket` (+ optional `@tabmesh/react`).
- The "copy the worker bundle to `public/`" step (per [ADR-0003](/adr/0003-distribute-prebuilt-worker-bundles)).
- Minimal example: send and receive an event across two tabs.
- React hooks variant.
- First time troubleshooting.
15 changes: 15 additions & 0 deletions docs/guide/gotchas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Gotchas

::: warning Migrating
The full gotchas list — the most important page for adopters — is being moved here from the [README gotchas section](https://github.com/CodeThicket/tabmesh#gotchas). Read the README until this page is filled in.
:::

The shortlist, until this page expands:

- **SharedWorker name caching** → set `workerVersion` per deploy.
- **`delivered` ≠ "the backend processed it"** → wait for explicit ACK if you need it; `ackMode: 'server'` is on the [roadmap](/roadmap).
- **No replay buffer for late-joining tabs** — by design.
- **In-browser only**, same-origin only.
- **Service Worker handoff requires `deliveryUrl`** — without it, the SW leaves pending events in the outbox for the next Hub session.

→ Read the [README gotchas](https://github.com/CodeThicket/tabmesh#gotchas) for the explanations until this page fills out.
13 changes: 13 additions & 0 deletions docs/guide/react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# React

::: warning Migrating
React-specific quickstart, hooks reference, and provider example arrive in the next docs PR.
:::

`@tabmesh/react` ships:

- `<TabMeshProvider>` — context provider for the mesh instance.
- `useTabMesh()` — returns `{ status, send }`.
- `useTabMeshEvent(type, handler)` — subscribe to an event type (or `'*'` for all).

Until this page fills out, see the [README React snippet](https://github.com/CodeThicket/tabmesh#react) and the [playground source](https://github.com/CodeThicket/tabmesh/tree/main/packages/playground/src) for a working example.
44 changes: 44 additions & 0 deletions docs/guide/what-is-tabmesh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# What is TabMesh?

TabMesh is a frontend event mesh — a small library that multiplexes a single backend transport (WebSocket today, SSE / long-poll later) across every browser tab of the same origin, persists outbound events to IndexedDB, and broadcasts events across tabs in real time.

It is **hub-and-spoke**, not peer-to-peer. The hub is a `SharedWorker` (primary) or an elected leader tab (fallback). Every tab is a spoke; the hub holds the transport and the durable outbox.

## What problem it solves

Open the same web app in three tabs and you'll typically see:

- Three independent WebSockets to the same backend, tripling connection count and idle CPU.
- Three copies of the same push notification.
- Three concurrent reconnect storms when the network blips.
- Cross-tab UI state that drifts because tabs don't talk to each other.
- Lost events when a tab closes mid-send.

TabMesh collapses this into one transport, one outbox, and a documented event protocol between tabs.

## What it isn't

- **Not a state management library.** It moves events; you decide how to update state. (Tip: pair it with whatever you already use — Redux, Zustand, signals, plain `useReducer`.)
- **Not a full PubSub broker.** No history, no replay buffer for late-joining tabs. New tabs fetch state from your backend, not from TabMesh.
- **Not cross-origin.** It coordinates tabs within the same origin only. Same-protocol, same-host, same-port.
- **Not a Node library.** Browser-side only.

## When to reach for it

You probably want TabMesh if:

- Your app has long-lived backend connections (chat, presence, dashboards, collaborative tools).
- Users routinely have multiple tabs open.
- You've noticed connection multiplication, duplicate notifications, or cross-tab inconsistency.

You probably don't need it if:

- Your app is a single-tab session per user.
- Your backend connections are short request/response, not push.
- You don't have cross-tab interactions worth coordinating.

## What's next

→ [Getting started](./getting-started)
→ [Architecture](./architecture)
→ [Gotchas](./gotchas) — read this before adopting in production
38 changes: 38 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
layout: home

hero:
name: TabMesh
text: One backend connection, every tab.
tagline: SharedWorker-primary event mesh with elected-leader fallback. Multiplex one WebSocket across all your browser tabs, persist outbound events to IndexedDB, broadcast across tabs in real time.
actions:
- theme: brand
text: Get started
link: /guide/getting-started
- theme: alt
text: Try the playground
link: /playground
- theme: alt
text: View on GitHub
link: https://github.com/CodeThicket/tabmesh

features:
- icon: 🔌
title: One WebSocket for N tabs
details: A SharedWorker holds the transport. Open the same app in 10 tabs and your backend sees one connection.
- icon: 💾
title: Durable outbox
details: Outbound events persist to IndexedDB before delivery. Offline events drain when the network comes back.
- icon: 🛰️
title: Cross-tab events
details: mesh.send in tab A surfaces in tab B as source = 'remote'. No app code wiring needed.
- icon: 🪪
title: Logout flow
details: Clear outbox, drop transport, broadcast logout, stop — in that order, in one library.
- icon: 🧪
title: Tested in real browsers
details: 11 Playwright contracts cover SharedWorker primacy, late-joiner replay, leader failover, SW handoff.
- icon: 🔬
title: Pre-1.0, honest about it
details: The roadmap names what's next, what's considered, and what's explicitly out of scope.
---
41 changes: 41 additions & 0 deletions docs/playground.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Playground
layout: page
---

<style scoped>
.pg-frame {
width: 100%;
min-height: calc(100vh - 240px);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
}
.pg-help {
margin: 16px 0 24px;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 4px;
font-size: 14px;
}
</style>

<div class="VPDoc has-aside">
<div class="container">
<div class="content">
<div class="content-container">
<h1>Playground</h1>
<div class="pg-help">
A live multi-tab demo. Open this page in two browser tabs to see TabMesh sharing a single WebSocket and broadcasting <code>todo.add</code> / <code>todo.complete</code> / <code>todo.delete</code> events between them. The activity feed shows <code>LOCAL</code> vs <code>REMOTE</code> classification and the system events the mesh is emitting.
</div>
<iframe
class="pg-frame"
src="/playground/index.html"
title="TabMesh playground"
loading="lazy"
></iframe>
</div>
</div>
</div>
</div>
14 changes: 14 additions & 0 deletions docs/recipes/auth-and-logout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Auth & logout

::: warning Coming soon
This recipe is planned but not written yet. The pattern is documented in [CONTEXT.md → Session & Auth](https://github.com/CodeThicket/tabmesh/blob/main/CONTEXT.md#session--auth) and the canonical sequence is exercised in the [playground's `MeshStatus.tsx`](https://github.com/CodeThicket/tabmesh/blob/main/packages/playground/src/components/MeshStatus.tsx). The order matters:

```ts
await mesh.clearOutbox();
await mesh.disconnectTransport();
mesh.broadcast({ type: 'auth.logout', payload: {} });
await mesh.stop();
```

The full recipe with race-condition reasoning, error handling, and multi-tab UI patterns lands in a follow-up. If you need it sooner, [open an issue](https://github.com/CodeThicket/tabmesh/issues/new) and it'll get prioritised.
:::
7 changes: 7 additions & 0 deletions docs/recipes/custom-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Custom transport

::: warning Coming soon
The `Transport` interface is small (5 methods + 3 callback slots, all in [`types.ts`](https://github.com/CodeThicket/tabmesh/blob/main/packages/core/src/types.ts)). The reference implementation is [`@tabmesh/transport-websocket`](https://github.com/CodeThicket/tabmesh/blob/main/packages/transport-websocket/src/WebSocketTransport.ts) — short enough to copy and adapt for SSE or long-poll.

Full recipe with reconnection wiring, `getWorkerConfig` (so the SharedWorker can rebuild the connection), and error-handling patterns lands in a follow-up. SSE and long-poll adapters are on the [roadmap](/roadmap) — if you build one in the meantime, a PR is very welcome.
:::
13 changes: 13 additions & 0 deletions docs/recipes/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Recipes

Cookbook-style how-tos for common patterns. New recipes are added when adopters hit them — open a [GitHub issue](https://github.com/CodeThicket/tabmesh/issues) to request one.

## Available

- [Auth & logout](./auth-and-logout) — clearing the outbox and tearing down transport on session end
- [Custom transport](./custom-transport) — implementing the `Transport` interface for SSE, long-poll, or a custom protocol
- [Service Worker handoff](./service-worker-handoff) — draining pending events after the last tab closes

::: tip Want a recipe?
Open a [docs request issue](https://github.com/CodeThicket/tabmesh/issues/new) and we'll add it.
:::
13 changes: 13 additions & 0 deletions docs/recipes/service-worker-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Service Worker handoff

::: warning Coming soon
The handoff path is implemented (see [PR #16](https://github.com/CodeThicket/tabmesh/pull/16)) and tested end-to-end via Playwright + CDP, but the recipe walking through `serviceWorker.deliveryUrl` setup, the `tabmesh-sw.js` deployment, and "what to do on the backend when sync fires" hasn't been written yet.

The minimum viable version:

1. Copy `node_modules/@tabmesh/core/dist/tabmesh-sw.js` to your app's `public/` directory.
2. Configure: `serviceWorker: { enabled: true, deliveryUrl: '/api/events' }`.
3. Your `/api/events` endpoint accepts JSON `POST` with `{ type, payload, id, sourceTabId }`. Return `200` to mark the event as delivered. Non-`200` responses leave it pending for the next sync.

A full walkthrough — including testing locally without waiting for the browser to schedule sync — lands in a follow-up.
:::
Loading
Loading