first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
# @suaveplan/react-network-libp2p-demo - React-tier demo UI for @suaveplan/network-transport-libp2p. Decomposed into per-concern subcomponents (status, controls, peer interaction, peer lists, log panel) each renderable standalone for Storybook isolation. Bootstrap list is a required React prop — no hardcoded public defaults.
## Summary
React-tier demo UI for `@suaveplan/network-transport-libp2p`. Splits the
upstream 749-LOC monolithic demo into per-concern subcomponents
(`<NodeStatusPanel>`, `<NodeControls>`, `<PeerInteractionPanel>`,
`<PeerList>`, `<LogPanel>`) wired together by a root `<Libp2pDemo>`
composer. Every subcomponent is standalone-renderable with mock data for
Storybook isolation; the lifecycle + state machine lives in the
`useLibp2pDemo` hook.
<!-- begin:auto-docs -->
## Documentation tree
- [libp2p-demo](./src/components/libp2p-demo/libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [libp2p-demo](./src/components/libp2p-demo/libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - wire-peer-events — Side-effect helper that subscrib…
- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - wire-peer-events — Side-effect helper that subscrib…
- [wire-peer-events](./src/hooks/use-libp2p-demo/wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
<!-- end:auto-docs --> - [libp2p-demo](./src/components/libp2p-demo/libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- wire-peer-events — Side-effect helper that subscrib…
- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- wire-peer-events — Side-effect helper that subscrib…
- [wire-peer-events](./src/hooks/use-libp2p-demo/wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
- [libp2p-demo](./src/components/libp2p-demo/libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- wire-peer-events — Side-effect helper that subscrib…
- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- wire-peer-events — Side-effect helper that subscrib…
- [wire-peer-events](./src/hooks/use-libp2p-demo/wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
- [libp2p-demo](./src/components/libp2p-demo/libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- wire-peer-events — Side-effect helper that subscrib…
- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- wire-peer-events — Side-effect helper that subscrib…
- [wire-peer-events](./src/hooks/use-libp2p-demo/wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
- [libp2p-demo](./src/components/libp2p-demo/libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [log-panel](./src/components/log-panel/log-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_ - [node-controls](./src/components/node-controls/node-controls.md) — <!-- begin:auto-docs --> _(no submodules)_- [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [node-status-panel](./src/components/node-status-panel/node-status-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [peer-interaction-panel](./src/components/peer-interaction-panel/peer-interaction-panel.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_ - [peer-list](./src/components/peer-list/peer-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ - [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-libp2p-demo](./src/hooks/use-libp2p-demo/use-libp2p-demo.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wire-peer-events](./src/hooks/use-libp2p-demo/wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
## Required user-supplied bootstrap
The package **never** publishes a public bootstrap list. The `<Libp2pDemo>`
composer (and the underlying `useLibp2pDemo` hook) accepts a `bootstrap:
readonly string[]` React prop that is REQUIRED — the array may be empty
(for mDNS-only or WebRTC-only flows) but the consumer must always supply
it explicitly. There are no hardcoded fallback peers anywhere in the
source tree.
```tsx
import { Libp2pDemo } from "@suaveplan/react-network-libp2p-demo";
<Libp2pDemo
nodeFactory={myLibp2pFactory}
bootstrap={["/dnsaddr/your-bootstrap.example/p2p/Qm…"]}
subTransport="webrtc"
/>;
```
This is a deliberate API choice: hardcoding a default would bake a
trust decision into the library that every downstream consumer would
silently inherit. Consumers stay in charge of which peers their demo
trusts.
## What this package owns
- **Root composer** — `<Libp2pDemo>` ([`components/libp2p-demo`](./src/components/libp2p-demo/libp2p-demo.md))
- **Hook** — `useLibp2pDemo` ([`hooks/use-libp2p-demo`](./src/hooks/use-libp2p-demo/use-libp2p-demo.md))
- **Peer-event wiring helper** — `wirePeerEvents` ([wiring](./src/hooks/use-libp2p-demo/wire-peer-events.md))
- **Status panel** — `<NodeStatusPanel>` ([panel](./src/components/node-status-panel/node-status-panel.md))
- **Lifecycle buttons** — `<NodeControls>` ([controls](./src/components/node-controls/node-controls.md))
- **Interaction row** — `<PeerInteractionPanel>` ([row](./src/components/peer-interaction-panel/peer-interaction-panel.md))
- **Peer lists** — `<PeerList>` ([list](./src/components/peer-list/peer-list.md))
- **Log viewer** — `<LogPanel>` ([logs](./src/components/log-panel/log-panel.md))
## Architecture
The package depends on `@suaveplan/network-transport-libp2p` as a peer
dependency, but never imports `libp2p` directly. Production consumers
implement a thin `Libp2pDemoNodeFactory` adapter that wraps the real
libp2p stack and conforms to the structural `Libp2pDemoNode` contract
defined here. Tests pass a deterministic fake conforming to the same
contract (`tests/setup/fake-node.ts`).
## Error registry
`defineCodes("REACT_LIBP2P_DEMO", […])` — 5 codes:
`INVALID_OPTIONS`, `DEMO_DISPOSED`, `NODE_NOT_STARTED`, `DIAL_FAILED`,
`ECHO_FAILED`.
## Status
- Wave 7.2 (final network-migration package).
- Source files all ≤300 LOC per CLAUDE.md.
- Every component carries Storybook stories (default + playground +
variants) per CLAUDE.md.

View File

@@ -0,0 +1,84 @@
# `libp2p-demo`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Root composer for the React libp2p demo. Wires `useLibp2pDemo` to five
presentational subcomponents (`<NodeStatusPanel>`, `<NodeControls>`,
`<PeerInteractionPanel>`, two `<PeerList>`s, `<LogPanel>`) and owns the
two pieces of UI-local state (target-peer + message text) the
interaction panel needs. The bootstrap multiaddr list is a REQUIRED
React prop — the package never publishes a public default — so consumers
must explicitly pin the peers their deployment trusts.
## Features
- Forwards every `UseLibp2pDemoOptions` field except `nodeFactory` and
`bootstrap` straight to the hook.
- Bootstrap list is a required prop (empty array permitted for
mDNS-only / WebRTC-only flows).
- `nodeFactory` is required — the package never imports libp2p directly;
consumers wire the real stack inside the factory.
- `debug` toggles the log panel without losing the underlying buffer.
- `heading` lets a consumer relabel the H2 row above the panels.
- Wrapper `style` merges over the built-in centred layout.
## Usage: Basic Example
```tsx
import { Libp2pDemo } from "@suaveplan/react-network-libp2p-demo";
<Libp2pDemo nodeFactory={myFactory} bootstrap={["/dnsaddr/peer/p2p/Qm"]} />;
```
## Usage: Advanced Example
```tsx
import { Libp2pDemo } from "@suaveplan/react-network-libp2p-demo";
function Page() {
return (
<Libp2pDemo
nodeFactory={buildLibp2pAdapter()}
bootstrap={[
"/dnsaddr/bootstrap-a/p2p/Qm…",
"/dnsaddr/bootstrap-b/p2p/Qm…",
]}
subTransport="webrtc"
listen={["/webrtc"]}
maxConnections={32}
dialTimeoutMs={5000}
protocolId="/myapp/echo/1.0.0"
autoStart={true}
debug={true}
heading="MyApp Network Demo"
style={{ maxWidth: 800 }}
/>
);
}
```
## API Reference
- `Libp2pDemoProps` — `Omit<UseLibp2pDemoOptions, "nodeFactory" | "bootstrap"> & {
nodeFactory; bootstrap; debug?; heading?; style? }`.
- `Libp2pDemo(props): ReactElement`.
## Implementation Notes
The composer is intentionally small: every panel is a self-contained
presentational component that can be rendered standalone in Storybook
with mocked data, while the composer's only job is to project the hook's
state into the props each panel expects. Two pieces of input state
(`targetPeer`, `messageText`) live in `useState` rather than in the hook
because they belong to the UI, not the libp2p layer — a future consumer
that wants its own UI for the interaction panel can ignore the composer
entirely and call the hook + subcomponents directly. Bootstrap is
required by deliberate API design: hardcoding a public list inside a
library would push a default trust decision onto every consumer, so the
package always errors towards explicit ownership. The composer never
imports anything from libp2p — production consumers wrap the real
`createLibp2p` call inside a factory adapter conforming to
`Libp2pDemoNodeFactory`.

View File

@@ -0,0 +1,77 @@
# `log-panel`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Append-only debug-log viewer for the libp2p demo. Renders the
`useLibp2pDemo` hook's `logs` array as timestamped, level-coloured rows
inside a fixed-height scroll container. Pure presentational component —
retention, ordering, and id generation live in the hook layer, so the
panel never owns mutable log state.
## Features
- Four-level colour coding (info / error / warning / success), overrideable
via the `levelColor` prop.
- Timestamp rendering is injectable through `formatTimestamp` for
locale-aware UI or test determinism; default shows ISO `HH:mm:ss.SSS`.
- Configurable `maxHeight` (defaults to 300 px) for the scroll container.
- Empty-state placeholder (`No log entries`) when the array is empty.
- `visible` flag returns `null` so consumers can toggle the panel out of
the tree without losing the underlying log buffer.
- Stable `data-testid` per row keyed on entry id.
## Usage: Basic Example
```tsx
import { LogPanel } from "@suaveplan/react-network-libp2p-demo";
<LogPanel entries={demo.logs} />;
```
## Usage: Advanced Example
```tsx
import { LogPanel } from "@suaveplan/react-network-libp2p-demo";
function Logs({ demo }: { demo: UseLibp2pDemoResult }) {
return (
<LogPanel
entries={demo.logs}
visible={demo.logs.length > 0}
maxHeight={500}
formatTimestamp={(ts) =>
new Date(ts).toLocaleTimeString("en-GB", {
hour12: false,
})
}
levelColor={(level) =>
level === "error" ? "#c0392b" : "#2c3e50"
}
/>
);
}
```
## API Reference
- `LogPanelProps` — `{ entries, visible?, maxHeight?, formatTimestamp?,
levelColor?, style? }`.
- `LogPanel(props): ReactElement | null` — returns `null` when
`visible === false`.
## Implementation Notes
The default timestamp formatter slices `Date.toISOString()` to extract
`HH:mm:ss.SSS` because that is locale-independent and renders identically
in every test environment; consumers wanting locale-aware times should
pass `formatTimestamp`. The level-colour palette intentionally uses pure
keyword colours (`red`, `green`, `orange`, `black`) rather than hex
because the panel is a debugging tool, not a branded surface — overrides
are easy when needed. The empty-state placeholder is rendered inside the
scroll container so it shifts with the same vertical rhythm as real
entries. Setting `visible={false}` returns `null` rather than
`display: none`; this keeps the DOM tree slimmer in the inactive case
without disturbing the underlying log buffer in the parent.

View File

@@ -0,0 +1,83 @@
# `node-controls`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Lifecycle button row for the libp2p demo: Start Node, Stop Node, Clear
Logs. Pure presentational subcomponent — each action is a prop callback,
so a host can wire any handler (real demo hook, Storybook noop, test spy).
The disabled state mirrors the demo's `isStarted` boolean so users can
never trigger an action the underlying node would reject.
## Features
- Three buttons with stable `data-testid`s (`node-controls-start`,
`node-controls-stop`, `node-controls-clear-logs`) for interaction tests.
- Start button disabled when `isStarted === true`.
- Stop button disabled when `isStarted === false`.
- Clear-logs button always enabled (clearing is safe at any lifecycle
phase).
- Colour cue per state (`#28a745` green / `#dc3545` red / `#6c757d` grey
for clear).
- Container style override.
## Usage: Basic Example
```tsx
import { NodeControls } from "@suaveplan/react-network-libp2p-demo";
<NodeControls
isStarted={false}
onStart={() => void demo.start()}
onStop={() => void demo.stop()}
onClearLogs={demo.clearLogs}
/>;
```
## Usage: Advanced Example
```tsx
import { NodeControls } from "@suaveplan/react-network-libp2p-demo";
function ControlsBar({ demo }: { demo: UseLibp2pDemoResult }) {
return (
<NodeControls
isStarted={demo.isStarted}
onStart={() => void demo.start()}
onStop={() => void demo.stop()}
onClearLogs={demo.clearLogs}
style={{
marginBottom: 0,
padding: 8,
background: "#fafafa",
borderRadius: 4,
}}
/>
);
}
```
## API Reference
- `NodeControlsProps` — `{ isStarted, onStart, onStop, onClearLogs,
style? }`.
- `NodeControls(props): ReactElement` — always renders all three
buttons; visibility is controlled by `disabled`, never by conditional
rendering, so layout never reflows.
## Implementation Notes
The component never renders fewer than three buttons. Hiding them
on lifecycle change would jank the layout and confuse keyboard focus.
Instead it disables and recolours, matching the upstream demo's UX. The
clear-logs button is always enabled because clearing local UI state has
no contraindication — separating it from the lifecycle pair also lets a
test driver assert on a single isolated action without provisioning a
running node first. Colour values match Bootstrap-3 convention so the
demo reads naturally even outside any theme. The container `style` prop
is applied directly (not merged through a base) so a consumer can fully
restyle the row without contortions; the buttons keep their built-in
styling because button-by-button override would have multiplied the
prop surface needlessly.

View File

@@ -0,0 +1,77 @@
# `node-status-panel`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Presentational status block for the libp2p demo. Shows whether the node is
running, its peer id, peer + listen-address counts, and the most recent
ping result. Pure props-in / DOM-out — owns no state, so Storybook can
render every state combination from static mock data without spinning a
real libp2p node.
## Features
- Coloured background (`#e7f5e7` running / `#ffe7e7` stopped) drives the
primary status signal pre-cognitively.
- Hides the peer-id row when `peerId === ""` so the panel collapses
cleanly while the node is stopped.
- Hides the last-ping row when `lastPingMs` is `null` or `undefined`.
- Renders every dynamic value behind a `data-testid` so interaction tests
can assert without DOM-string fragility.
- Accepts a `style` override that wins over the built-in background.
## Usage: Basic Example
```tsx
import { NodeStatusPanel } from "@suaveplan/react-network-libp2p-demo";
<NodeStatusPanel
isStarted={false}
peerId=""
discoveredCount={0}
connectedCount={0}
listenCount={0}
/>;
```
## Usage: Advanced Example
```tsx
import { NodeStatusPanel } from "@suaveplan/react-network-libp2p-demo";
function Status({ demo }: { demo: UseLibp2pDemoResult }) {
return (
<NodeStatusPanel
isStarted={demo.isStarted}
peerId={demo.peerId}
discoveredCount={demo.discoveredPeers.length}
connectedCount={demo.connectedPeers.length}
listenCount={demo.listenAddresses.length}
lastPingMs={demo.lastPingMs}
style={{ borderColor: "#888" }}
/>
);
}
```
## API Reference
- `NodeStatusPanelProps` — `{ isStarted, peerId, discoveredCount,
connectedCount, listenCount, lastPingMs?, style? }`.
- `NodeStatusPanel(props): ReactElement` — never returns `null`.
## Implementation Notes
The component is intentionally dumb: every value rendered is a prop. The
hook layer (`useLibp2pDemo`) is responsible for projecting state into the
shape this panel expects, which keeps the panel reusable in tests and
Storybook regardless of the underlying libp2p plumbing. Peer-id is wrapped
in a `<code>` element so long base58 strings keep their monospace identity
without wrapping awkwardly inside the surrounding flow. The merged style
order is base → status-coloured → user-supplied, so consumers can stamp
in a darker theme without losing the running/stopped colour cue unless
they override `backgroundColor` explicitly. Every data-testid is stable
across renders, which means snapshot tests should anchor on the testid
rather than DOM hierarchy.

View File

@@ -0,0 +1,85 @@
# `peer-interaction-panel`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Controlled peer-interaction row used by the libp2p demo. Hosts the
target-peer text input, the echo-message text input, and the three action
buttons (dial / ping / send-echo). The panel is fully controlled: every
input value and action handler lives in the parent, so the same component
renders identically inside the demo, inside a Storybook fixture, and
inside an interaction test.
## Features
- Two controlled text inputs (`targetPeer`, `messageText`).
- Three action buttons (`dial`, `ping`, `send-echo`).
- Dial + ping enabled only when the node is started AND a target is
populated.
- Send-echo additionally requires a non-empty message.
- Stable `data-testid` for every interactive element.
- Container style override.
## Usage: Basic Example
```tsx
import { PeerInteractionPanel } from "@suaveplan/react-network-libp2p-demo";
<PeerInteractionPanel
isStarted={true}
targetPeer="12D3KooW…"
messageText="hi"
onTargetPeerChange={setTarget}
onMessageTextChange={setMessage}
onDial={(t) => demo.dial(t)}
onPing={(t) => demo.ping(t)}
onEcho={(t, m) => demo.echo(t, m)}
/>;
```
## Usage: Advanced Example
```tsx
import { PeerInteractionPanel } from "@suaveplan/react-network-libp2p-demo";
function InteractionRow({ demo }: { demo: UseLibp2pDemoResult }) {
const [target, setTarget] = useState("");
const [message, setMessage] = useState("Hello from libp2p!");
return (
<PeerInteractionPanel
isStarted={demo.isStarted}
targetPeer={target}
messageText={message}
onTargetPeerChange={setTarget}
onMessageTextChange={setMessage}
onDial={(t) => void demo.dial(t)}
onPing={(t) => void demo.ping(t)}
onEcho={(t, m) => void demo.echo(t, m)}
/>
);
}
```
## API Reference
- `PeerInteractionPanelProps` — `{ isStarted, targetPeer, messageText,
onTargetPeerChange, onMessageTextChange, onDial, onPing, onEcho,
style? }`.
- `PeerInteractionPanel(props): ReactElement`.
## Implementation Notes
Every action button uses the same `buttonColor` + `buttonCursor` helpers
so adding a fourth action would be a 6-line change. The component
intentionally does not trim inputs before invoking callbacks — the parent
hook knows whether trimming makes sense for the underlying call (some
multiaddrs are sensitive to whitespace prefixes). The enabled/disabled
logic uses two derived booleans (`peerActionsEnabled` and `echoEnabled`)
which encode the three required props in one place — keeping that logic
inside the panel rather than the parent means consumers cannot
accidentally allow a click that the underlying hook would reject. The
panel renders both rows even when nothing is enabled because layout
stability matters more than minimising DOM during the demo's brief
stopped state.

View File

@@ -0,0 +1,80 @@
# `peer-list`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Reusable peer-list panel. Renders either discovered or connected peers
behind a single component — the caller supplies the title, the array, an
empty-state message, and the row-selection callback. Selecting a row
invokes `onSelect(peerId)` with the full (untruncated) peer id so the
caller can paste it directly into a dial/ping/echo command.
## Features
- Single component covers both lists used by the demo (Discovered + Connected).
- Truncates ids in the row label (`id.substring(0, 20) + "…"`) but passes
the full id to `onSelect`.
- Empty-state message customisable per use site.
- Connected rows can be highlighted (`highlightConnected`, default true).
- Each row carries `data-testid="peer-list-row-<peerId>"` so interaction
tests can target a single peer.
## Usage: Basic Example
```tsx
import { PeerList } from "@suaveplan/react-network-libp2p-demo";
<PeerList
title="Connected"
peers={demo.connectedPeers}
onSelect={setTargetPeer}
/>;
```
## Usage: Advanced Example
```tsx
import { PeerList } from "@suaveplan/react-network-libp2p-demo";
function Lists({ demo, onPick }: Props) {
return (
<div style={{ display: "flex", gap: 20 }}>
<PeerList
title="Discovered"
peers={demo.discoveredPeers}
emptyMessage="No peers discovered yet"
onSelect={onPick}
/>
<PeerList
title="Connected"
peers={demo.connectedPeers}
emptyMessage="No peers connected"
onSelect={onPick}
highlightConnected={false}
/>
</div>
);
}
```
## API Reference
- `PeerListProps` — `{ title, peers, emptyMessage?, onSelect,
highlightConnected?, style? }`.
- `PeerList(props): ReactElement`.
## Implementation Notes
A nested `<PeerRow>` component sits inside this file rather than under
its own folder because it is purely an implementation detail and has no
useful public surface. The truncation rule is a constant (20 + ellipsis)
matching the upstream demo; consumers wanting a different truncation
should compose the rows themselves. The component uses
`buttons` for every row so keyboard-only users get free focus
management — visually styled as rows but semantically a clickable list.
Highlight defaults to true because the most common consumer ("Discovered"
column) needs to distinguish discovered-only from discovered-and-connected
peers; the connected list typically passes `highlightConnected=false`
because every row would otherwise highlight redundantly.

View File

@@ -0,0 +1,88 @@
# `errors`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `REACT_LIBP2P_DEMO` error-code registry and `ReactLibp2pDemoError`
class used across the `@suaveplan/react-network-libp2p-demo` package. Every
public surface that fails with a domain-level reason throws a typed
`ReactLibp2pDemoError` rather than a bare `Error`, allowing callers to
discriminate on the stable `code` literal at the boundary without resorting to
message-string matching or `instanceof` chains. The package is a demo UI for
`@suaveplan/network-transport-libp2p`, so the registry covers user-facing
options validation plus the small set of runtime failure modes the demo
surfaces through its log panel.
## Features
- Frozen `REACT_LIBP2P_DEMO_CODES` literal union via `defineCodes()` from
`@suaveplan/error`.
- 5 canonical failure classes: `INVALID_OPTIONS`, `DEMO_DISPOSED`,
`NODE_NOT_STARTED`, `DIAL_FAILED`, `ECHO_FAILED`.
- Type-narrowed `ReactLibp2pDemoErrorCode` for switch-exhaustiveness in
consumer code.
- Class identity preserved via explicit `this.name = "ReactLibp2pDemoError"`.
- ES2022 `cause` chain plumbed through the `SuaveplanErrorOptions` argument so
the underlying libp2p / Zod / transport error stays attached.
## Usage: Basic Example
```ts
import {
REACT_LIBP2P_DEMO_CODES,
ReactLibp2pDemoError,
} from "@suaveplan/react-network-libp2p-demo";
throw new ReactLibp2pDemoError(
REACT_LIBP2P_DEMO_CODES.INVALID_OPTIONS,
"react_libp2p_demo.options.invalid",
);
```
## Usage: Advanced Example
```ts
import {
REACT_LIBP2P_DEMO_CODES,
ReactLibp2pDemoError,
} from "@suaveplan/react-network-libp2p-demo";
function handle(err: unknown): void {
if (err instanceof ReactLibp2pDemoError) {
switch (err.code) {
case REACT_LIBP2P_DEMO_CODES.NODE_NOT_STARTED:
ui.flashWarning("Click Start Node first.");
return;
case REACT_LIBP2P_DEMO_CODES.DIAL_FAILED:
ui.flashError("Dial failed — check the multiaddr.");
return;
default:
ui.flashError(err.code);
}
}
throw err;
}
```
## API Reference
- `REACT_LIBP2P_DEMO_CODES` — frozen record literal of the 5 codes.
- `ReactLibp2pDemoErrorCode` — discriminated string-literal union over the
values.
- `ReactLibp2pDemoError extends SuaveplanError<ReactLibp2pDemoErrorCode>` —
domain error class. Constructor `(code, messageKey, options?)`.
## Implementation Notes
Errors are translation-key-driven (`messageKey`) so user-visible strings can
be resolved through `@suaveplan/i18n` at the boundary. The registry is short
by design — every failure category should map to an actionable demo response
(retry, validate input, restart node). The class is exported alongside the
codes object so callers can both narrow on the `instanceof ReactLibp2pDemoError`
discriminant AND switch on the `code` literal; combining both gives the
strongest type-narrowing in a single check. Avoid catching the base
`SuaveplanError` class — always catch `ReactLibp2pDemoError` specifically so
cross-package error confusion never surfaces as silent recovery in the demo
UI's log feed.

View File

@@ -0,0 +1,116 @@
# `use-libp2p-demo`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - wire-peer-events — Side-effect helper that subscrib…
- [wire-peer-events](./wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
- wire-peer-events — Side-effect helper that subscrib…
- [wire-peer-events](./wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
- wire-peer-events — Side-effect helper that subscrib…
- [wire-peer-events](./wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
- wire-peer-events — Side-effect helper that subscrib…
- [wire-peer-events](./wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
- [wire-peer-events](./wire-peer-events.md) — Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events (`peer:discovery`, `peer:connect`, `peer:d…
## Purpose
State + lifecycle React hook that drives the libp2p demo. Owns the
underlying `Libp2pDemoNode` ref, wires peer-event subscriptions through
`wirePeerEvents`, maintains the discovered + connected peer lists, runs the
append-only log ring, and exposes start / stop / dial / ping / echo /
clearLogs callbacks. The hook is the only stateful piece of the package;
every component below it is presentational and consumes either the result
shape directly or pulled-out slices of it.
## Features
- Validates options through `UseLibp2pDemoOptionsSchema` (Zod) on every
render call; failed validation raises a typed `ReactLibp2pDemoError`
(`INVALID_OPTIONS`).
- Generates monotonic ids for log entries via an internal ref counter so
React's list-reconciler never warns about duplicate keys during fast
event bursts.
- Caps log retention at `maxLogEntries` (default 100) using an
always-trim-then-append ring; reads stay O(1) per append.
- Injectable `clock` (defaults to `Date.now`) for deterministic tests.
- AbortSignal threaded through every async action (`dial`, `ping`, `echo`).
- Idempotent `start` (silently returns when a node already exists) and
`stop` (silently returns when none does).
- Cleans up subscriptions + stops the node on unmount.
## Usage: Basic Example
```tsx
import { useLibp2pDemo } from "@suaveplan/react-network-libp2p-demo";
function MyDemo({ nodeFactory }: { nodeFactory: Libp2pDemoNodeFactory }) {
const demo = useLibp2pDemo({
nodeFactory,
bootstrap: [],
});
return <button type="button" onClick={() => void demo.start()}>Start</button>;
}
```
## Usage: Advanced Example
```tsx
import { useLibp2pDemo } from "@suaveplan/react-network-libp2p-demo";
function FullDemo({ nodeFactory }: { nodeFactory: Libp2pDemoNodeFactory }) {
const demo = useLibp2pDemo({
nodeFactory,
bootstrap: ["/dnsaddr/example/p2p/Qm…"],
subTransport: "webrtc",
listen: ["/webrtc"],
autoStart: true,
clock: () => performance.now(),
maxLogEntries: 250,
});
return (
<>
<p>Peer id: {demo.peerId}</p>
<p>Connected: {demo.connectedPeers.length}</p>
<ul>
{demo.logs.map((l) => (
<li key={l.id}>{l.message}</li>
))}
</ul>
</>
);
}
```
## API Reference
- `UseLibp2pDemoOptions` — `{ nodeFactory: Libp2pDemoNodeFactory;
bootstrap: readonly string[]; subTransport?; listen?; maxConnections?;
dialTimeoutMs?; protocolId?; autoStart?; maxLogEntries?; clock? }`.
- `UseLibp2pDemoResult` — `{ isStarted; peerId; listenAddresses;
discoveredPeers; connectedPeers; logs; lastPingMs; start; stop;
clearLogs; dial; ping; echo; appendLog }`.
- `useLibp2pDemo(options)` — throws `ReactLibp2pDemoError(INVALID_OPTIONS)`
on bad input.
## Implementation Notes
The hook accepts a `Libp2pDemoNodeFactory` (structural — no libp2p type
leaks into the React tree). Production consumers wrap the real libp2p
stack inside the factory; tests pass a deterministic fake driving the
same `Libp2pDemoNode` contract. Auto-start fires once on mount and is
intentionally not retriggered when options change — repeatedly tearing
down + recreating a libp2p node on every prop tweak would make the demo
unusable; consumers can drive lifecycle explicitly through the `start` /
`stop` callbacks. The `_demoCleanup` field stashed on the node instance
holds the wire-events unsubscribe closure so `stop` can call it once
without owning the subscription identities itself. The log ring uses an
`Array.prototype.slice(-(max-1))` trim instead of `Array.splice` to keep
state updates immutable (React's reconciler relies on identity changes).
Disposal flips a ref flag so any in-flight async actions racing the
unmount no longer append logs to a state slot React has already
discarded.

View File

@@ -0,0 +1,74 @@
# `wire-peer-events`
## Purpose
Side-effect helper that subscribes to a `Libp2pDemoNode`'s three peer events
(`peer:discovery`, `peer:connect`, `peer:disconnect`) and translates them into
React state updates on the discovered + connected peer lists plus log-panel
entries. Lives outside the main `useLibp2pDemo` hook file so the hook stays
under the package's 300-LOC source-file cap.
## Features
- Three handlers wired to the node's typed event subscription API.
- Idempotent: connect/discovery events ignore duplicates by peer id.
- Updates both lists on connect — a peer can be present in `discovered`
before its `connect` event fires, and that record needs its `connected`
flag flipped.
- Returns a single cleanup callback that unsubscribes all three handlers
in one call.
- Pure data flow into the supplied setters + log appender — no DOM, no
refs, no timers.
## Usage: Basic Example
```ts
import { wirePeerEvents } from "@suaveplan/react-network-libp2p-demo";
const cleanup = wirePeerEvents({
node,
setDiscoveredPeers,
setConnectedPeers,
appendLog,
});
// later:
cleanup();
```
## Usage: Advanced Example
```ts
import { wirePeerEvents } from "@suaveplan/react-network-libp2p-demo";
useEffect(() => {
const cleanup = wirePeerEvents({
node: nodeRef.current!,
setDiscoveredPeers,
setConnectedPeers,
appendLog,
});
return cleanup;
}, [appendLog]);
```
## API Reference
- `WirePeerEventsArgs` — `{ node, setDiscoveredPeers, setConnectedPeers,
appendLog }`.
- `wirePeerEvents(args: WirePeerEventsArgs): () => void` — returns the
cleanup function.
## Implementation Notes
Cleanup is fire-and-forget — the returned closure invokes
`unsubscribe()` on every captured `DemoSubscription`, never throws, and is
safe to call multiple times because the underlying subscriptions are
idempotent per the structural contract. The dedupe-by-id check uses
`Array.prototype.some` for clarity over a `Map` cache; in the demo's
typical peer-population sizes (≤ 100) the linear scan is dominated by
React's render cost anyway. We translate the three event types into
`PeerInfo` records directly inside the wiring layer so the rest of the
hook stays free of any libp2p-shaped types. Two-list state (`discovered`
and `connected`) is preserved as in the upstream demo because it lets the
peer-list panels render both ‘seen but not yet connected’ and ‘actively
exchanging traffic’ without bespoke filtering in the rendering layer.

View File

@@ -0,0 +1,67 @@
# `log-entry.types`
## Purpose
Defines the `LogLevel` discriminated union and the `LogEntry` value object
that the demo's `<LogPanel>` consumes. Both types are shared across the demo
hook (which appends entries) and the panel (which renders them), so the
contract lives in the top-level `types/` folder to avoid a circular import
between sibling component packages.
## Features
- `LogLevel = "info" | "error" | "warning" | "success"` — four severity
classes mapping cleanly to render colours without sub-string parsing.
- `LogEntry` with `{ id, timestamp, level, message }`, all `readonly`.
- Timestamps are unix-ms numbers — no `Date` objects so the value is
trivially JSON-serialisable for tests.
- Each entry carries a string `id`; the hook generates a monotonic key so
React's reconciler never collides on identical timestamps.
## Usage: Basic Example
```ts
import type { LogEntry } from "@suaveplan/react-network-libp2p-demo";
const entry: LogEntry = {
id: "1",
timestamp: Date.now(),
level: "info",
message: "Node starting…",
};
```
## Usage: Advanced Example
```tsx
import type { LogEntry, LogLevel } from "@suaveplan/react-network-libp2p-demo";
function appendLog(prev: LogEntry[], level: LogLevel, message: string) {
return [
...prev.slice(-100),
{
id: `${prev.length + 1}`,
timestamp: Date.now(),
level,
message,
},
] satisfies LogEntry[];
}
```
## API Reference
- `LogLevel` — string-literal union: `"info" | "error" | "warning" | "success"`.
- `LogEntry` — `{ id: string; timestamp: number; level: LogLevel; message: string }`,
all `readonly`.
## Implementation Notes
The `id` field is mandatory specifically because two log entries appended
inside the same microtask can share a millisecond. Without a stable id,
React's list reconciler would emit a duplicate-key warning on every fast
event burst (peer-discovery storms hit this immediately). Generating ids
inside the hook lets `<LogPanel>` stay a pure presentational component.
The level set is deliberately small — four colours is enough to encode
operator intent (success ✓, warning ⚠, error ✗, info i) without overwhelming
the panel layout, and matches the colour palette used by upstream's demo.

View File

@@ -0,0 +1,65 @@
# `peer-info.types`
## Purpose
Defines the `PeerInfo` snapshot value object that the demo's `<PeerList>`
panel renders. The shape is intentionally structural so the demo never
imports any libp2p type tree directly — the underlying transport
(`@suaveplan/network-transport-libp2p`) translates `peer:discovery`,
`peer:connect`, and `peer:disconnect` events into `PeerInfo` records before
they enter the React tree.
## Features
- `id` — the peer's libp2p PeerID rendered as its base58 string form.
- `protocols` — list of supported multistream protocol ids; empty array when
the peer is only discovered (not yet identified).
- `addresses` — list of dialable multiaddrs.
- `connected` — boolean indicating whether the local node holds an open
connection to the peer right now.
- All fields `readonly`; the array fields are `readonly string[]` so a
consumer accidentally calling `.push()` is a compile-time error.
## Usage: Basic Example
```ts
import type { PeerInfo } from "@suaveplan/react-network-libp2p-demo";
const peer: PeerInfo = {
id: "12D3KooW…",
protocols: ["/ipfs/ping/1.0.0"],
addresses: ["/ip4/192.0.2.1/tcp/4001"],
connected: true,
};
```
## Usage: Advanced Example
```tsx
import type { PeerInfo } from "@suaveplan/react-network-libp2p-demo";
import { PeerList } from "@suaveplan/react-network-libp2p-demo";
function ConnectedOnly({ peers }: { peers: readonly PeerInfo[] }) {
const connected = peers.filter((p) => p.connected);
return <PeerList title="Connected" peers={connected} onSelect={() => undefined} />;
}
```
## API Reference
- `PeerInfo` — `{ id: string; protocols: readonly string[]; addresses:
readonly string[]; connected: boolean }`, all `readonly`.
## Implementation Notes
The structural shape lets the demo render mock data in Storybook without
spinning up a real libp2p node. Two-state design (`connected: boolean`) is
sufficient for the demo UI — production consumers wanting richer state
(idle, dialing, identified-only) should build on top of the transport
package directly rather than extending this DTO. The id is rendered
truncated in the peer-list buttons (`id.substring(0, 20) + "…"`) which is
purely a presentation concern; the full id is preserved on the record so
the dial / ping / echo handlers receive the full string verbatim. Keeping
this type structural also lets test fixtures construct `PeerInfo` records
directly without indirecting through libp2p's `PeerId` constructor, which
keeps unit tests fast and dependency-free.