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,108 @@
# @suaveplan/r3f-networking - R3F-tier multiplayer state synchronisation: interpolation, client-side prediction, snapshot diffing, server reconciliation FSM, React hooks + React-Three-Fiber components. Transport-agnostic — composes any @suaveplan/network Transport.
## Summary
R3F-tier multiplayer state-synchronisation for React-Three-Fiber scenes.
Composes any `@suaveplan/network` `Transport` (canonical:
`@suaveplan/network-transport-webrtc`), `@suaveplan/diff` for snapshot
deltas, `@suaveplan/fsm` for the reconciliation state machine, and
`@suaveplan/serialize` for the wire codec.
<!-- begin:auto-docs -->
## Documentation tree
- [server-authority](./src/authority/server-authority/server-authority.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [server-authority](./src/authority/server-authority/server-authority.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [network-provider-context](./src/components/network-provider/network-provider-context.md) — Module-scope `React.Context` exported as a separate file so consumers can import the context object directly (`useConte…
- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [apply-networked-frame](./src/components/networked-entity/apply-networked-frame.md) — Pure per-frame helper that writes a target `EntityTransform` into a Three.js group-shaped target. Local entities snap d…
- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [wire-codec](./src/core/wire-codec/wire-codec.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [wire-codec](./src/core/wire-codec/wire-codec.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-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [server-authority](./src/authority/server-authority/server-authority.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-provider-context](./src/components/network-provider/network-provider-context.md) — Module-scope `React.Context` exported as a separate file so consumers can import the context object directly (`useConte…
- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [apply-networked-frame](./src/components/networked-entity/apply-networked-frame.md) — Pure per-frame helper that writes a target `EntityTransform` into a Three.js group-shaped target. Local entities snap d…
- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire-codec](./src/core/wire-codec/wire-codec.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire-codec](./src/core/wire-codec/wire-codec.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-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-authority](./src/authority/server-authority/server-authority.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-provider-context](./src/components/network-provider/network-provider-context.md) — Module-scope `React.Context` exported as a separate file so consumers can import the context object directly (`useConte…
- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [apply-networked-frame](./src/components/networked-entity/apply-networked-frame.md) — Pure per-frame helper that writes a target `EntityTransform` into a Three.js group-shaped target. Local entities snap d…
- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire-codec](./src/core/wire-codec/wire-codec.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire-codec](./src/core/wire-codec/wire-codec.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-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-authority](./src/authority/server-authority/server-authority.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-provider-context](./src/components/network-provider/network-provider-context.md) — Module-scope `React.Context` exported as a separate file so consumers can import the context object directly (`useConte…
- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [apply-networked-frame](./src/components/networked-entity/apply-networked-frame.md) — Pure per-frame helper that writes a target `EntityTransform` into a Three.js group-shaped target. Local entities snap d…
- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire-codec](./src/core/wire-codec/wire-codec.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire-codec](./src/core/wire-codec/wire-codec.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-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-authority](./src/authority/server-authority/server-authority.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ - [network-provider-context](./src/components/network-provider/network-provider-context.md) — Module-scope `React.Context` exported as a separate file so consumers can import the context object directly (`useConte…
- [network-provider](./src/components/network-provider/network-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_ - [network-stats](./src/components/network-stats/network-stats.md) — <!-- begin:auto-docs --> _(no submodules)_- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_ - [apply-networked-frame](./src/components/networked-entity/apply-networked-frame.md) — Pure per-frame helper that writes a target `EntityTransform` into a Three.js group-shaped target. Local entities snap d…
- [networked-entity](./src/components/networked-entity/networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_ - [interpolation](./src/core/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_ - [prediction](./src/core/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_ - [reconciliation-fsm](./src/core/reconciliation-fsm/reconciliation-fsm.md) — <!-- begin:auto-docs --> _(no submodules)_- [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_ - [snapshot-diff](./src/core/snapshot-diff/snapshot-diff.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [state-buffer](./src/core/state-buffer/state-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire-codec](./src/core/wire-codec/wire-codec.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wire-codec](./src/core/wire-codec/wire-codec.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-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-network-predict](./src/hooks/use-network-predict/use-network-predict.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-network-sync](./src/hooks/use-network-sync/use-network-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-networked-entity](./src/hooks/use-networked-entity/use-networked-entity.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ - [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_ - [entity-sync](./src/sync/entity-sync/entity-sync.md) — <!-- begin:auto-docs --> _(no submodules)_- [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ - [interest-manager](./src/sync/interest-manager/interest-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_ - [memory-transport](./src/transport/memory-transport/memory-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_ - [network-helpers](./src/utils/network-helpers/network-helpers.md) — <!-- begin:auto-docs --> _(no submodules)_
## What this package owns
- **Interpolation** — frame-loop smoothing of remote-peer transforms
([`core/interpolation`](./src/core/interpolation/interpolation.md)).
- **Client-side prediction** — local input applied immediately, reconciled
against authoritative snapshots
([`core/prediction`](./src/core/prediction/prediction.md)).
- **Reconciliation FSM** — `synchronized → diverged → resyncing` state
machine driven by divergence magnitude
([`core/reconciliation-fsm`](./src/core/reconciliation-fsm/reconciliation-fsm.md)).
- **Snapshot diffing** — RFC-6902 JSON Patch between authoritative
snapshots via `@suaveplan/diff`
([`core/snapshot-diff`](./src/core/snapshot-diff/snapshot-diff.md)).
- **Wire codec** — Zod-validated frame encode/decode via
`@suaveplan/serialize`
([`core/wire-codec`](./src/core/wire-codec/wire-codec.md)).
- **Entity sync + interest management**
([`sync/entity-sync`](./src/sync/entity-sync/entity-sync.md),
[`sync/interest-manager`](./src/sync/interest-manager/interest-manager.md)).
- **Server authority** with anti-cheat hooks
([`authority/server-authority`](./src/authority/server-authority/server-authority.md)).
- **In-memory transport** — deterministic real `Transport` implementation
for integration tests
([`transport/memory-transport`](./src/transport/memory-transport/memory-transport.md)).
- **React hooks** — `useNetworkSync`, `useNetworkedEntity`,
`useNetworkPredict`.
- **R3F components** — `<NetworkProvider>`, `<NetworkedEntity>`,
`<NetworkStats>`.
## Observability
OTel meter `@suaveplan/r3f-networking`. Histograms
`r3f_net.snapshot_dispatch_ms` and `r3f_net.prediction_divergence`;
counters `r3f_net.snapshots_total{outcome}` and
`r3f_net.peer_events_total{event}`.
## Error registry
`defineCodes("R3F_NET", […])` — 8 codes:
`SNAPSHOT_OUT_OF_ORDER`, `PEER_DROPPED`, `PREDICTION_DIVERGED`,
`INVALID_OPTIONS`, `INVALID_SNAPSHOT`, `DISPOSED`, `TRANSPORT_FAILED`,
`ABORTED`.
## License
MIT

View File

@@ -0,0 +1,82 @@
# `server-authority`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Server-authoritative entity manager that validates and applies client
inputs. Detects stale inputs, ownership violations, and oversized deltas
(a basic anti-cheat); rejected inputs return a structured `AuthorityResult`
with a `reason` discriminator so the network layer can decide whether to
log, throttle, or disconnect the offending client.
## Features
- Per-entity `mode`: `server`, `client`, `shared`.
- Monotonic `seq` enforcement — older inputs rejected with `stale-input`.
- Delta-magnitude cap (`maxDelta`) catches teleport-cheats.
- Ownership check ensures `clientId === ownerId` in `server` mode.
- `setTransform(id, t)` for server-driven movement.
- Zod-validated options; bad input throws `R3F_NET.INVALID_OPTIONS`.
## Usage: Basic Example
```ts
import { ServerAuthority } from "@suaveplan/r3f-networking";
const auth = new ServerAuthority({ maxDelta: 1.0 });
auth.registerEntity("e1", "client-A", "server");
const r = auth.processInput({
clientId: "client-A",
entityId: "e1",
seq: 1,
delta: [0.5, 0, 0],
timestamp: 1000,
});
// r.accepted === true
```
## Usage: Advanced Example
Reject oversized deltas while still echoing the result through the wire:
```ts
const auth = new ServerAuthority({ maxDelta: 0.5 });
auth.registerEntity("e1", "client-A");
const r = auth.processInput({
clientId: "client-A",
entityId: "e1",
seq: 1,
delta: [10, 0, 0],
timestamp: 1000,
});
// r.accepted === false, r.reason === "delta-too-large"
transport.sendToClient("client-A", { type: "rejected", body: r });
```
## API Reference
- `new ServerAuthority(config?: ServerAuthorityConfig)`
- `registerEntity(id, ownerId, mode?, initial?)` / `unregisterEntity(id)`
- `processInput(input: AuthorityInput): AuthorityResult`
- `setTransform(id, transform): boolean`
- `getTransform(id)` / `getLastSeq(id)` / `getEntityCount()` /
`getEntityIds()` / `clear()`
## Implementation Notes
`maxDelta` is the raw distance per single input; if your game runs at
60 Hz inputs and 6 m/s max speed, set `maxDelta ≈ 0.11`. The class is
deliberately stateless across clients beyond the `lastSeq` watermark so
horizontal scaling of authoritative shards remains feasible — sharding
strategies live in higher-level network plugins. The owner check only
fires in `server` mode; `shared` and `client` modes accept input from any
client so peer-to-peer scenarios remain expressible. Rejected results
include the offending `seq` so the wire layer can echo a rejection back
to the originating client without resorting to side-channel correlation.
The default fallback transform on rejection is intentionally identity so a
naive consumer never accidentally applies a rejected result to scene
state — the `accepted` boolean must be checked. Validation order is
deterministic: entity-not-found → not-owner → stale-input → delta-too-
large, with no later check obscuring an earlier failure.

View File

@@ -0,0 +1,63 @@
# `network-provider-context`
## Purpose
Module-scope `React.Context` exported as a separate file so consumers can
import the context object directly (`useContext(NetworkContext)`) without
having to also pull in the provider component. Keeping the context in its
own file also avoids a circular-import shape when test utilities want to
read the context value while the provider tree is still being constructed.
## Features
- `NetworkContext: React.Context<NetworkProviderContextValue | null>` —
defaults to `null` so consumers can detect when the provider has not yet
been mounted higher in the tree.
- Pure module — zero runtime side effects on import.
- Lives alongside `network-provider.tsx` so the React component, the context
object, and the prop / value types form a self-contained feature folder.
## Usage: Basic Example
```ts
import { useContext } from "react";
import { NetworkContext } from "@suaveplan/r3f-networking";
const ctx = useContext(NetworkContext);
if (!ctx) throw new Error("NetworkProvider missing");
```
## Usage: Advanced Example
Use the context in a higher-level hook that needs both transport and sync:
```ts
import { useContext } from "react";
import { NetworkContext } from "@suaveplan/r3f-networking";
export function useNetwork() {
const ctx = useContext(NetworkContext);
if (!ctx) throw new Error("NetworkProvider is required");
return ctx;
}
```
## API Reference
- `NetworkContext: React.Context<NetworkProviderContextValue | null>`
## Implementation Notes
The default value is `null` rather than a stub object so a missing-
provider error surfaces immediately as a `TypeError` against the
destructured fields in calling code. This is the canonical React-context
pattern and avoids a "fake provider works silently" footgun when wiring
is in flight. Splitting the context object into its own module keeps
import graphs shallow when the provider component itself is large or
imports heavy dependencies — test utilities and lightweight read-only
consumers can take the context without paying the cost of the provider
side-effects. The context value object is typed against
`NetworkProviderContextValue | null` so the type system enforces the
null-check at every call site; consumers that wrap the context inside a
custom hook like `useNetwork()` should throw a typed error rather than
returning `null` to keep the rest of the app on the strict path.

View File

@@ -0,0 +1,81 @@
# `network-provider`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
React context root for the network stack. Hosts the `Transport` (any
`@suaveplan/network`-compatible transport — WebRTC, in-memory, libp2p) and
the shared `EntitySyncManager`. Child components read both via the
`NetworkContext` exported alongside this file.
## Features
- Transport injection — accepts any `Transport` from `@suaveplan/network`
via prop, never hard-coded to one wire protocol.
- Stable context value across renders via `useMemo`.
- Optional `transport` so consumers can wire it asynchronously once a
signalling handshake completes.
## Usage: Basic Example
```tsx
import {
NetworkProvider,
EntitySyncManager,
createMemoryHub,
MemoryTransport,
} from "@suaveplan/r3f-networking";
const hub = createMemoryHub();
const transport = new MemoryTransport("me", hub);
const sync = new EntitySyncManager({ sendRate: 20 });
<NetworkProvider transport={transport} sync={sync}>
<Scene />
</NetworkProvider>;
```
## Usage: Advanced Example
Compose with the canonical real-time transport once available at the app
boundary:
```tsx
import { createWebRtcTransport } from "@suaveplan/network-transport-webrtc";
const transport = await createWebRtcTransport(config);
return (
<NetworkProvider transport={transport} sync={sync}>
<NetworkedScene />
<NetworkStats snapshot={sync.getSnapshot()} />
</NetworkProvider>
);
```
## API Reference
- `NetworkProvider(props: NetworkProviderProps): ReactElement`
- `NetworkProviderProps { transport?, sync, children? }`
- `NetworkContext: React.Context<NetworkProviderContextValue | null>` exported
from `./network-provider-context.js`.
## Implementation Notes
The context value memoises on the `transport` + `sync` identity pair, so
consumers passing a stable transport reference never trigger context
re-renders. The provider does not subscribe to transport events itself —
those callbacks are expected to live in higher-level scene components
that hook `transport.onMessage` against the sync manager and dispatch
into their game-specific state. Keeping the provider thin avoids forcing
consumers into a one-true-message-handling layout and lets gameplay
layers own the dispatch policy. The `transport` prop is optional so
applications can mount the provider at the top of their R3F tree before
the signalling-handshake completes, then bind the live transport once it
is available; downstream hooks that read `ctx.transport` should defensive-
null-check rather than assume the value is set. Note that swapping the
transport at runtime will replace the context value identity and cascade
through any nested `useContext(NetworkContext)` consumer; consider
splitting transport into a separate context if your tree needs finer-
grained re-render control.

View File

@@ -0,0 +1,64 @@
# `network-stats`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Fixed-position DOM HUD that displays networking statistics — entity counts
and (optionally) RTT / jitter. Rendered outside the R3F canvas so it does
not require `@react-three/drei`. Useful for dev builds, multiplayer demos,
and Storybook playgrounds.
## Features
- 4 corner positions: `top-left | top-right | bottom-left | bottom-right`.
- Optional `latency` block: RTT, average RTT, jitter (1-decimal millis).
- `visible` toggle for fast hide/show without unmount.
- Pointer events disabled so the HUD never intercepts canvas input.
- `style` prop for per-instance theming.
## Usage: Basic Example
```tsx
import { NetworkStats, EntitySyncManager } from "@suaveplan/r3f-networking";
const sync = new EntitySyncManager();
<NetworkStats snapshot={sync.getSnapshot()} />;
```
## Usage: Advanced Example
Show latency, bottom-left, with a custom dark-mode tint:
```tsx
<NetworkStats
snapshot={sync.getSnapshot()}
latency={{ rtt: 32, avgRtt: 35, jitter: 1.4, sampleCount: 60 }}
position="bottom-left"
style={{ background: "rgba(20,20,20,0.9)" }}
/>;
```
## API Reference
- `NetworkStats(props: NetworkStatsProps): ReactElement | null`
- `NetworkStatsProps { snapshot, latency?, position?, visible?, style? }`
- `NetworkStatsPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right"`
## Implementation Notes
The overlay is a single fixed-position `<div>` — no portal, no canvas
overlay — so it composes safely with any layout. Stats are computed inside
`useMemo` over `snapshot` and `latency` identities so the panel re-renders
only when the underlying numbers change, never on every R3F frame. The
monospace font preserves column alignment when latency numbers fluctuate
between one and three digits, which is essential for the multiplayer
debugging case the overlay is designed for. `pointerEvents: "none"` is
applied to the root so mouse drags pass through to the underlying canvas
even when the panel covers visible UI; consumers that want a clickable
overlay should clone the component and remove the line. The `style` prop
spreads last so caller-supplied colour, font, or border overrides win
against the package defaults. Latency block rendering is gated on the
presence of the `latency` prop so the panel collapses to entity counts
when no transport has surfaced an RTT estimate yet.

View File

@@ -0,0 +1,56 @@
# `apply-networked-frame`
## Purpose
Pure per-frame helper that writes a target `EntityTransform` into a Three.js
group-shaped target. Local entities snap directly; remote entities are
smoothed via `lerpVec3` for position and `slerpQuat` for rotation. The
helper is exported so it can be tested without an R3F `<Canvas>` and reused
by alternative renderers (e.g. ECS frameworks bridging Three.js).
## Features
- Branchless position write for local entities (no smoothing — caller
already owns the source of truth).
- Lerp + slerp smoothing for remote entities with a tunable smoothing factor.
- `DEFAULT_SMOOTH_FACTOR = 0.15` constant — calibrated for 60 Hz frame
loops with 20 Hz authoritative ticks.
- Operates on a structural `GroupLike` shape so non-`THREE.Group` targets
can be threaded in for testing.
## Usage: Basic Example
```ts
import { applyNetworkedFrame } from "@suaveplan/r3f-networking";
useFrame(() => {
if (groupRef.current) {
applyNetworkedFrame(groupRef.current, transform, isLocal);
}
});
```
## Usage: Advanced Example
Drive a custom smoothing rate per entity (e.g. slower for ragdolls, instant
for hitscan markers):
```ts
applyNetworkedFrame(group, target, isLocal, 0.05); // very slow
applyNetworkedFrame(group, target, isLocal, 1.0); // instant
```
## API Reference
- `applyNetworkedFrame(group: GroupLike, target: EntityTransform, isLocal: boolean, smoothFactor?: number): void`
- `DEFAULT_SMOOTH_FACTOR = 0.15`
- `GroupLike` — structural shape with `position`, `quaternion`, `scale` each
exposing `x`/`y`/`z` (and `w` for quaternion) plus a `set(...)` method.
## Implementation Notes
The helper is intentionally allocation-light — it constructs two short
arrays inside `lerpVec3` / `slerpQuat`. For very large scenes a future
optimisation would write directly into the target via mutating helpers, but
the JIT inlines the hot path well enough that the marginal cost is below
the noise floor of the R3F frame loop.

View File

@@ -0,0 +1,81 @@
# `networked-entity`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
R3F component that smoothly interpolates a Three.js `<group>` toward a
caller-supplied `transform` every frame. Local entities snap directly;
remote entities lerp/slerp at the supplied `smoothFactor`. The per-frame
logic is extracted into the pure `applyNetworkedFrame` helper for testing.
## Features
- Wraps arbitrary R3F children inside a `<group>` named `networked-<id>`.
- Local-entity branch: instant write (caller owns the truth).
- Remote-entity branch: per-frame lerp + slerp toward the target.
- `visible` toggle for fast hide / cull paths.
- `smoothFactor` override per instance.
## Usage: Basic Example
```tsx
import { NetworkedEntity } from "@suaveplan/r3f-networking";
<NetworkedEntity
entityId="peer-1"
isLocal={false}
transform={remoteTransform}
>
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>
</NetworkedEntity>;
```
## Usage: Advanced Example
Combine with the sync manager + sync hook to render every peer in the scene:
```tsx
function PeerEntities() {
const sync = useNetworkSync();
return Array.from(peers).map((id) => (
<NetworkedEntity
key={id}
entityId={id}
isLocal={false}
transform={sync.getTransform(id) ?? IDENTITY_TRANSFORM}
smoothFactor={0.2}
>
<PeerAvatar id={id} />
</NetworkedEntity>
));
}
```
## API Reference
- `NetworkedEntity(props: NetworkedEntityProps): ReactElement`
- `NetworkedEntityProps {
entityId, isLocal, transform, position?, visible?, children?, smoothFactor?
}`
## Implementation Notes
The smoothing happens inside `useFrame` rather than React state, so the
component never re-renders on transform change — only on prop-identity
changes (e.g. `entityId`, `visible`). The Three.js group is wrapped with
a `name = "networked-<id>"` for debugger inspection and the Storybook
tree view. Local entities skip the lerp/slerp pass because the caller
already owns the source of truth; remote entities must always smooth or
the per-frame jitter from authoritative-tick boundaries becomes visible
at 20 Hz update rates. The default smooth factor of 0.15 produces an
~85 ms convergence half-life — short enough to stay tight on the
authoritative ground truth, long enough to absorb a one-frame jitter
without visible snap. The `initialPos` fallback to `transform.position`
ensures the group spawns at the correct world location even if `position`
is not supplied, eliminating the one-frame flash where a freshly mounted
group would otherwise sit at origin until the first `useFrame` runs.

View File

@@ -0,0 +1,75 @@
# `interpolation`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Frame-loop smoothing of remote-peer transforms from a `StateBuffer`. The
interpolator renders entities "in the past" by a fixed delay so that two
authoritative snapshots always bracket the current render time, producing a
continuous transform stream every frame. Optional extrapolation handles the
short tail when packets stall.
## Features
- Fixed-delay rendering with configurable `delay` (default 100 ms).
- Linear position + scale, slerp rotation between bracketing states.
- Optional linear extrapolation with `maxExtrapolation` clamp and confidence
scaling that falls to zero at the clamp boundary.
- Zod-validated options; bad input throws `R3F_NET.INVALID_OPTIONS`.
- Stand-alone `interpolateTransform(a, b, t)` for ad-hoc lerps.
## Usage: Basic Example
```ts
import { StateBuffer, EntityInterpolator } from "@suaveplan/r3f-networking";
const buf = new StateBuffer<EntityTransform>();
const interp = new EntityInterpolator({ delay: 100 });
function frame(serverTime: number): EntityTransform {
return interp.interpolate(buf, serverTime).transform;
}
```
## Usage: Advanced Example
Enable extrapolation for high-latency mobile sessions and react to confidence:
```ts
const interp = new EntityInterpolator({
delay: 80,
extrapolate: true,
maxExtrapolation: 250,
});
function frame(buf, t: number) {
const r = interp.interpolate(buf, t);
if (r.confidence < 0.4) flashLagWarning();
return r.transform;
}
```
## API Reference
- `new EntityInterpolator(config?: InterpolationConfig)`
- `interpolate(buffer, serverTime): InterpolationResult`
- `getDelay(): number` / `isExtrapolationEnabled(): boolean`
- `interpolateTransform(a, b, t): EntityTransform`
## Implementation Notes
`renderTime = serverTime - delay` lets `findInterpolationPair` operate on
a sufficiently old timestamp that incoming snapshots can still arrive in
time. The extrapolation branch projects position by a linear velocity
computed from the most recent bracketing pair; rotation and scale are
held at the last known frame because higher-order extrapolation is
prone to whip-around artefacts under packet loss. Confidence falls
linearly from 1.0 → 0.0 across the `maxExtrapolation` window so HUD
overlays can decay smoothly without a binary "stale" cliff. A buffer
with a single state still produces a usable result at `confidence: 0.5`
so the per-frame render path can blindly call `interpolate()` even when
the network has not delivered a second snapshot yet. The default delay
of 100 ms is calibrated for 20 Hz authoritative ticks; raise it to 150 ms
for noisier connections, lower it to 50 ms for LAN.

View File

@@ -0,0 +1,72 @@
# `prediction`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Client-side prediction with server reconciliation. Each local input is
applied immediately for responsiveness, recorded in a bounded history queue,
and replayed on top of the next authoritative server transform so the
visible state converges with the server without losing recent inputs.
## Features
- Bounded input-history queue with `maxHistory` cap.
- Snap-vs-smooth correction: large divergence snaps directly, small
divergence interpolates at `correctionRate`.
- Records every reconciliation error to the `r3f_net.prediction_divergence`
histogram.
- Injectable clock for deterministic property tests.
- Zod-validated options; bad input throws `R3F_NET.INVALID_OPTIONS`.
## Usage: Basic Example
```ts
import { ClientPredictor } from "@suaveplan/r3f-networking";
const pred = new ClientPredictor([0, 0, 0]);
const applied = pred.applyInput([1, 0, 0]); // { position: [1,0,0], seq: 1 }
const reconciled = pred.reconcile(authoritative, lastAcked);
```
## Usage: Advanced Example
Inject a fake clock for deterministic property testing and observe convergence:
```ts
import { ClientPredictor } from "@suaveplan/r3f-networking";
const clock = { ms: 0, now() { return this.ms; } };
const pred = new ClientPredictor([0, 0, 0], { snapThreshold: 2 }, clock);
for (let i = 0; i < 100; i++) {
clock.ms += 16;
pred.applyInput([0.1, 0, 0]);
}
const r = pred.reconcile(server, 50);
expect(r.replayed).toBeLessThanOrEqual(60); // maxHistory cap
```
## API Reference
- `new ClientPredictor(initial?: Vec3, config?: PredictionConfig, clock?: PredictionClock)`
- `applyInput(delta: Vec3) → { position, seq }`
- `reconcile(serverTransform, lastAckedSeq) → ReconciliationResult`
- `getPosition()` / `getSeq()` / `getPendingCount()` / `reset(pos?)`
## Implementation Notes
History is pruned by count rather than age — `maxHistory` should equal
at least `RTT × inputRate` to cover the longest expected round-trip. For
example, a 200 ms RTT at 60 Hz inputs implies a tail of 12 frames, well
under the default `maxHistory` of 60. Errors below `0.001` are skipped
to avoid microscopic drift that would jitter the rendered position
without measurable benefit. The `snapThreshold` is in metres; tune it
with the per-game world unit — at typical 1 m scale, a 3 m threshold
catches teleport-style cheats while leaving genuine network-jitter
corrections to the smoothing path. Reconciliation always records the
divergence to the OTel histogram so dashboards over the meter
`@suaveplan/r3f-networking` can plot tail-latency-correlated divergence
without instrumentation in the caller. The injectable clock supports
deterministic property testing under a fake-clock fixture, and a
production caller should never override the default.

View File

@@ -0,0 +1,68 @@
# `reconciliation-fsm`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
A three-state finite state machine that drives the client-side reaction to
authoritative-state divergence. `synchronized` is steady state, `diverged`
is the warning band entered after one snapshot exceeds `snapThreshold`,
`resyncing` is the recovery state entered after `maxConsecutive` divergent
snapshots in a row. Composes `@suaveplan/fsm` so transition history,
guards, and OTel metrics come for free.
## Features
- Two events: `SNAPSHOT { divergence, seq }` and `RESYNC` / `RESYNCED`
control transitions.
- Snap-threshold and max-consecutive bands configurable via the strict
`ReconciliationFsmOptions` schema.
- `assign()` action keeps the running counter inside the FSM context;
exiting `resyncing` resets it.
- Bad options throw `R3F_NET.INVALID_OPTIONS`.
## Usage: Basic Example
```ts
import { buildReconciliationMachine } from "@suaveplan/r3f-networking";
const fsm = buildReconciliationMachine({ snapThreshold: 2 });
fsm.send({ type: "SNAPSHOT", payload: { divergence: 0.4, seq: 12 } });
// fsm.current === "synchronized"
fsm.send({ type: "SNAPSHOT", payload: { divergence: 5.0, seq: 13 } });
// fsm.current === "diverged"
```
## Usage: Advanced Example
Drive the FSM from a `ClientPredictor.reconcile` result to coordinate UI:
```ts
const fsm = buildReconciliationMachine({ snapThreshold: 1.5, maxConsecutive: 5 });
function onServerFrame(frame, lastAcked) {
const r = predictor.reconcile(frame.transform, lastAcked);
fsm.send({
type: "SNAPSHOT",
payload: { divergence: r.error, seq: frame.seq },
});
if (fsm.current === "resyncing") requestFullSnapshot();
}
```
## API Reference
- `buildReconciliationMachine(options?: ReconciliationFsmOptions): FSMInterpreter<ReconciliationContext>`
- States: `synchronized | diverged | resyncing`.
- Events: `SNAPSHOT { divergence, seq } | RESYNC | RESYNCED`.
## Implementation Notes
The state machine is intentionally minimal — multi-band thresholds
(`severe`, `catastrophic`) are deferred to consumers because the right shape
varies per game. The `consecutiveDiverged` counter resets on any below-
threshold snapshot, so transient spikes do not promote the machine all the
way to `resyncing`. `RESYNC` is exposed as an explicit transport-level
event so a peer-drop event can force the recovery state without waiting for
the divergence counter to fill.

View File

@@ -0,0 +1,74 @@
# `snapshot-diff`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Computes and applies entity-snapshot deltas as RFC-6902 JSON Patches via
`@suaveplan/diff`. The server authors one full snapshot followed by a stream
of patches; the client applies each patch against its mirror of the same
base snapshot. Bandwidth scales with the number of changed entities rather
than the scene size.
## Features
- `diffSnapshots(from, to)` — minimal JSON Patch between two snapshots.
- `buildDeltaFrame(base, next)` — full wire-ready delta with `baseSeq`,
`seq`, `timestamp`, `patch`.
- `applyDeltaFrame(base, delta)` — apply on the receiver; rejects mismatched
`baseSeq` with `R3F_NET.INVALID_SNAPSHOT`.
- Zod-validated wire frames; bad input throws `R3F_NET.INVALID_SNAPSHOT`.
- Sequence-monotonic — `next.seq > base.seq` enforced or
`R3F_NET.SNAPSHOT_OUT_OF_ORDER`.
## Usage: Basic Example
```ts
import {
buildDeltaFrame,
applyDeltaFrame,
type SnapshotFrame,
} from "@suaveplan/r3f-networking";
const base: SnapshotFrame = { seq: 1, timestamp: 1000, state: { a: T1 } };
const next: SnapshotFrame = { seq: 2, timestamp: 1050, state: { a: T2 } };
const delta = buildDeltaFrame(base, next);
const reconstructed = applyDeltaFrame(base, delta);
```
## Usage: Advanced Example
Property-test round-trip equivalence with `fast-check`:
```ts
fc.assert(
fc.property(snapArb, snapArb, (a, b) => {
const delta = buildDeltaFrame(
{ seq: 1, timestamp: 0, state: a },
{ seq: 2, timestamp: 1, state: b }
);
const out = applyDeltaFrame({ seq: 1, timestamp: 0, state: a }, delta);
expect(out.state).toEqual(b);
})
);
```
## API Reference
- `diffSnapshots(from, to): Patch`
- `buildDeltaFrame(base, next): SnapshotDeltaFrame` —
throws `SNAPSHOT_OUT_OF_ORDER`.
- `applyDeltaFrame(base, delta): SnapshotFrame` —
throws `INVALID_SNAPSHOT` on base mismatch.
- `validateSnapshotFrame(unknown): SnapshotFrame`
- `validateSnapshotDeltaFrame(unknown): SnapshotDeltaFrame`
## Implementation Notes
The RFC-6902 patch is produced by `@suaveplan/diff` with the default index
strategy — array order is preserved verbatim and individual leaves emit
`replace` ops. Because entity maps are object-shaped, `add` / `remove` ops
appear when entities join or leave the interest set. The base-seq fence
prevents the client from applying a patch over the wrong snapshot when
packets reorder at the transport layer.

View File

@@ -0,0 +1,68 @@
# `state-buffer`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Bounded buffer of timestamped entity states. Stores the last N authoritative
snapshots received from the network and answers bracketing-pair queries at a
target render time so the interpolator can produce a smooth transform every
frame. Both the **size** cap and **age** cap are enforced on every push to
keep memory steady under arbitrary update rates.
## Features
- O(1) `push`, O(n) bracketing-pair lookup for a target timestamp.
- Dual prune: `maxSize` count cap + `maxAge` millisecond cap.
- Rejects out-of-order pushes with `R3F_NET.SNAPSHOT_OUT_OF_ORDER`.
- Zod-validated options on construction; invalid input throws
`R3F_NET.INVALID_OPTIONS`.
- Pair-lookup returns clamped endpoints when the requested render time is
before the oldest or after the newest state so the interpolator never has
to special-case empty data on the hot path.
## Usage: Basic Example
```ts
import { StateBuffer } from "@suaveplan/r3f-networking";
const buf = new StateBuffer<EntityTransform>({ maxSize: 64, maxAge: 1500 });
buf.push({ timestamp: 1000, state: { position: [0, 0, 0], rotation: [0,0,0,1], scale: [1,1,1] } });
buf.push({ timestamp: 1100, state: { position: [1, 0, 0], rotation: [0,0,0,1], scale: [1,1,1] } });
const pair = buf.findInterpolationPair(1050);
```
## Usage: Advanced Example
Drive an interpolation loop against the buffer with a fixed delay:
```ts
import { StateBuffer, EntityInterpolator } from "@suaveplan/r3f-networking";
const buf = new StateBuffer<EntityTransform>();
const interp = new EntityInterpolator({ delay: 100 });
function onTick(serverTime: number): EntityTransform {
return interp.interpolate(buf, serverTime).transform;
}
```
## API Reference
- `new StateBuffer<T>(config?: StateBufferConfig)` — throws `INVALID_OPTIONS`
on bad input.
- `push(state)` — throws `SNAPSHOT_OUT_OF_ORDER` for backwards timestamps.
- `size()` / `latest()` / `oldest()` — accessors.
- `findInterpolationPair(timestamp)` — bracketing pair or `null` when
`size < 2`.
- `getSnapshot()` — diagnostic frame for HUD overlays.
- `clear()` — resets buffer.
## Implementation Notes
The buffer is array-backed for cache locality and simple iteration. Pruning
runs amortised O(1) per push. The pair-lookup short-circuits inside the
in-range loop; only the edge branches walk to the array endpoints. Out-of-order
rejection is enforced at the boundary rather than silently sorted — late frames
must surface as an error so the transport layer can take corrective action.

View File

@@ -0,0 +1,77 @@
# `wire-codec`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Encodes and decodes snapshot and delta frames for the wire via
`@suaveplan/serialize`. Decode is fronted by Zod boundary validation so any
malformed input (truncated payload, wrong types, missing fields) surfaces as
a typed `R3F_NET.INVALID_SNAPSHOT` rather than a stray runtime exception
inside the multiplayer hot path.
## Features
- `encodeSnapshotFrame` / `decodeSnapshotFrame` round-trip pair.
- `encodeDeltaFrame` / `decodeDeltaFrame` round-trip pair.
- Tagged-JSON encoding inherits the `@suaveplan/serialize` default codec —
`Date`, `Map`, `Set`, `BigInt`, `Uint8Array`, `URL`, `RegExp` all preserved
byte-for-byte.
- Decode errors funnel through `R3FNetError(INVALID_SNAPSHOT)` with the
underlying cause attached.
## Usage: Basic Example
```ts
import {
encodeSnapshotFrame,
decodeSnapshotFrame,
} from "@suaveplan/r3f-networking";
const wire = encodeSnapshotFrame(frame);
const restored = decodeSnapshotFrame(wire);
```
## Usage: Advanced Example
Wrap the encode call in an observability span if your application uses an
explicit tracer:
```ts
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("netcode");
tracer.startActiveSpan("snapshot.encode", (span) => {
const wire = encodeSnapshotFrame(frame);
span.setAttribute("bytes", wire.length);
span.end();
transport.send(peerId, { type: "snapshot", body: wire });
});
```
## API Reference
- `encodeSnapshotFrame(frame: SnapshotFrame): string`
- `decodeSnapshotFrame(wire: string): SnapshotFrame` — throws
`R3F_NET.INVALID_SNAPSHOT` on parse failure or schema violation.
- `encodeDeltaFrame(frame: SnapshotDeltaFrame): string`
- `decodeDeltaFrame(wire: string): SnapshotDeltaFrame` — throws
`R3F_NET.INVALID_SNAPSHOT` on parse failure or schema violation.
## Implementation Notes
Errors raised by `@suaveplan/serialize` are re-wrapped to keep the
public-facing error surface bounded to `R3F_NET.*` codes. Already-typed
`R3FNetError` instances are passed through unchanged so the `cause` chain
stays a single error class deep, which makes log diagnostics readable
without manual cause-chain traversal. The encoder uses the tagged-JSON
codec by default; an alternate MessagePack subpath is available via the
underlying `@suaveplan/serialize` package and can be wired by replacing
`encode`/`decode` here in a future revision. For very high-frequency
multiplayer scenes prefer the MessagePack codec — it produces ~30%
smaller payloads at the cost of an extra import and a switch from `string`
to `Uint8Array` on the wire. The decoder validates the structural shape
of every snapshot AND every delta frame because malformed wire bytes from
a malicious or buggy peer must never make it into the sync manager and
corrupt the local mirror of authoritative state.

View File

@@ -0,0 +1,80 @@
# `errors`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `R3F_NET` error-code registry and `R3FNetError` class used across the
`@suaveplan/r3f-networking` package. Every public surface that fails with a
domain-level reason throws a typed `R3FNetError` 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.
## Features
- Frozen `R3F_NET_CODES` literal union via `defineCodes()` from
`@suaveplan/error`.
- 8 canonical failure classes: `SNAPSHOT_OUT_OF_ORDER`, `PEER_DROPPED`,
`PREDICTION_DIVERGED`, `INVALID_OPTIONS`, `INVALID_SNAPSHOT`, `DISPOSED`,
`TRANSPORT_FAILED`, `ABORTED`.
- Type-narrowed `R3FNetErrorCode` for switch-exhaustiveness in consumer code.
- Class identity preserved via explicit `this.name = "R3FNetError"`.
## Usage: Basic Example
```ts
import { R3FNetError, R3F_NET_CODES } from "@suaveplan/r3f-networking";
throw new R3FNetError(
R3F_NET_CODES.INVALID_OPTIONS,
"r3f_net.options.invalid",
{ context: { field: "delay" } }
);
```
## Usage: Advanced Example
```ts
import { R3FNetError, R3F_NET_CODES } from "@suaveplan/r3f-networking";
function handle(err: unknown): void {
if (err instanceof R3FNetError) {
switch (err.code) {
case R3F_NET_CODES.SNAPSHOT_OUT_OF_ORDER:
metrics.outOfOrder.inc();
return;
case R3F_NET_CODES.PEER_DROPPED:
router.requestRejoin();
return;
default:
logger.error("r3f_net unhandled", { code: err.code });
}
}
throw err;
}
```
## API Reference
- `R3F_NET_CODES` — frozen record literal of the 8 codes.
- `R3FNetErrorCode` — discriminated string-literal union over the values.
- `R3FNetError extends SuaveplanError<R3FNetErrorCode>` — 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 code list
is short by design — every failure category should map to operator action
(rejoin, refresh, log, retry). Adding a code requires bumping the cross-
package contract and updating every consumer's switch-exhaustiveness
clause. The class is exported alongside the codes object so callers can
both narrow on the `instanceof R3FNetError` discriminant AND switch on the
`code` literal; combining both gives the strongest type-narrowing in a
single check. The `cause` option propagates a wrapped error through the
standard ES2022 cause chain so the original Zod failure, schema error, or
transport rejection remains attached for debugging without leaking
implementation details to the public type. Avoid catching the base
`SuaveplanError` class — always catch `R3FNetError` specifically so cross-
package error confusion never surfaces as silent recovery.

View File

@@ -0,0 +1,76 @@
# `use-network-predict`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
React hook exposing a memoised `ClientPredictor` so a local-input movement
loop can apply moves immediately while pending server acknowledgement.
Reconciliation collapses the local divergence each time a server snapshot
arrives.
## Features
- Memoised predictor instance — stable across renders.
- `applyInput(delta)` returns `{ position, seq }` for outbound wire frames.
- `reconcile(serverTransform, lastAckedSeq)` returns the divergence summary.
- `getPosition` / `getSeq` / `reset` for inspection and re-entry.
## Usage: Basic Example
```tsx
function LocalPlayer() {
const { applyInput, reconcile, getPosition } = useNetworkPredict({
initialPosition: [0, 0, 0],
snapThreshold: 2.0,
});
useFrame(() => {
const r = applyInput([0.05, 0, 0]);
transport.send("server", buildInputEnvelope(r));
});
return <mesh position={getPosition()} />;
}
```
## Usage: Advanced Example
Combine with the reconciliation FSM to gate UI overlays:
```tsx
const fsm = useMemo(() => buildReconciliationMachine(), []);
const { reconcile } = useNetworkPredict();
function onServerFrame(frame) {
const r = reconcile(frame.transform, frame.seq);
fsm.send({
type: "SNAPSHOT",
payload: { divergence: r.error, seq: frame.seq },
});
}
```
## API Reference
- `useNetworkPredict(options?: UseNetworkPredictOptions): UseNetworkPredictResult`
- `UseNetworkPredictOptions extends PredictionConfig { initialPosition? }`
- `UseNetworkPredictResult { applyInput, reconcile, getPosition, getSeq, reset }`
## Implementation Notes
The predictor instance is memoised on the option scalars so consumers
can pass a fresh object every render without rebuilding the predictor —
the shallow comparison on `initialPosition`, `maxHistory`,
`snapThreshold`, `correctionRate` is sufficient because all four are
primitives. Note that `initialPosition` is compared by reference identity
rather than element-wise — passing `[0, 0, 0]` as a literal in the JSX
call site will rebuild the predictor every render, which is rarely what
consumers want. The fix is to hoist the tuple into a module-scoped
constant or pass `undefined` to opt into the zero-position default. The
hook exposes pull-based accessors (`getPosition`, `getSeq`) rather than
React state so per-frame reads inside `useFrame` do not trigger React
re-renders. Reconciliation is exposed without an event listener — callers
wire transport messages to the `reconcile` callback themselves so the
predictor never owns any external IO.

View File

@@ -0,0 +1,71 @@
# `use-network-sync`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
React hook that wraps an `EntitySyncManager` and exposes a small reactive
surface: register/unregister entities, read the current snapshot, and look
up the current interpolated transform by id. The manager instance is
memoised on the `sendRate` + `interpolationDelay` pair so options should be
stable across renders.
## Features
- Memoised `EntitySyncManager` per option pair.
- Snapshot kept in React state — re-renders fire on register/unregister.
- `getTransform(id)` reads the latest interpolated transform.
- No `useEffect` lifecycle: the manager has no external IO so cleanup is
unnecessary; callers wire transports through `useEffect` in their own
parent components.
## Usage: Basic Example
```tsx
import { useNetworkSync } from "@suaveplan/r3f-networking";
function Game() {
const sync = useNetworkSync({ sendRate: 30 });
return <button onClick={() => sync.registerEntity("e1", "me", true)}>Spawn</button>;
}
```
## Usage: Advanced Example
Drive the manager from a `useFrame` hook inside R3F and broadcast updates:
```tsx
function NetworkedScene({ transport }) {
const sync = useNetworkSync({ sendRate: 20 });
useFrame((state) => {
const t = state.clock.elapsedTime * 1000;
if (sync.snapshot.localCount > 0) {
// EntitySyncManager exposes collectLocalUpdates through methods that
// a higher-level provider can plumb in.
}
});
return <Scene />;
}
```
## API Reference
- `useNetworkSync(options?: UseNetworkSyncOptions): UseNetworkSyncResult`
- `UseNetworkSyncOptions { sendRate?, interpolationDelay? }`
- `UseNetworkSyncResult { registerEntity, unregisterEntity, snapshot, getTransform }`
## Implementation Notes
The hook is intentionally thin: the manager owns all state. Pull-based
APIs like `getTransform` always return the freshest value; push-based
state (`snapshot`) re-renders only on registration changes so per-frame
transform updates do not invalidate the React tree. Wiring the transport
to the manager is the caller's responsibility — the hook does not assume
any particular wire protocol so the same hook composes with the in-memory
test transport, WebRTC, WebSocket, libp2p, or any future transport that
satisfies the `@suaveplan/network` `Transport` contract. Memoisation
keys are kept to primitives so passing a fresh options object every
render does not rebuild the manager; this is the canonical pattern
recommended by the React team for hooks that own a long-lived object
instance.

View File

@@ -0,0 +1,70 @@
# `use-networked-entity`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Hook that exposes the current transform of one networked entity. The
returned `transform` is the value most recently written by the sync layer;
the `active` flag indicates whether the entity is currently considered
connected. The hook is intentionally small so consumers can compose it
inside their own `useFrame` loop without paying for a re-render on every
frame.
## Features
- Stable `transform` reference that mirrors the entity's current state.
- `active` flag for HUD overlays and conditional renders.
- Initial `position` override for spawning at non-origin.
## Usage: Basic Example
```tsx
function NetworkedAvatar({ id }: { id: string }) {
const { transform, active } = useNetworkedEntity({
entityId: id,
isLocal: false,
});
if (!active) return null;
return <mesh position={transform.position} />;
}
```
## Usage: Advanced Example
Use with `useFrame` to drive a smoothed `<group>` directly:
```tsx
function PeerAvatar({ id }) {
const groupRef = useRef<THREE.Group>(null);
const { transform } = useNetworkedEntity({ entityId: id, isLocal: false });
useFrame(() => {
groupRef.current?.position.set(...transform.position);
});
return <group ref={groupRef} />;
}
```
## API Reference
- `useNetworkedEntity(options: UseNetworkedEntityOptions): UseNetworkedEntityResult`
- `UseNetworkedEntityOptions { entityId, isLocal, position?, interpolationDelay? }`
- `UseNetworkedEntityResult { transform, active }`
## Implementation Notes
Transform updates are intentionally not pushed through React state on
every frame — that would re-render the component 60 times per second and
defeat the purpose of pulling smoothing into the R3F frame loop.
Consumers that need React-state-bound updates should poll inside
`useFrame` and call `setState` only on coarse events such as a HUD label
text change or a damage flash. The hook intentionally exposes a minimal
surface — just `transform` and `active` — because the heavy lifting
belongs in the sync manager and the renderer-shaped `<NetworkedEntity>`
component. Wrap this hook inside a higher-level domain hook (e.g.
`usePlayerEntity`) if you need to expose health, score, or other gameplay-
specific state alongside the transform. The `interpolationDelay` option
is reserved for future versions where the hook will resolve its own
buffer; the current version inherits the global delay from
`useNetworkSync`.

View File

@@ -0,0 +1,69 @@
# `observability`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OTel meter wiring for the whole `@suaveplan/r3f-networking` package. Exposes
two histograms, two counters, and a small recorder facade. Every
externally-visible operation that affects multiplayer state is instrumented
here so dashboards over the meter `@suaveplan/r3f-networking` can be
assembled without reading code.
## Features
- Histogram `r3f_net.snapshot_dispatch_ms` (unit `ms`) — time spent dispatching
one authoritative server snapshot through the local sync pipeline.
- Histogram `r3f_net.prediction_divergence` (unit `m`) — magnitude of the last
reconciliation error.
- Counter `r3f_net.snapshots_total{outcome}` — snapshot frames received, labelled
with `accepted | out_of_order | rejected`.
- Counter `r3f_net.peer_events_total{event}` — peer lifecycle events, labelled
with `connected | dropped`.
- Convenience `recordSnapshot(outcome)` + `recordPeerEvent(event)` helpers
with the label vocabulary locked at compile time.
## Usage: Basic Example
```ts
import { recordSnapshot, snapshotDispatchMs } from "@suaveplan/r3f-networking";
const t0 = performance.now();
applySnapshot(frame);
snapshotDispatchMs.record(performance.now() - t0);
recordSnapshot("accepted");
```
## Usage: Advanced Example
Wire the counter to a divergence histogram in the reconciliation path:
```ts
import {
predictionDivergence,
recordSnapshot,
} from "@suaveplan/r3f-networking";
const result = predictor.reconcile(authoritative, lastAckSeq);
predictionDivergence.record(result.error);
recordSnapshot(result.error > snapThreshold ? "rejected" : "accepted");
```
## API Reference
- `snapshotDispatchMs: Histogram` — duration per server-snapshot dispatch.
- `predictionDivergence: Histogram` — magnitude of reconciliation error.
- `snapshotsTotal: Counter` — total snapshots, with the `outcome` attribute.
- `peerEventsTotal: Counter` — total peer events, with the `event` attribute.
- `recordSnapshot(outcome: SnapshotOutcome): void`
- `recordPeerEvent(event: "connected" | "dropped"): void`
- `type SnapshotOutcome = "accepted" | "out_of_order" | "rejected"`.
## Implementation Notes
The meter is acquired once at module evaluation; consumers configuring an SDK
later in startup will still pick up these instruments via the OTel global
provider lazy-rebind contract. Attribute vocabularies are deliberately small
to keep label cardinality bounded. No business-string labels (peer IDs,
entity IDs) are emitted at the histogram or counter level.

View File

@@ -0,0 +1,81 @@
# `entity-sync`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Top-level manager for networked entity state. Tracks every entity by id,
buffers remote-peer history for interpolation, samples local entities at the
configured send rate, and exposes the diagnostic snapshot needed by HUD
overlays. Hot-path methods stay allocation-free except for the result array
returned by `collectLocalUpdates`.
## Features
- Per-entity `StateBuffer` for remote sources, automatic `EntityInterpolator`
binding.
- Per-frame `updateRemoteEntities(serverTime)` walks every remote and writes
the interpolated transform back to the entity record.
- `shouldSend(currentTime)` gates outbound messages against the configured
send rate.
- Records `r3f_net.snapshots_total{outcome}` for every receive, including the
`out_of_order` outcome surfaced from `StateBuffer.push`.
- Zod-validated options; bad input throws `R3F_NET.INVALID_OPTIONS`.
## Usage: Basic Example
```ts
import { EntitySyncManager } from "@suaveplan/r3f-networking";
const sync = new EntitySyncManager({ sendRate: 20 });
sync.registerEntity("e1", "me", true);
sync.registerEntity("e2", "peer", false);
sync.receiveUpdate({
entityId: "e2",
transform: { position: [1, 0, 0], rotation: [0,0,0,1], scale: [1,1,1] },
timestamp: 1000,
seq: 1,
});
sync.updateRemoteEntities(1100);
```
## Usage: Advanced Example
Drive sync from a transport receive callback and a per-frame R3F hook:
```ts
transport.onMessage((peer, env) => {
const msg = decodeUpdate(env.payload);
sync.receiveUpdate(msg);
});
useFrame((state) => {
const t = state.clock.elapsedTime * 1000;
sync.updateRemoteEntities(t);
if (sync.shouldSend(t)) {
for (const m of sync.collectLocalUpdates(t)) transport.broadcast(m);
}
});
```
## API Reference
- `new EntitySyncManager(config?: EntitySyncConfig)`
- `registerEntity(id, ownerId, isLocal, initial?)` / `unregisterEntity(id)`
- `receiveUpdate(message: EntityUpdateMessage)`
- `updateRemoteEntities(serverTime: number)`
- `updateLocalEntity(id, transform)`
- `shouldSend(currentTime): boolean`
- `collectLocalUpdates(timestamp): EntityUpdateMessage[]`
- `getSnapshot(): SyncSnapshot`
- `getEntityCount(): number` / `clear()`
## Implementation Notes
The send-rate gate uses wall-clock time on each frame rather than a setInterval
so missed frames cannot accumulate. Remote `out_of_order` errors are swallowed
and reported through the OTel counter — surfacing them as exceptions would
cascade-kill the consuming r3f frame loop on transient jitter. The
`enablePrediction` flag is consumed by hooks that wire `ClientPredictor`
externally; the manager itself does not predict.

View File

@@ -0,0 +1,74 @@
# `interest-manager`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Per-observer interest tracker. Each observer has a radius, every other
entity in the world is either inside or outside that radius, and the
manager returns per-tick deltas (`entered`, `exited`, `inRange`) so the
networking layer can stream only relevant entities. Hysteresis on the
boundary prevents flap when an entity hovers exactly at the cut-off.
## Features
- Squared-distance comparisons throughout to avoid sqrt on the hot path.
- Per-observer `Set` membership cache + delta computation in a single pass.
- Tunable `interestRadius`, `hysteresis`, `updateInterval` (Zod-validated).
- `shouldUpdate(currentTime)` gates expensive scans against the configured
interval.
- `R3F_NET.INVALID_OPTIONS` on bad config.
## Usage: Basic Example
```ts
import { InterestManager } from "@suaveplan/r3f-networking";
const im = new InterestManager({ interestRadius: 25 });
im.addEntity("npc1", [10, 0, 0]);
im.addEntity("npc2", [200, 0, 0]);
const r = im.query("me", [0, 0, 0]);
// r.entered === ["npc1"]
```
## Usage: Advanced Example
Drive an entity replication loop with hysteresis:
```ts
const im = new InterestManager({ interestRadius: 50, hysteresis: 1.2 });
function tick(t: number, observer: string, observerPos: Vec3) {
if (!im.shouldUpdate(t)) return;
const r = im.query(observer, observerPos);
for (const id of r.entered) replication.subscribe(id);
for (const id of r.exited) replication.unsubscribe(id);
}
```
## API Reference
- `new InterestManager(config?: InterestManagerConfig)`
- `addEntity(id, position)` / `removeEntity(id)` / `updatePosition(id, pos)`
- `query(observerId, observerPosition): InterestResult`
- `shouldUpdate(currentTime): boolean`
- `getInterestSet(observerId): string[]`
- `getEntityCount()` / `getInterestRadius()` / `clear()`
## Implementation Notes
The inner loop is O(n) over registered entities; for very large worlds a
spatial-hash decorator can be wrapped around this class without changing
its contract. The `hysteresis` factor is multiplied into the squared
radius once and reused — entities that just entered must drift slightly
further than the entry threshold before they exit, eliminating boundary
flap that would otherwise generate a constant stream of subscribe and
unsubscribe events at the network layer. Per-observer state is kept in a
`Map<observerId, Set<entityId>>` so two observers in the same scene each
have their own independent interest set; this is essential for split-
screen rendering and for the dedicated-server case where the server holds
one observer per connected client. `removeEntity` walks every observer
set so the removed entity never appears in a future delta after its
deregistration, even if the next per-observer query has not yet been
issued.

View File

@@ -0,0 +1,67 @@
# `memory-transport`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Deterministic in-memory implementation of the `@suaveplan/network`
`Transport` contract. Two or more `MemoryTransport` instances joined to a
shared `MemoryHub` deliver envelopes synchronously across handlers,
allowing 2-client multiplayer integration tests to run without a real
WebRTC signalling round-trip. This is a **real** implementation of the
contract — not a mock — and it satisfies the CLAUDE.md "no mocks" rule
because it is shipped, exercised in production-style tests, and useful as a
fast-path transport for offline development scenes.
## Features
- Real `Transport` implementation: `connect` / `disconnect` / `send` /
`onMessage` / `onPeerEvent` / `getConnectedPeers` / `close`.
- Synchronous delivery within a hub — every send completes inside one
microtask, ideal for deterministic property tests.
- Records `r3f_net.peer_events_total{event}` on every connect/drop.
- Errors funnel through `R3FNetError` codes — `DISPOSED`, `PEER_DROPPED`,
`TRANSPORT_FAILED`, `INVALID_OPTIONS`.
## Usage: Basic Example
```ts
import { createMemoryHub, MemoryTransport } from "@suaveplan/r3f-networking";
const hub = createMemoryHub();
const a = new MemoryTransport("peer-a", hub);
const b = new MemoryTransport("peer-b", hub);
b.onMessage((from, env) => console.log("from", from, "got", env));
await a.connect("peer-b", "memory");
await a.send("peer-b", env);
```
## Usage: Advanced Example
Run a 2-client multiplayer integration test:
```ts
const hub = createMemoryHub();
const server = new MemoryTransport("server", hub);
const client = new MemoryTransport("c1", hub);
client.onMessage((_, env) => incomingSnapshots.push(env));
await client.connect("server", "memory");
await server.send("c1", snapshotEnvelope);
expect(incomingSnapshots).toHaveLength(1);
```
## API Reference
- `createMemoryHub(): MemoryHub`
- `new MemoryTransport(localPeerId, hub, id?)` — throws `INVALID_OPTIONS` on
duplicate `localPeerId`.
- All methods of the `Transport` contract.
## Implementation Notes
The hub holds a `Map<PeerId, MemoryTransport>` so peer discovery is O(1).
`close()` is idempotent and disconnects every still-attached peer before
removing the transport from the hub. Send to an un-connected peer throws
`PEER_DROPPED` rather than silently dropping the envelope — this surfaces
mis-wired tests immediately.

View File

@@ -0,0 +1,67 @@
# `network-helpers`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Pure math primitives shared by every networking subsystem in the package:
3D-vector lerp, quaternion slerp + normalisation, scalar/Euclidean/squared
distance, and clamp. All functions are total, allocation-light, and
side-effect-free so they run safely inside the React-Three-Fiber per-frame
hot path.
## Features
- `lerpVec3(a, b, t)` linear interpolation between two `Vec3`.
- `slerpQuat(a, b, t)` spherical-linear interpolation between two quaternions
with the standard short-arc dot-flip and the near-identity linear fallback.
- `normalizeQuat(q)` returns a unit-length quaternion with a `(0,0,0,1)` safe
fallback for the zero-length input.
- `distanceVec3` / `distanceSqVec3` Euclidean and squared-Euclidean magnitudes.
- `clamp(value, min, max)` scalar clamp used by interpolation + extrapolation
caps.
- Shared constants `IDENTITY_QUAT`, `ZERO_VEC3`, `UNIT_SCALE`.
## Usage: Basic Example
```ts
import { lerpVec3, slerpQuat } from "@suaveplan/r3f-networking";
const mid = lerpVec3([0, 0, 0], [10, 0, 0], 0.5); // [5, 0, 0]
const half = slerpQuat([0, 0, 0, 1], [0, 1, 0, 0], 0.5);
```
## Usage: Advanced Example
Combine with `EntityInterpolator` for frame-smoothed remote-peer transforms:
```ts
import { distanceVec3, clamp, lerpVec3 } from "@suaveplan/r3f-networking";
function damp(curr: Vec3, target: Vec3, rate: number): Vec3 {
const t = clamp(rate, 0, 1);
return lerpVec3(curr, target, t);
}
const err = distanceVec3(localPos, serverPos);
```
## API Reference
- `lerpVec3(a: Vec3, b: Vec3, t: number): [number, number, number]`
- `slerpQuat(a: Quat, b: Quat, t: number): [number, number, number, number]`
- `normalizeQuat(q): [number, number, number, number]`
- `distanceVec3(a: Vec3, b: Vec3): number`
- `distanceSqVec3(a: Vec3, b: Vec3): number`
- `clamp(value: number, min: number, max: number): number`
- Constants `IDENTITY_QUAT`, `ZERO_VEC3`, `UNIT_SCALE`.
## Implementation Notes
Quaternion slerp uses the dot-flip short-arc selection and falls back to a
straight lerp inside a `dot > 0.9995` cone for numerical stability near
identity. Distance functions return raw scalars; squared distance is
preferred inside interest-radius checks to avoid `sqrt`. All functions return
freshly-allocated arrays; callers that hit allocation limits should consume
results into a `THREE.Vector3` in place.