first commit
This commit is contained in:
108
docs/threejs/r3f/r3f-networking/README.md
Normal file
108
docs/threejs/r3f/r3f-networking/README.md
Normal 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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
80
docs/threejs/r3f/r3f-networking/src/errors/errors.md
Normal file
80
docs/threejs/r3f/r3f-networking/src/errors/errors.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user