first commit
This commit is contained in:
99
docs/networking/network/react-network-libp2p-demo/README.md
Normal file
99
docs/networking/network/react-network-libp2p-demo/README.md
Normal 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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user