murow 0.0.60 → 0.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -37
- package/dist/cjs/core/binary-codec/binary-codec.js +1 -0
- package/dist/cjs/core/binary-codec/index.js +1 -0
- package/dist/cjs/core/driver/driver.js +1 -0
- package/dist/cjs/core/driver/drivers/immediate.js +1 -0
- package/dist/cjs/core/driver/drivers/index.js +1 -0
- package/dist/cjs/core/driver/drivers/raf.js +1 -0
- package/dist/cjs/core/driver/drivers/timeout.js +1 -0
- package/dist/cjs/core/driver/index.js +1 -0
- package/dist/cjs/core/events/event-system.js +1 -0
- package/dist/cjs/core/events/index.js +1 -0
- package/dist/cjs/core/fixed-ticker/fixed-ticker.js +1 -0
- package/dist/cjs/core/fixed-ticker/index.js +1 -0
- package/dist/cjs/core/free-list/free-list.js +1 -0
- package/dist/cjs/core/free-list/index.js +1 -0
- package/dist/cjs/core/generate-id/generate-id.js +1 -0
- package/dist/cjs/core/generate-id/index.js +1 -0
- package/dist/cjs/core/index.js +1 -0
- package/dist/cjs/core/input/index.js +1 -0
- package/dist/cjs/core/input/manager.js +1 -0
- package/dist/cjs/core/input/sources/browser.js +1 -0
- package/dist/cjs/core/input/sources/index.js +1 -0
- package/dist/cjs/core/input/types.js +1 -0
- package/dist/cjs/core/lerp/index.js +1 -0
- package/dist/cjs/core/lerp/lerp.js +1 -0
- package/dist/cjs/core/navmesh/index.js +1 -0
- package/dist/cjs/core/navmesh/navmesh-worker-pool.js +1 -0
- package/dist/cjs/core/navmesh/navmesh.js +1 -0
- package/dist/cjs/core/navmesh/navmesh.worker.js +1 -0
- package/dist/cjs/core/pooled-codec/index.js +1 -0
- package/dist/cjs/core/pooled-codec/pooled-codec.js +1 -0
- package/dist/cjs/core/prediction/index.js +1 -0
- package/dist/cjs/core/prediction/prediction.js +1 -0
- package/dist/cjs/core/ray/index.js +1 -0
- package/dist/cjs/core/ray/ray-2d.js +1 -0
- package/dist/cjs/core/ray/ray-3d.js +1 -0
- package/dist/cjs/core/simple-rng/index.js +1 -0
- package/dist/cjs/core/simple-rng/simple-rng.js +1 -0
- package/dist/cjs/core/sparse-batcher/index.js +1 -0
- package/dist/cjs/core/sparse-batcher/sparse-batcher.js +1 -0
- package/dist/cjs/ecs/component-store.js +1 -0
- package/dist/cjs/ecs/component.js +1 -0
- package/dist/cjs/ecs/entity-handle.js +1 -0
- package/dist/cjs/ecs/index.js +1 -0
- package/dist/cjs/ecs/system-builder.js +1 -0
- package/dist/cjs/ecs/world-systems.js +1 -0
- package/dist/cjs/ecs/world.js +1 -0
- package/dist/cjs/game/index.js +1 -0
- package/dist/cjs/game/loop/index.js +1 -0
- package/dist/cjs/game/loop/loop.js +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/net/adapters/browser-websocket.js +1 -0
- package/dist/cjs/net/adapters/bun-websocket.js +1 -0
- package/dist/cjs/net/buffer-pool.js +1 -0
- package/dist/cjs/net/client.js +1 -0
- package/dist/cjs/net/index.js +1 -0
- package/dist/cjs/net/server.js +1 -0
- package/dist/cjs/net/types.js +1 -0
- package/dist/cjs/net/validators.js +1 -0
- package/dist/cjs/protocol/index.js +1 -0
- package/dist/cjs/protocol/intent/define-intent.js +1 -0
- package/dist/cjs/protocol/intent/index.js +1 -0
- package/dist/cjs/protocol/intent/intent-registry.js +1 -0
- package/dist/cjs/protocol/intent/intent.js +1 -0
- package/dist/cjs/protocol/rpc/define-rpc.js +1 -0
- package/dist/cjs/protocol/rpc/index.js +1 -0
- package/dist/cjs/protocol/rpc/rpc-registry.js +1 -0
- package/dist/cjs/protocol/rpc/rpc.js +1 -0
- package/dist/cjs/protocol/snapshot/index.js +1 -0
- package/dist/cjs/protocol/snapshot/snapshot-codec.js +1 -0
- package/dist/cjs/protocol/snapshot/snapshot-registry.js +1 -0
- package/dist/cjs/protocol/snapshot/snapshot.js +1 -0
- package/dist/cjs/renderer/base-2d-renderer.js +1 -0
- package/dist/cjs/renderer/base-3d-renderer.js +1 -0
- package/dist/cjs/renderer/base-renderer.js +1 -0
- package/dist/cjs/renderer/index.js +1 -0
- package/dist/cjs/renderer/types.js +1 -0
- package/dist/esm/core/binary-codec/binary-codec.js +1 -0
- package/dist/esm/core/binary-codec/index.js +1 -0
- package/dist/esm/core/driver/driver.js +1 -0
- package/dist/esm/core/driver/drivers/immediate.js +1 -0
- package/dist/esm/core/driver/drivers/index.js +1 -0
- package/dist/esm/core/driver/drivers/raf.js +1 -0
- package/dist/esm/core/driver/drivers/timeout.js +1 -0
- package/dist/esm/core/driver/index.js +1 -0
- package/dist/esm/core/events/event-system.js +1 -0
- package/dist/esm/core/events/index.js +1 -0
- package/dist/esm/core/fixed-ticker/fixed-ticker.js +1 -0
- package/dist/esm/core/fixed-ticker/index.js +1 -0
- package/dist/esm/core/free-list/free-list.js +1 -0
- package/dist/esm/core/free-list/index.js +1 -0
- package/dist/esm/core/generate-id/generate-id.js +1 -0
- package/dist/esm/core/generate-id/index.js +1 -0
- package/dist/esm/core/index.js +1 -0
- package/dist/esm/core/input/index.js +1 -0
- package/dist/esm/core/input/manager.js +1 -0
- package/dist/esm/core/input/sources/browser.js +1 -0
- package/dist/esm/core/input/sources/index.js +1 -0
- package/dist/esm/core/input/types.js +0 -0
- package/dist/esm/core/lerp/index.js +1 -0
- package/dist/esm/core/lerp/lerp.js +1 -0
- package/dist/esm/core/navmesh/index.js +1 -0
- package/dist/esm/core/navmesh/navmesh-worker-pool.js +1 -0
- package/dist/esm/core/navmesh/navmesh.js +1 -0
- package/dist/esm/core/navmesh/navmesh.worker.js +1 -0
- package/dist/esm/core/pooled-codec/index.js +1 -0
- package/dist/esm/core/pooled-codec/pooled-codec.js +1 -0
- package/dist/esm/core/prediction/index.js +1 -0
- package/dist/esm/core/prediction/prediction.js +1 -0
- package/dist/esm/core/ray/index.js +1 -0
- package/dist/esm/core/ray/ray-2d.js +1 -0
- package/dist/esm/core/ray/ray-3d.js +1 -0
- package/dist/esm/core/simple-rng/index.js +1 -0
- package/dist/esm/core/simple-rng/simple-rng.js +1 -0
- package/dist/esm/core/sparse-batcher/index.js +1 -0
- package/dist/esm/core/sparse-batcher/sparse-batcher.js +1 -0
- package/dist/esm/ecs/component-store.js +1 -0
- package/dist/esm/ecs/component.js +1 -0
- package/dist/esm/ecs/entity-handle.js +1 -0
- package/dist/esm/ecs/index.js +1 -0
- package/dist/esm/ecs/system-builder.js +1 -0
- package/dist/esm/ecs/world-systems.js +1 -0
- package/dist/esm/ecs/world.js +1 -0
- package/dist/esm/game/index.js +1 -0
- package/dist/esm/game/loop/index.js +1 -0
- package/dist/esm/game/loop/loop.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/net/adapters/browser-websocket.js +1 -0
- package/dist/esm/net/adapters/bun-websocket.js +1 -0
- package/dist/esm/net/buffer-pool.js +1 -0
- package/dist/esm/net/client.js +1 -0
- package/dist/esm/net/index.js +1 -0
- package/dist/esm/net/server.js +1 -0
- package/dist/esm/net/types.js +1 -0
- package/dist/esm/net/validators.js +1 -0
- package/dist/esm/protocol/index.js +1 -0
- package/dist/esm/protocol/intent/define-intent.js +1 -0
- package/dist/esm/protocol/intent/index.js +1 -0
- package/dist/esm/protocol/intent/intent-registry.js +1 -0
- package/dist/esm/protocol/intent/intent.js +0 -0
- package/dist/esm/protocol/rpc/define-rpc.js +1 -0
- package/dist/esm/protocol/rpc/index.js +1 -0
- package/dist/esm/protocol/rpc/rpc-registry.js +1 -0
- package/dist/esm/protocol/rpc/rpc.js +0 -0
- package/dist/esm/protocol/snapshot/index.js +1 -0
- package/dist/esm/protocol/snapshot/snapshot-codec.js +1 -0
- package/dist/esm/protocol/snapshot/snapshot-registry.js +1 -0
- package/dist/esm/protocol/snapshot/snapshot.js +1 -0
- package/dist/esm/renderer/base-2d-renderer.js +1 -0
- package/dist/esm/renderer/base-3d-renderer.js +1 -0
- package/dist/esm/renderer/base-renderer.js +1 -0
- package/dist/esm/renderer/index.js +1 -0
- package/dist/esm/renderer/types.js +0 -0
- package/dist/{core → types/core}/binary-codec/binary-codec.d.ts +4 -0
- package/dist/{core/loop → types/core/driver}/drivers/immediate.d.ts +1 -1
- package/dist/{core/loop → types/core/driver}/drivers/raf.d.ts +1 -1
- package/dist/{core/loop → types/core/driver}/drivers/timeout.d.ts +1 -1
- package/dist/{core/loop → types/core/driver}/index.d.ts +1 -1
- package/dist/{core → types/core}/events/event-system.d.ts +14 -33
- package/dist/{core → types/core}/fixed-ticker/fixed-ticker.d.ts +1 -1
- package/dist/types/core/free-list/free-list.d.ts +31 -0
- package/dist/types/core/free-list/index.d.ts +1 -0
- package/dist/{core → types/core}/index.d.ts +7 -1
- package/dist/types/core/input/index.d.ts +3 -0
- package/dist/types/core/input/manager.d.ts +56 -0
- package/dist/types/core/input/sources/browser.d.ts +9 -0
- package/dist/types/core/input/sources/index.d.ts +1 -0
- package/dist/types/core/input/types.d.ts +36 -0
- package/dist/{core → types/core}/navmesh/navmesh.d.ts +1 -21
- package/dist/types/core/ray/index.d.ts +2 -0
- package/dist/types/core/ray/ray-2d.d.ts +37 -0
- package/dist/types/core/ray/ray-3d.d.ts +42 -0
- package/dist/types/core/simple-rng/index.d.ts +1 -0
- package/dist/types/core/simple-rng/simple-rng.d.ts +36 -0
- package/dist/types/core/sparse-batcher/index.d.ts +1 -0
- package/dist/types/core/sparse-batcher/sparse-batcher.d.ts +55 -0
- package/dist/{ecs → types/ecs}/system-builder.d.ts +20 -9
- package/dist/{ecs → types/ecs}/world.d.ts +11 -0
- package/dist/types/game/index.d.ts +1 -0
- package/dist/types/game/loop/index.d.ts +1 -0
- package/dist/types/game/loop/loop.d.ts +175 -0
- package/dist/{index.d.ts → types/index.d.ts} +2 -0
- package/dist/{net → types/net}/index.d.ts +2 -2
- package/dist/{net → types/net}/server.d.ts +39 -19
- package/dist/{protocol → types/protocol}/intent/define-intent.d.ts +15 -0
- package/dist/{protocol → types/protocol}/intent/index.d.ts +1 -1
- package/dist/types/renderer/base-2d-renderer.d.ts +13 -0
- package/dist/types/renderer/base-3d-renderer.d.ts +10 -0
- package/dist/types/renderer/base-renderer.d.ts +21 -0
- package/dist/types/renderer/index.d.ts +4 -0
- package/dist/types/renderer/types.d.ts +79 -0
- package/dist/webgpu/cjs/index.js +6004 -0
- package/dist/webgpu/esm/index.js +5972 -0
- package/dist/webgpu/types/2d/animation.d.ts +97 -0
- package/dist/webgpu/types/2d/renderer.d.ts +55 -0
- package/dist/webgpu/types/2d/shader.d.ts +61 -0
- package/dist/webgpu/types/2d/sprite-accessor.d.ts +47 -0
- package/dist/webgpu/types/2d/sprite-accessor.test.d.ts +1 -0
- package/dist/webgpu/types/3d/gltf-skin-parser.d.ts +101 -0
- package/dist/webgpu/types/3d/morph-animation.d.ts +69 -0
- package/dist/webgpu/types/3d/morph-animation.test.d.ts +1 -0
- package/dist/webgpu/types/3d/renderer.d.ts +216 -0
- package/dist/webgpu/types/3d/shader.d.ts +136 -0
- package/dist/webgpu/types/3d/skeletal-animation-compute/index.d.ts +2 -0
- package/dist/webgpu/types/3d/skeletal-animation-compute/kernel.d.ts +8 -0
- package/dist/webgpu/types/3d/skeletal-animation-compute/packer.d.ts +32 -0
- package/dist/webgpu/types/3d/skeletal-animation.d.ts +90 -0
- package/dist/webgpu/types/camera/camera-2d.d.ts +53 -0
- package/dist/webgpu/types/camera/camera-2d.test.d.ts +1 -0
- package/dist/webgpu/types/camera/camera-3d.d.ts +81 -0
- package/dist/webgpu/types/camera/camera-3d.test.d.ts +1 -0
- package/dist/webgpu/types/camera/index.d.ts +2 -0
- package/dist/webgpu/types/compute/compute-builder.d.ts +123 -0
- package/dist/webgpu/types/compute/compute-builder.test.d.ts +1 -0
- package/dist/webgpu/types/core/constants.d.ts +59 -0
- package/dist/webgpu/types/core/constants.test.d.ts +1 -0
- package/dist/webgpu/types/core/index.d.ts +2 -0
- package/dist/webgpu/types/core/math.d.ts +37 -0
- package/dist/webgpu/types/core/types.d.ts +125 -0
- package/dist/webgpu/types/core/types.test.d.ts +1 -0
- package/dist/webgpu/types/geometry/built-in.d.ts +58 -0
- package/dist/webgpu/types/geometry/built-in.test.d.ts +1 -0
- package/dist/webgpu/types/geometry/geometry-builder.d.ts +281 -0
- package/dist/webgpu/types/geometry/geometry-builder.test.d.ts +1 -0
- package/dist/webgpu/types/geometry/index.d.ts +2 -0
- package/dist/webgpu/types/index.d.ts +32 -0
- package/dist/webgpu/types/particle/emitter.d.ts +36 -0
- package/dist/webgpu/types/shaders/index.d.ts +2 -0
- package/dist/webgpu/types/shaders/runtime-transpile.d.ts +18 -0
- package/dist/webgpu/types/shaders/sprite-2d.wgsl.d.ts +10 -0
- package/dist/webgpu/types/shaders/typegpu.d.ts +9 -0
- package/dist/webgpu/types/shaders/utils.d.ts +28 -0
- package/dist/webgpu/types/shaders/utils.test.d.ts +1 -0
- package/dist/webgpu/types/spritesheet/index.d.ts +1 -0
- package/dist/webgpu/types/spritesheet/spritesheet.d.ts +57 -0
- package/dist/webgpu/types/spritesheet/spritesheet.test.d.ts +1 -0
- package/package.json +96 -26
- package/dist/core/binary-codec/binary-codec.js +0 -354
- package/dist/core/binary-codec/index.js +0 -1
- package/dist/core/events/event-system.js +0 -88
- package/dist/core/events/index.js +0 -1
- package/dist/core/fixed-ticker/fixed-ticker.js +0 -101
- package/dist/core/fixed-ticker/index.js +0 -1
- package/dist/core/generate-id/generate-id.js +0 -25
- package/dist/core/generate-id/index.js +0 -1
- package/dist/core/index.js +0 -9
- package/dist/core/lerp/index.js +0 -1
- package/dist/core/lerp/lerp.js +0 -42
- package/dist/core/loop/drivers/immediate.js +0 -61
- package/dist/core/loop/drivers/index.js +0 -3
- package/dist/core/loop/drivers/raf.js +0 -62
- package/dist/core/loop/drivers/timeout.js +0 -71
- package/dist/core/loop/index.js +0 -2
- package/dist/core/loop/loop.js +0 -47
- package/dist/core/navmesh/index.js +0 -1
- package/dist/core/navmesh/navmesh-worker-pool.js +0 -180
- package/dist/core/navmesh/navmesh.js +0 -799
- package/dist/core/navmesh/navmesh.worker.js +0 -79
- package/dist/core/pooled-codec/index.js +0 -1
- package/dist/core/pooled-codec/pooled-codec.js +0 -410
- package/dist/core/prediction/index.js +0 -1
- package/dist/core/prediction/prediction.js +0 -99
- package/dist/core.esm.js +0 -1
- package/dist/core.js +0 -1
- package/dist/ecs/component-store.js +0 -175
- package/dist/ecs/component.js +0 -43
- package/dist/ecs/entity-handle.js +0 -515
- package/dist/ecs/example.js +0 -125
- package/dist/ecs/index.js +0 -4
- package/dist/ecs/system-builder.js +0 -180
- package/dist/ecs/system.d.ts +0 -63
- package/dist/ecs/system.js +0 -92
- package/dist/ecs/world-systems.js +0 -79
- package/dist/ecs/world.js +0 -684
- package/dist/index.js +0 -24
- package/dist/net/adapters/browser-websocket.js +0 -74
- package/dist/net/adapters/bun-websocket.js +0 -245
- package/dist/net/buffer-pool.js +0 -89
- package/dist/net/client.js +0 -586
- package/dist/net/index.js +0 -58
- package/dist/net/server.js +0 -938
- package/dist/net/types.js +0 -31
- package/dist/net/validators.js +0 -88
- package/dist/protocol/index.js +0 -92
- package/dist/protocol/intent/define-intent.js +0 -125
- package/dist/protocol/intent/index.js +0 -91
- package/dist/protocol/intent/intent-registry.js +0 -91
- package/dist/protocol/rpc/define-rpc.js +0 -84
- package/dist/protocol/rpc/index.js +0 -3
- package/dist/protocol/rpc/rpc-registry.js +0 -159
- package/dist/protocol/rpc/rpc.js +0 -12
- package/dist/protocol/snapshot/index.js +0 -43
- package/dist/protocol/snapshot/snapshot-codec.js +0 -67
- package/dist/protocol/snapshot/snapshot-registry.js +0 -168
- package/dist/protocol/snapshot/snapshot.js +0 -30
- package/src/core/binary-codec/README.md +0 -60
- package/src/core/binary-codec/binary-codec.test.ts +0 -300
- package/src/core/binary-codec/binary-codec.ts +0 -448
- package/src/core/binary-codec/index.ts +0 -1
- package/src/core/events/README.md +0 -47
- package/src/core/events/event-system.test.ts +0 -243
- package/src/core/events/event-system.ts +0 -140
- package/src/core/events/index.ts +0 -1
- package/src/core/fixed-ticker/README.md +0 -77
- package/src/core/fixed-ticker/fixed-ticker.test.ts +0 -151
- package/src/core/fixed-ticker/fixed-ticker.ts +0 -169
- package/src/core/fixed-ticker/index.ts +0 -1
- package/src/core/generate-id/README.md +0 -18
- package/src/core/generate-id/generate-id.test.ts +0 -79
- package/src/core/generate-id/generate-id.ts +0 -37
- package/src/core/generate-id/index.ts +0 -1
- package/src/core/index.ts +0 -9
- package/src/core/lerp/README.md +0 -79
- package/src/core/lerp/index.ts +0 -1
- package/src/core/lerp/lerp.test.ts +0 -90
- package/src/core/lerp/lerp.ts +0 -42
- package/src/core/loop/README.md +0 -97
- package/src/core/loop/drivers/immediate.ts +0 -66
- package/src/core/loop/drivers/index.ts +0 -3
- package/src/core/loop/drivers/raf.ts +0 -67
- package/src/core/loop/drivers/timeout.ts +0 -77
- package/src/core/loop/index.ts +0 -2
- package/src/core/loop/loop.test.ts +0 -414
- package/src/core/loop/loop.ts +0 -71
- package/src/core/navmesh/README.md +0 -164
- package/src/core/navmesh/index.ts +0 -1
- package/src/core/navmesh/navmesh-worker-pool.ts +0 -236
- package/src/core/navmesh/navmesh-workers.test.ts +0 -356
- package/src/core/navmesh/navmesh.test.ts +0 -344
- package/src/core/navmesh/navmesh.ts +0 -1047
- package/src/core/navmesh/navmesh.worker.ts +0 -147
- package/src/core/pooled-codec/README.md +0 -70
- package/src/core/pooled-codec/index.ts +0 -1
- package/src/core/pooled-codec/pooled-codec.test.ts +0 -862
- package/src/core/pooled-codec/pooled-codec.ts +0 -504
- package/src/core/prediction/README.md +0 -64
- package/src/core/prediction/index.ts +0 -1
- package/src/core/prediction/prediction.test.ts +0 -423
- package/src/core/prediction/prediction.ts +0 -112
- package/src/ecs/README.md +0 -427
- package/src/ecs/benchmark.test.ts +0 -1645
- package/src/ecs/component-store.ts +0 -198
- package/src/ecs/component.ts +0 -90
- package/src/ecs/entity-handle.test.ts +0 -393
- package/src/ecs/entity-handle.ts +0 -563
- package/src/ecs/example.ts +0 -152
- package/src/ecs/index.ts +0 -4
- package/src/ecs/system-builder.ts +0 -309
- package/src/ecs/system.ts +0 -111
- package/src/ecs/world-systems.ts +0 -83
- package/src/ecs/world.test.ts +0 -310
- package/src/ecs/world.ts +0 -828
- package/src/index.ts +0 -28
- package/src/net/README.md +0 -474
- package/src/net/adapters/browser-websocket.ts +0 -86
- package/src/net/adapters/bun-websocket.ts +0 -292
- package/src/net/buffer-pool.ts +0 -106
- package/src/net/client.test.ts +0 -807
- package/src/net/client.ts +0 -695
- package/src/net/index.ts +0 -60
- package/src/net/server.test.ts +0 -799
- package/src/net/server.ts +0 -1116
- package/src/net/types.ts +0 -228
- package/src/net/validators.ts +0 -104
- package/src/protocol/README.md +0 -469
- package/src/protocol/index.ts +0 -93
- package/src/protocol/intent/define-intent.test.ts +0 -397
- package/src/protocol/intent/define-intent.ts +0 -182
- package/src/protocol/intent/index.ts +0 -94
- package/src/protocol/intent/intent-registry.test.ts +0 -198
- package/src/protocol/intent/intent-registry.ts +0 -112
- package/src/protocol/intent/intent.ts +0 -12
- package/src/protocol/rpc/define-rpc.test.ts +0 -141
- package/src/protocol/rpc/define-rpc.ts +0 -113
- package/src/protocol/rpc/index.ts +0 -3
- package/src/protocol/rpc/rpc-registry.test.ts +0 -168
- package/src/protocol/rpc/rpc-registry.ts +0 -176
- package/src/protocol/rpc/rpc.ts +0 -37
- package/src/protocol/snapshot/index.ts +0 -45
- package/src/protocol/snapshot/snapshot-codec.test.ts +0 -138
- package/src/protocol/snapshot/snapshot-codec.ts +0 -87
- package/src/protocol/snapshot/snapshot-registry.test.ts +0 -310
- package/src/protocol/snapshot/snapshot-registry.ts +0 -201
- package/src/protocol/snapshot/snapshot.test.ts +0 -76
- package/src/protocol/snapshot/snapshot.ts +0 -41
- /package/dist/{core → types/core}/binary-codec/index.d.ts +0 -0
- /package/dist/{core/loop/loop.d.ts → types/core/driver/driver.d.ts} +0 -0
- /package/dist/{core/loop → types/core/driver}/drivers/index.d.ts +0 -0
- /package/dist/{core → types/core}/events/index.d.ts +0 -0
- /package/dist/{core → types/core}/fixed-ticker/index.d.ts +0 -0
- /package/dist/{core → types/core}/generate-id/generate-id.d.ts +0 -0
- /package/dist/{core → types/core}/generate-id/index.d.ts +0 -0
- /package/dist/{core → types/core}/lerp/index.d.ts +0 -0
- /package/dist/{core → types/core}/lerp/lerp.d.ts +0 -0
- /package/dist/{core → types/core}/navmesh/index.d.ts +0 -0
- /package/dist/{core → types/core}/navmesh/navmesh-worker-pool.d.ts +0 -0
- /package/dist/{core → types/core}/navmesh/navmesh.worker.d.ts +0 -0
- /package/dist/{core → types/core}/pooled-codec/index.d.ts +0 -0
- /package/dist/{core → types/core}/pooled-codec/pooled-codec.d.ts +0 -0
- /package/dist/{core → types/core}/prediction/index.d.ts +0 -0
- /package/dist/{core → types/core}/prediction/prediction.d.ts +0 -0
- /package/dist/{ecs → types/ecs}/component-store.d.ts +0 -0
- /package/dist/{ecs → types/ecs}/component.d.ts +0 -0
- /package/dist/{ecs → types/ecs}/entity-handle.d.ts +0 -0
- /package/dist/{ecs → types/ecs}/example.d.ts +0 -0
- /package/dist/{ecs → types/ecs}/index.d.ts +0 -0
- /package/dist/{ecs → types/ecs}/world-systems.d.ts +0 -0
- /package/dist/{net → types/net}/adapters/browser-websocket.d.ts +0 -0
- /package/dist/{net → types/net}/adapters/bun-websocket.d.ts +0 -0
- /package/dist/{net → types/net}/buffer-pool.d.ts +0 -0
- /package/dist/{net → types/net}/client.d.ts +0 -0
- /package/dist/{net → types/net}/types.d.ts +0 -0
- /package/dist/{net → types/net}/validators.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/index.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/intent/intent-registry.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/intent/intent.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/rpc/define-rpc.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/rpc/index.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/rpc/rpc-registry.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/rpc/rpc.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/snapshot/index.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/snapshot/snapshot-codec.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/snapshot/snapshot-registry.d.ts +0 -0
- /package/dist/{protocol → types/protocol}/snapshot/snapshot.d.ts +0 -0
- /package/dist/{protocol/intent/intent.js → webgpu/types/2d/animation.test.d.ts} +0 -0
package/src/net/server.ts
DELETED
|
@@ -1,1116 +0,0 @@
|
|
|
1
|
-
import type { IntentRegistry } from "../protocol/intent/intent-registry";
|
|
2
|
-
import type { SnapshotRegistry } from "../protocol/snapshot/snapshot-registry";
|
|
3
|
-
import type { Snapshot } from "../protocol/snapshot/snapshot";
|
|
4
|
-
import type { Intent } from "../protocol/intent/intent";
|
|
5
|
-
import type { RpcRegistry } from "../protocol/rpc/rpc-registry";
|
|
6
|
-
import type { DefinedRPC } from "../protocol/rpc/rpc";
|
|
7
|
-
import { MessageType, MessagePriority, type PeerState, type ServerTransportAdapter, type TransportAdapter, type NetworkConfig, type QueuedMessage } from "./types";
|
|
8
|
-
import { MessageWrapperPool } from "./buffer-pool";
|
|
9
|
-
import { DefinedIntent } from "../protocol";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Configuration for ServerNetwork
|
|
13
|
-
*/
|
|
14
|
-
export interface ServerNetworkConfig<TPeer extends TransportAdapter, TSnapshots> {
|
|
15
|
-
/** Transport adapter for managing peer connections */
|
|
16
|
-
transport: ServerTransportAdapter<TPeer>;
|
|
17
|
-
|
|
18
|
-
/** Intent registry for decoding client messages */
|
|
19
|
-
intentRegistry: IntentRegistry;
|
|
20
|
-
|
|
21
|
-
/** Factory to create per-peer snapshot registries */
|
|
22
|
-
createPeerSnapshotRegistry: () => SnapshotRegistry<TSnapshots>;
|
|
23
|
-
|
|
24
|
-
/** RPC registry for bidirectional remote procedure calls (optional) */
|
|
25
|
-
rpcRegistry?: RpcRegistry;
|
|
26
|
-
|
|
27
|
-
/** Network configuration */
|
|
28
|
-
config?: NetworkConfig;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Generic game server that manages multiple peer connections
|
|
33
|
-
* Each peer gets its own snapshot registry for per-peer state tracking (fog of war, interest management)
|
|
34
|
-
*
|
|
35
|
-
* @template TPeer The transport adapter type for peer connections
|
|
36
|
-
* @template TSnapshots Union type of all possible snapshot update types
|
|
37
|
-
*
|
|
38
|
-
* @remarks
|
|
39
|
-
* **Client-Side Prediction Support:**
|
|
40
|
-
* - Automatically tracks the last processed client tick for each peer
|
|
41
|
-
* - All intents include a 'tick' field (added by defineIntent())
|
|
42
|
-
* - Use `getConfirmedClientTick(peerId)` to get the confirmed tick for snapshots
|
|
43
|
-
* - Use `onAnyIntent()` to track which peers need snapshot responses
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```ts
|
|
47
|
-
* type GameSnapshots = PlayerUpdate | ScoreUpdate | ProjectileUpdate;
|
|
48
|
-
*
|
|
49
|
-
* const server = new ServerNetwork<WebSocketPeer, GameSnapshots>({
|
|
50
|
-
* transport: wsServerTransport,
|
|
51
|
-
* intentRegistry,
|
|
52
|
-
* createPeerSnapshotRegistry: () => {
|
|
53
|
-
* const registry = new SnapshotRegistry<GameSnapshots>();
|
|
54
|
-
* registry.register('players', playerCodec);
|
|
55
|
-
* registry.register('score', scoreCodec);
|
|
56
|
-
* return registry;
|
|
57
|
-
* },
|
|
58
|
-
* });
|
|
59
|
-
*
|
|
60
|
-
* // Track which peers need responses (fires for ALL intents)
|
|
61
|
-
* const pendingResponses = new Set<string>();
|
|
62
|
-
* server.onAnyIntent((peerId) => {
|
|
63
|
-
* pendingResponses.add(peerId);
|
|
64
|
-
* });
|
|
65
|
-
*
|
|
66
|
-
* // Type-safe intent handlers
|
|
67
|
-
* server.onIntent<MoveIntent>(IntentKind.Move, (peerId, intent) => {
|
|
68
|
-
* intent.tick // ✅ Automatically included in all intents
|
|
69
|
-
* intent.dx // ✅ Correctly typed
|
|
70
|
-
* });
|
|
71
|
-
*
|
|
72
|
-
* // Send snapshot with confirmed client tick (for client-side prediction)
|
|
73
|
-
* const confirmedTick = server.getConfirmedClientTick(peerId);
|
|
74
|
-
* server.sendSnapshotToPeer(peerId, 'players', {
|
|
75
|
-
* tick: confirmedTick, // Client can reconcile based on this
|
|
76
|
-
* updates: { players: [...] }
|
|
77
|
-
* });
|
|
78
|
-
* ```
|
|
79
|
-
*/
|
|
80
|
-
export class ServerNetwork<TPeer extends TransportAdapter = TransportAdapter, TSnapshots = unknown> {
|
|
81
|
-
private transport: ServerTransportAdapter<TPeer>;
|
|
82
|
-
private intentRegistry: IntentRegistry;
|
|
83
|
-
private createPeerSnapshotRegistry: () => SnapshotRegistry<TSnapshots>;
|
|
84
|
-
private rpcRegistry?: RpcRegistry;
|
|
85
|
-
private config: Required<NetworkConfig>;
|
|
86
|
-
|
|
87
|
-
/** Per-peer state tracking */
|
|
88
|
-
private peers = new Map<string, PeerState>();
|
|
89
|
-
|
|
90
|
-
/** Per-peer snapshot registries - this is the key feature! */
|
|
91
|
-
private peerSnapshotRegistries = new Map<string, SnapshotRegistry<TSnapshots>>();
|
|
92
|
-
|
|
93
|
-
/** Track last processed client tick per peer (for client-side prediction) */
|
|
94
|
-
private lastProcessedClientTick = new Map<string, number>();
|
|
95
|
-
|
|
96
|
-
/** Track last sent snapshot hashes per peer per type (for delta detection) */
|
|
97
|
-
private lastSnapshotHashes = new Map<string, Map<string, number>>();
|
|
98
|
-
|
|
99
|
-
/** Intent handlers: kind -> handler[] (supports multiple handlers) */
|
|
100
|
-
private intentHandlers = new Map<number, Array<(peerId: string, intent: Intent) => void>>();
|
|
101
|
-
|
|
102
|
-
/** Global intent handler called for ALL intents before specific handlers */
|
|
103
|
-
private anyIntentHandlers: Array<(peerId: string, intent: Intent) => void> = [];
|
|
104
|
-
|
|
105
|
-
/** RPC method handlers: method -> handler[] (supports multiple handlers) */
|
|
106
|
-
private rpcHandlers = new Map<string, Array<(peerId: string, data: any) => void>>();
|
|
107
|
-
|
|
108
|
-
/** Connection lifecycle handlers */
|
|
109
|
-
private connectionHandlers: Array<(peerId: string) => void> = [];
|
|
110
|
-
private disconnectionHandlers: Array<(peerId: string) => void> = [];
|
|
111
|
-
|
|
112
|
-
/** Message wrapper pool for zero-allocation message wrapping */
|
|
113
|
-
private messagePool: MessageWrapperPool | null = null;
|
|
114
|
-
|
|
115
|
-
/** Heartbeat interval timer */
|
|
116
|
-
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
117
|
-
|
|
118
|
-
constructor(config: ServerNetworkConfig<TPeer, TSnapshots>) {
|
|
119
|
-
this.transport = config.transport;
|
|
120
|
-
this.intentRegistry = config.intentRegistry;
|
|
121
|
-
this.createPeerSnapshotRegistry = config.createPeerSnapshotRegistry;
|
|
122
|
-
this.rpcRegistry = config.rpcRegistry;
|
|
123
|
-
this.config = {
|
|
124
|
-
maxMessageSize: config.config?.maxMessageSize ?? 65536,
|
|
125
|
-
debug: config.config?.debug ?? false,
|
|
126
|
-
maxMessagesPerSecond: config.config?.maxMessagesPerSecond ?? 100,
|
|
127
|
-
maxSendQueueSize: config.config?.maxSendQueueSize ?? 100,
|
|
128
|
-
enableBufferPooling: config.config?.enableBufferPooling ?? true,
|
|
129
|
-
heartbeatInterval: config.config?.heartbeatInterval ?? 30000,
|
|
130
|
-
heartbeatTimeout: config.config?.heartbeatTimeout ?? 60000,
|
|
131
|
-
lagSimulation: config.config?.lagSimulation ?? 0,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Initialize message pool if buffer pooling is enabled
|
|
135
|
-
if (this.config.enableBufferPooling) {
|
|
136
|
-
this.messagePool = new MessageWrapperPool();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
this.setupTransportHandlers();
|
|
140
|
-
this.setupHeartbeat();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get the snapshot registry for a specific peer
|
|
145
|
-
*/
|
|
146
|
-
getPeerSnapshotRegistry(peerId: string): SnapshotRegistry<TSnapshots> | undefined {
|
|
147
|
-
return this.peerSnapshotRegistries.get(peerId);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Register a handler for a specific intent kind (type-safe)
|
|
152
|
-
* Supports multiple handlers per intent type
|
|
153
|
-
* @template T The intent type for this handler
|
|
154
|
-
* @param validator Optional validation function - if it returns false, intent is rejected
|
|
155
|
-
* @returns Unsubscribe function to remove this handler
|
|
156
|
-
*/
|
|
157
|
-
onIntent<T extends Intent>(
|
|
158
|
-
intent: DefinedIntent<T['kind'], T>,
|
|
159
|
-
handler: (peerId: string, intent: T) => void,
|
|
160
|
-
validator?: (peerId: string, intent: T) => boolean
|
|
161
|
-
): () => void {
|
|
162
|
-
let handlers = this.intentHandlers.get(intent.kind);
|
|
163
|
-
if (!handlers) {
|
|
164
|
-
handlers = [];
|
|
165
|
-
this.intentHandlers.set(intent.kind, handlers);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Wrap handler with validator if provided
|
|
169
|
-
const wrappedHandler = (peerId: string, intent: Intent) => {
|
|
170
|
-
if (validator) {
|
|
171
|
-
if (!validator(peerId, intent as T)) {
|
|
172
|
-
this.log(`Intent validation failed for peer ${peerId}, kind ${intent.kind}`);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
handler(peerId, intent as T);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
handlers.push(wrappedHandler);
|
|
180
|
-
|
|
181
|
-
// Return unsubscribe function
|
|
182
|
-
return () => {
|
|
183
|
-
const handlers = this.intentHandlers.get(intent.kind);
|
|
184
|
-
if (handlers) {
|
|
185
|
-
const index = handlers.indexOf(wrappedHandler);
|
|
186
|
-
if (index > -1) {
|
|
187
|
-
handlers.splice(index, 1);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Register a handler that fires for ALL intents, regardless of kind.
|
|
195
|
-
* Useful for tracking which peers sent intents this tick.
|
|
196
|
-
*
|
|
197
|
-
* @param handler Callback invoked for every intent before specific handlers
|
|
198
|
-
* @returns Unsubscribe function
|
|
199
|
-
*
|
|
200
|
-
* @remarks
|
|
201
|
-
* This handler is called BEFORE the specific intent handlers registered via onIntent().
|
|
202
|
-
* Common use case: Track which peers need snapshot responses this tick.
|
|
203
|
-
*
|
|
204
|
-
* @example
|
|
205
|
-
* ```ts
|
|
206
|
-
* const pendingResponses = new Set<string>();
|
|
207
|
-
*
|
|
208
|
-
* server.onAnyIntent((peerId, intent) => {
|
|
209
|
-
* // Mark this peer as needing a response on next tick
|
|
210
|
-
* pendingResponses.add(peerId);
|
|
211
|
-
* });
|
|
212
|
-
* ```
|
|
213
|
-
*/
|
|
214
|
-
onAnyIntent(handler: (peerId: string, intent: Intent) => void): () => void {
|
|
215
|
-
this.anyIntentHandlers.push(handler);
|
|
216
|
-
|
|
217
|
-
// Return unsubscribe function
|
|
218
|
-
return () => {
|
|
219
|
-
const index = this.anyIntentHandlers.indexOf(handler);
|
|
220
|
-
if (index > -1) {
|
|
221
|
-
this.anyIntentHandlers.splice(index, 1);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Register a handler for new connections
|
|
228
|
-
*/
|
|
229
|
-
onConnection(handler: (peerId: string) => void): void {
|
|
230
|
-
this.connectionHandlers.push(handler);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Register a handler for disconnections
|
|
235
|
-
*/
|
|
236
|
-
onDisconnection(handler: (peerId: string) => void): void {
|
|
237
|
-
this.disconnectionHandlers.push(handler);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Send an RPC to a specific peer (type-safe)
|
|
242
|
-
*
|
|
243
|
-
* @template TSchema The RPC data type
|
|
244
|
-
* @param peerId The peer to send to
|
|
245
|
-
* @param rpc The RPC definition created by defineRPC()
|
|
246
|
-
* @param data The RPC data to send
|
|
247
|
-
* @param priority Message priority (default: NORMAL)
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* ```ts
|
|
251
|
-
* const MatchCountdown = defineRPC({
|
|
252
|
-
* method: 'matchCountdown',
|
|
253
|
-
* schema: { secondsRemaining: BinaryCodec.u8 }
|
|
254
|
-
* });
|
|
255
|
-
*
|
|
256
|
-
* server.sendRpc(peerId, MatchCountdown, { secondsRemaining: 10 });
|
|
257
|
-
* ```
|
|
258
|
-
*/
|
|
259
|
-
sendRPC<TSchema extends Record<string, any>>(
|
|
260
|
-
peerId: string,
|
|
261
|
-
rpc: DefinedRPC<TSchema>,
|
|
262
|
-
data: TSchema,
|
|
263
|
-
priority: MessagePriority = MessagePriority.NORMAL
|
|
264
|
-
): void {
|
|
265
|
-
if (!this.rpcRegistry) {
|
|
266
|
-
throw new Error('RpcRegistry not configured. Pass rpcRegistry to ServerNetworkConfig.');
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const peer = this.peers.get(peerId);
|
|
270
|
-
if (!peer) {
|
|
271
|
-
this.log(`Cannot send RPC to unknown peer: ${peerId}`);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
// Encode RPC
|
|
277
|
-
const rpcData = this.rpcRegistry.encode(rpc, data);
|
|
278
|
-
|
|
279
|
-
// Wrap with message type header (use pool if enabled)
|
|
280
|
-
let message: Uint8Array;
|
|
281
|
-
if (this.messagePool) {
|
|
282
|
-
message = this.messagePool.wrap(MessageType.CUSTOM, rpcData);
|
|
283
|
-
} else {
|
|
284
|
-
message = new Uint8Array(1 + rpcData.byteLength);
|
|
285
|
-
message[0] = MessageType.CUSTOM;
|
|
286
|
-
message.set(rpcData, 1);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Check backpressure and queue if necessary
|
|
290
|
-
if (peer.isBackpressured || peer.sendQueue.length > 0) {
|
|
291
|
-
// Peer is experiencing backpressure, queue the message with priority
|
|
292
|
-
this.queueMessage(peer, message, priority);
|
|
293
|
-
|
|
294
|
-
// Release pooled buffer since we copied it in queueMessage
|
|
295
|
-
if (this.messagePool) {
|
|
296
|
-
this.messagePool.release(message);
|
|
297
|
-
}
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Try to send immediately
|
|
302
|
-
this.sendMessageToPeer(peer, message);
|
|
303
|
-
|
|
304
|
-
// Release pooled buffer after send
|
|
305
|
-
if (this.messagePool) {
|
|
306
|
-
this.messagePool.release(message);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
this.log(`Sent RPC (method: ${rpc.method}) to peer: ${peerId}`);
|
|
310
|
-
} catch (error) {
|
|
311
|
-
this.log(`Failed to send RPC to peer ${peerId}: ${error}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Send an RPC to all connected peers (broadcast)
|
|
317
|
-
*
|
|
318
|
-
* @template TSchema The RPC data type
|
|
319
|
-
* @param rpc The RPC definition created by defineRPC()
|
|
320
|
-
* @param data The RPC data to send
|
|
321
|
-
* @param priority Message priority (default: NORMAL)
|
|
322
|
-
*
|
|
323
|
-
* @example
|
|
324
|
-
* ```ts
|
|
325
|
-
* server.sendRpcBroadcast(MatchCountdown, { secondsRemaining: 3 });
|
|
326
|
-
* ```
|
|
327
|
-
*/
|
|
328
|
-
sendRpcBroadcast<TSchema extends Record<string, any>>(
|
|
329
|
-
rpc: DefinedRPC<TSchema>,
|
|
330
|
-
data: TSchema,
|
|
331
|
-
priority: MessagePriority = MessagePriority.NORMAL
|
|
332
|
-
): void {
|
|
333
|
-
for (const peerId of this.getPeerIds()) {
|
|
334
|
-
this.sendRPC(peerId, rpc, data, priority);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Register a handler for incoming RPCs from clients (type-safe)
|
|
340
|
-
* Supports multiple handlers per RPC method
|
|
341
|
-
*
|
|
342
|
-
* @template TSchema The RPC data type
|
|
343
|
-
* @param rpc The RPC definition created by defineRPC()
|
|
344
|
-
* @param handler Callback function to handle the RPC
|
|
345
|
-
* @returns Unsubscribe function to remove this handler
|
|
346
|
-
*
|
|
347
|
-
* @example
|
|
348
|
-
* ```ts
|
|
349
|
-
* const BuyItem = defineRPC({
|
|
350
|
-
* method: 'buyItem',
|
|
351
|
-
* schema: { itemId: BinaryCodec.string(32) }
|
|
352
|
-
* });
|
|
353
|
-
*
|
|
354
|
-
* server.onRpc(BuyItem, (peerId, rpc) => {
|
|
355
|
-
* console.log(`${peerId} wants to buy ${rpc.itemId}`);
|
|
356
|
-
* });
|
|
357
|
-
* ```
|
|
358
|
-
*/
|
|
359
|
-
onRPC<TSchema extends Record<string, any>>(
|
|
360
|
-
rpc: DefinedRPC<TSchema>,
|
|
361
|
-
handler: (peerId: string, data: TSchema) => void
|
|
362
|
-
): () => void {
|
|
363
|
-
if (!this.rpcRegistry) {
|
|
364
|
-
throw new Error('RpcRegistry not configured. Pass rpcRegistry to ServerNetworkConfig.');
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
let handlers = this.rpcHandlers.get(rpc.method);
|
|
368
|
-
if (!handlers) {
|
|
369
|
-
handlers = [];
|
|
370
|
-
this.rpcHandlers.set(rpc.method, handlers);
|
|
371
|
-
}
|
|
372
|
-
handlers.push(handler as (peerId: string, data: any) => void);
|
|
373
|
-
|
|
374
|
-
// Return unsubscribe function
|
|
375
|
-
return () => {
|
|
376
|
-
const handlers = this.rpcHandlers.get(rpc.method);
|
|
377
|
-
if (handlers) {
|
|
378
|
-
const index = handlers.indexOf(handler as (peerId: string, data: any) => void);
|
|
379
|
-
if (index > -1) {
|
|
380
|
-
handlers.splice(index, 1);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Send a snapshot to a specific peer using their dedicated snapshot registry (type-safe)
|
|
388
|
-
* @template T The specific snapshot update type
|
|
389
|
-
* @param priority Message priority (default: NORMAL)
|
|
390
|
-
*/
|
|
391
|
-
sendSnapshotToPeer<T extends Partial<TSnapshots>>(
|
|
392
|
-
peerId: string,
|
|
393
|
-
type: string,
|
|
394
|
-
snapshot: Snapshot<T>,
|
|
395
|
-
priority: MessagePriority = MessagePriority.NORMAL
|
|
396
|
-
): void {
|
|
397
|
-
const peer = this.peers.get(peerId);
|
|
398
|
-
if (!peer) {
|
|
399
|
-
this.log(`Cannot send snapshot to unknown peer: ${peerId}`);
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const registry = this.peerSnapshotRegistries.get(peerId);
|
|
404
|
-
if (!registry) {
|
|
405
|
-
throw new Error(`No snapshot registry registered for peer: ${peerId}`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Encode snapshot using peer-specific registry
|
|
409
|
-
const snapshotData = registry.encode(type, snapshot);
|
|
410
|
-
|
|
411
|
-
// Wrap with message type header (use pool if enabled)
|
|
412
|
-
let message: Uint8Array;
|
|
413
|
-
if (this.messagePool) {
|
|
414
|
-
// Zero-copy path: reuse pooled buffer
|
|
415
|
-
message = this.messagePool.wrap(MessageType.SNAPSHOT, snapshotData);
|
|
416
|
-
} else {
|
|
417
|
-
// Fallback path: allocate new buffer
|
|
418
|
-
message = new Uint8Array(1 + snapshotData.byteLength);
|
|
419
|
-
message[0] = MessageType.SNAPSHOT;
|
|
420
|
-
message.set(snapshotData, 1);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Check backpressure and queue if necessary
|
|
424
|
-
if (peer.isBackpressured || peer.sendQueue.length > 0) {
|
|
425
|
-
// Peer is experiencing backpressure, queue the message with priority
|
|
426
|
-
this.queueMessage(peer, message, priority);
|
|
427
|
-
|
|
428
|
-
// Release pooled buffer since we copied it in queueMessage
|
|
429
|
-
if (this.messagePool) {
|
|
430
|
-
this.messagePool.release(message);
|
|
431
|
-
}
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Try to send immediately
|
|
436
|
-
this.sendMessageToPeer(peer, message);
|
|
437
|
-
|
|
438
|
-
// Update tracking
|
|
439
|
-
peer.lastSentTick = snapshot.tick;
|
|
440
|
-
|
|
441
|
-
this.log(`Sent snapshot (type: ${type}, tick: ${snapshot.tick}) to peer ${peerId}`);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Send a snapshot to a peer only if it has changed since the last send.
|
|
446
|
-
* Uses fast binary hash comparison with zero allocations.
|
|
447
|
-
*
|
|
448
|
-
* @template T The specific snapshot update type
|
|
449
|
-
* @param peerId The peer to send to
|
|
450
|
-
* @param type The snapshot type identifier
|
|
451
|
-
* @param snapshot The snapshot to send
|
|
452
|
-
* @param priority Message priority (default: NORMAL)
|
|
453
|
-
* @returns true if snapshot was sent, false if skipped (no change)
|
|
454
|
-
*
|
|
455
|
-
* @remarks
|
|
456
|
-
* This method encodes the snapshot and computes a hash of the binary data.
|
|
457
|
-
* This is more efficient than JSON.stringify because:
|
|
458
|
-
* 1. No string allocations (hash is a number)
|
|
459
|
-
* 2. Binary hashing is faster than string hashing
|
|
460
|
-
* 3. Reuses the encoding work (binary data needed for sending anyway)
|
|
461
|
-
*
|
|
462
|
-
* @example
|
|
463
|
-
* ```ts
|
|
464
|
-
* // In your game tick loop
|
|
465
|
-
* for (const peerId of server.getPeerIds()) {
|
|
466
|
-
* const wasSent = server.sendSnapshotToPeerIfChanged(peerId, 'gameState', {
|
|
467
|
-
* tick: currentTick,
|
|
468
|
-
* updates: gameState
|
|
469
|
-
* });
|
|
470
|
-
* if (wasSent) sentCount++;
|
|
471
|
-
* }
|
|
472
|
-
* ```
|
|
473
|
-
*/
|
|
474
|
-
sendSnapshotToPeerIfChanged<T extends Partial<TSnapshots>>(
|
|
475
|
-
peerId: string,
|
|
476
|
-
type: string,
|
|
477
|
-
snapshot: Snapshot<T>,
|
|
478
|
-
priority: MessagePriority = MessagePriority.NORMAL
|
|
479
|
-
): boolean {
|
|
480
|
-
const peer = this.peers.get(peerId);
|
|
481
|
-
if (!peer) {
|
|
482
|
-
this.log(`Cannot send snapshot to unknown peer: ${peerId}`);
|
|
483
|
-
return false;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const registry = this.peerSnapshotRegistries.get(peerId);
|
|
487
|
-
if (!registry) {
|
|
488
|
-
throw new Error(`No snapshot registry registered for peer: ${peerId}`);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Encode snapshot (needed for both hashing and sending)
|
|
492
|
-
const snapshotData = registry.encode(type, snapshot);
|
|
493
|
-
|
|
494
|
-
// Get or create hash map for this peer
|
|
495
|
-
let peerHashes = this.lastSnapshotHashes.get(peerId);
|
|
496
|
-
if (!peerHashes) {
|
|
497
|
-
peerHashes = new Map<string, number>();
|
|
498
|
-
this.lastSnapshotHashes.set(peerId, peerHashes);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Compute hash of only the updates portion (skip typeId + tick)
|
|
502
|
-
// Binary format: [typeId(1) + tick(4) + updates(...)]
|
|
503
|
-
// We only want to hash the updates, not the tick
|
|
504
|
-
const updatesData = snapshotData.subarray(5); // Skip first 5 bytes
|
|
505
|
-
const currentHash = this.hashBinary(updatesData);
|
|
506
|
-
const lastHash = peerHashes.get(type);
|
|
507
|
-
|
|
508
|
-
// Only send if changed (or first time)
|
|
509
|
-
if (lastHash === undefined || currentHash !== lastHash) {
|
|
510
|
-
// Wrap with message type header (use pool if enabled)
|
|
511
|
-
let message: Uint8Array;
|
|
512
|
-
if (this.messagePool) {
|
|
513
|
-
// Zero-copy path: reuse pooled buffer
|
|
514
|
-
message = this.messagePool.wrap(MessageType.SNAPSHOT, snapshotData);
|
|
515
|
-
} else {
|
|
516
|
-
// Fallback path: allocate new buffer
|
|
517
|
-
message = new Uint8Array(1 + snapshotData.byteLength);
|
|
518
|
-
message[0] = MessageType.SNAPSHOT;
|
|
519
|
-
message.set(snapshotData, 1);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Check backpressure and queue if necessary
|
|
523
|
-
if (peer.isBackpressured || peer.sendQueue.length > 0) {
|
|
524
|
-
this.queueMessage(peer, message, priority);
|
|
525
|
-
if (this.messagePool) {
|
|
526
|
-
this.messagePool.release(message);
|
|
527
|
-
}
|
|
528
|
-
} else {
|
|
529
|
-
// Try to send immediately
|
|
530
|
-
this.sendMessageToPeer(peer, message);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Update tracking
|
|
534
|
-
peer.lastSentTick = snapshot.tick;
|
|
535
|
-
peerHashes.set(type, currentHash);
|
|
536
|
-
|
|
537
|
-
this.log(`Sent snapshot (type: ${type}, tick: ${snapshot.tick}) to peer ${peerId}`);
|
|
538
|
-
return true; // Sent
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
this.log(`Skipped snapshot (type: ${type}) to peer ${peerId} - no change detected`);
|
|
542
|
-
return false; // Skipped (no change)
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Fast binary hash function with zero allocations.
|
|
547
|
-
* Uses FNV-1a algorithm optimized for binary data.
|
|
548
|
-
*
|
|
549
|
-
* @param data The binary data to hash
|
|
550
|
-
* @returns A 32-bit hash value
|
|
551
|
-
*
|
|
552
|
-
* @remarks
|
|
553
|
-
* This is significantly faster than JSON.stringify + string hashing because:
|
|
554
|
-
* - No string allocations
|
|
555
|
-
* - Direct byte-level hashing
|
|
556
|
-
* - Operates on data that's already needed for encoding
|
|
557
|
-
*
|
|
558
|
-
* FNV-1a is chosen for its speed and good distribution properties.
|
|
559
|
-
*/
|
|
560
|
-
private hashBinary(data: Uint8Array): number {
|
|
561
|
-
let hash = 2166136261; // FNV-1a 32-bit offset basis
|
|
562
|
-
|
|
563
|
-
for (let i = 0; i < data.length; i++) {
|
|
564
|
-
hash ^= data[i];
|
|
565
|
-
hash = Math.imul(hash, 16777619); // FNV-1a 32-bit prime
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return hash >>> 0; // Ensure unsigned 32-bit integer
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Queue a message with priority
|
|
573
|
-
*/
|
|
574
|
-
private queueMessage(peer: PeerState, message: Uint8Array, priority: MessagePriority): void {
|
|
575
|
-
const queuedMessage: QueuedMessage = {
|
|
576
|
-
data: new Uint8Array(message), // Copy to avoid pool reuse issues
|
|
577
|
-
priority,
|
|
578
|
-
timestamp: Date.now(),
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
// If queue is full, drop lowest priority message
|
|
582
|
-
if (peer.sendQueue.length >= this.config.maxSendQueueSize) {
|
|
583
|
-
// Sort by priority (ascending) to find lowest priority message
|
|
584
|
-
peer.sendQueue.sort((a, b) => a.priority - b.priority);
|
|
585
|
-
|
|
586
|
-
// Drop the lowest priority message (first in sorted array)
|
|
587
|
-
const dropped = peer.sendQueue.shift();
|
|
588
|
-
this.log(`Send queue full for peer ${peer.peerId}, dropping ${MessagePriority[dropped!.priority]} priority message`);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Insert message in priority order (higher priority first)
|
|
592
|
-
let insertIndex = peer.sendQueue.length;
|
|
593
|
-
for (let i = 0; i < peer.sendQueue.length; i++) {
|
|
594
|
-
if (queuedMessage.priority > peer.sendQueue[i].priority) {
|
|
595
|
-
insertIndex = i;
|
|
596
|
-
break;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
peer.sendQueue.splice(insertIndex, 0, queuedMessage);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Internal: Send a message to a peer, handling pooling and bandwidth tracking
|
|
604
|
-
*/
|
|
605
|
-
private sendMessageToPeer(peer: PeerState, message: Uint8Array): void {
|
|
606
|
-
// Track bandwidth
|
|
607
|
-
this.trackBandwidth(peer, message.byteLength);
|
|
608
|
-
|
|
609
|
-
// Send to peer
|
|
610
|
-
const sendResult = peer.transport.send(message);
|
|
611
|
-
|
|
612
|
-
// Handle both sync and async transports for pool cleanup
|
|
613
|
-
if (this.messagePool) {
|
|
614
|
-
if (sendResult instanceof Promise) {
|
|
615
|
-
// Async transport: wait for send to complete before releasing
|
|
616
|
-
sendResult.then(() => {
|
|
617
|
-
this.messagePool!.release(message);
|
|
618
|
-
// Try to flush queue if there are pending messages
|
|
619
|
-
this.flushSendQueue(peer.peerId);
|
|
620
|
-
}).catch(() => {
|
|
621
|
-
// Mark peer as backpressured on send failure
|
|
622
|
-
peer.isBackpressured = true;
|
|
623
|
-
this.messagePool!.release(message);
|
|
624
|
-
this.log(`Send failed for peer ${peer.peerId}, marking as backpressured`);
|
|
625
|
-
});
|
|
626
|
-
} else {
|
|
627
|
-
// Sync transport: release immediately
|
|
628
|
-
this.messagePool.release(message);
|
|
629
|
-
}
|
|
630
|
-
} else if (sendResult instanceof Promise) {
|
|
631
|
-
// No pool but async transport - still handle failures
|
|
632
|
-
sendResult.catch(() => {
|
|
633
|
-
peer.isBackpressured = true;
|
|
634
|
-
this.log(`Send failed for peer ${peer.peerId}, marking as backpressured`);
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Flush send queue for a peer (called after successful sends)
|
|
641
|
-
* Sends messages in priority order (highest priority first)
|
|
642
|
-
*/
|
|
643
|
-
private flushSendQueue(peerId: string): void {
|
|
644
|
-
const peer = this.peers.get(peerId);
|
|
645
|
-
if (!peer || peer.sendQueue.length === 0) {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Mark as no longer backpressured
|
|
650
|
-
peer.isBackpressured = false;
|
|
651
|
-
|
|
652
|
-
// Send queued messages (up to a limit per flush to avoid blocking)
|
|
653
|
-
// Queue is already sorted by priority (highest first)
|
|
654
|
-
const maxMessagesPerFlush = 10;
|
|
655
|
-
let sent = 0;
|
|
656
|
-
|
|
657
|
-
while (peer.sendQueue.length > 0 && sent < maxMessagesPerFlush) {
|
|
658
|
-
const queuedMessage = peer.sendQueue.shift()!;
|
|
659
|
-
this.sendMessageToPeer(peer, queuedMessage.data);
|
|
660
|
-
sent++;
|
|
661
|
-
|
|
662
|
-
// If we hit backpressure again, stop flushing
|
|
663
|
-
if (peer.isBackpressured) {
|
|
664
|
-
break;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
if (peer.sendQueue.length > 0) {
|
|
669
|
-
this.log(`Peer ${peerId} still has ${peer.sendQueue.length} queued messages`);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Track bandwidth usage for a peer
|
|
675
|
-
*/
|
|
676
|
-
private trackBandwidth(peer: PeerState, bytes: number): void {
|
|
677
|
-
const now = Date.now();
|
|
678
|
-
const windowStart = Math.floor(now / 1000) * 1000;
|
|
679
|
-
|
|
680
|
-
// Reset counter if we're in a new time window
|
|
681
|
-
if (peer.bandwidthWindow !== windowStart) {
|
|
682
|
-
peer.bandwidthWindow = windowStart;
|
|
683
|
-
peer.bytesSent = 0;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
peer.bytesSent += bytes;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* Broadcast a snapshot to all connected peers (type-safe)
|
|
691
|
-
* Each peer receives the snapshot encoded with their own snapshot registry
|
|
692
|
-
* @template T The specific snapshot update type
|
|
693
|
-
* @param priority Message priority (default: NORMAL)
|
|
694
|
-
*/
|
|
695
|
-
broadcastSnapshot<T extends Partial<TSnapshots>>(
|
|
696
|
-
type: string,
|
|
697
|
-
snapshot: Snapshot<T>,
|
|
698
|
-
filter?: (peerId: string) => boolean,
|
|
699
|
-
priority: MessagePriority = MessagePriority.NORMAL
|
|
700
|
-
): void {
|
|
701
|
-
for (const peerId of this.peers.keys()) {
|
|
702
|
-
if (filter && !filter(peerId)) {
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
this.sendSnapshotToPeer(peerId, type, snapshot, priority);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* Advanced: Broadcast with per-peer snapshot customization (type-safe)
|
|
711
|
-
* Allows you to modify the snapshot for each peer (e.g., fog of war, interest management)
|
|
712
|
-
* @template T The specific snapshot update type
|
|
713
|
-
* @param priority Message priority (default: NORMAL)
|
|
714
|
-
*/
|
|
715
|
-
broadcastSnapshotWithCustomization<T extends Partial<TSnapshots>>(
|
|
716
|
-
type: string,
|
|
717
|
-
baseSnapshot: Snapshot<T>,
|
|
718
|
-
customize: (peerId: string, snapshot: Snapshot<T>) => Snapshot<T>,
|
|
719
|
-
priority: MessagePriority = MessagePriority.NORMAL
|
|
720
|
-
): void {
|
|
721
|
-
for (const peerId of this.peers.keys()) {
|
|
722
|
-
const customSnapshot = customize(peerId, baseSnapshot);
|
|
723
|
-
this.sendSnapshotToPeer(peerId, type, customSnapshot, priority);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Get all connected peer IDs
|
|
729
|
-
*/
|
|
730
|
-
getPeerIds(): string[] {
|
|
731
|
-
return Array.from(this.peers.keys());
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Get peer state for a specific peer
|
|
736
|
-
*/
|
|
737
|
-
getPeerState(peerId: string): PeerState | undefined {
|
|
738
|
-
return this.peers.get(peerId);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Update peer metadata
|
|
743
|
-
*/
|
|
744
|
-
setPeerMetadata(peerId: string, key: string, value: unknown): void {
|
|
745
|
-
const peer = this.peers.get(peerId);
|
|
746
|
-
if (peer) {
|
|
747
|
-
peer.metadata[key] = value;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Get bandwidth usage for a peer (bytes per second in current window)
|
|
753
|
-
*/
|
|
754
|
-
getPeerBandwidth(peerId: string): number {
|
|
755
|
-
const peer = this.peers.get(peerId);
|
|
756
|
-
return peer ? peer.bytesSent : 0;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Get total bandwidth usage across all peers
|
|
761
|
-
*/
|
|
762
|
-
getTotalBandwidth(): number {
|
|
763
|
-
let total = 0;
|
|
764
|
-
for (const peer of this.peers.values()) {
|
|
765
|
-
total += peer.bytesSent;
|
|
766
|
-
}
|
|
767
|
-
return total;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Check if a peer is experiencing backpressure
|
|
772
|
-
*/
|
|
773
|
-
isPeerBackpressured(peerId: string): boolean {
|
|
774
|
-
const peer = this.peers.get(peerId);
|
|
775
|
-
return peer ? peer.isBackpressured || peer.sendQueue.length > 0 : false;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Get the last confirmed client tick for a peer.
|
|
780
|
-
* Used for client-side prediction reconciliation.
|
|
781
|
-
*
|
|
782
|
-
* @param peerId The peer ID to query
|
|
783
|
-
* @returns The last processed client tick number, or 0 if peer not found
|
|
784
|
-
*
|
|
785
|
-
* @remarks
|
|
786
|
-
* This is automatically tracked when intents arrive, as all intents
|
|
787
|
-
* include a 'tick' field (added by defineIntent).
|
|
788
|
-
*/
|
|
789
|
-
getConfirmedClientTick(peerId: string): number {
|
|
790
|
-
return this.lastProcessedClientTick.get(peerId) ?? 0;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Set the confirmed client tick for a peer.
|
|
795
|
-
* Rarely needed as this is automatically tracked by handleIntent.
|
|
796
|
-
*
|
|
797
|
-
* @param peerId The peer ID
|
|
798
|
-
* @param tick The client tick number to set
|
|
799
|
-
*
|
|
800
|
-
* @remarks
|
|
801
|
-
* All intents automatically include a 'tick' field via defineIntent(),
|
|
802
|
-
* so this is tracked automatically when intents are received.
|
|
803
|
-
*/
|
|
804
|
-
setConfirmedClientTick(peerId: string, tick: number): void {
|
|
805
|
-
this.lastProcessedClientTick.set(peerId, tick);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Close the server and all connections
|
|
810
|
-
*/
|
|
811
|
-
close(): void | Promise<void> {
|
|
812
|
-
this.log("Closing server...");
|
|
813
|
-
|
|
814
|
-
// Stop heartbeat timer
|
|
815
|
-
if (this.heartbeatTimer) {
|
|
816
|
-
clearInterval(this.heartbeatTimer);
|
|
817
|
-
this.heartbeatTimer = null;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
return this.transport.close();
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
/**
|
|
824
|
-
* Setup transport event handlers
|
|
825
|
-
*/
|
|
826
|
-
private setupTransportHandlers(): void {
|
|
827
|
-
this.transport.onConnection((peer, peerId) => {
|
|
828
|
-
this.handleConnection(peer, peerId);
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
this.transport.onDisconnection((peerId) => {
|
|
832
|
-
this.handleDisconnection(peerId);
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/**
|
|
837
|
-
* Setup heartbeat mechanism
|
|
838
|
-
*/
|
|
839
|
-
private setupHeartbeat(): void {
|
|
840
|
-
if (this.config.heartbeatInterval === 0) {
|
|
841
|
-
return; // Heartbeats disabled
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
this.heartbeatTimer = setInterval(() => {
|
|
845
|
-
this.checkHeartbeats();
|
|
846
|
-
}, this.config.heartbeatInterval);
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Check all peers for heartbeat timeout and send heartbeats
|
|
851
|
-
*/
|
|
852
|
-
private checkHeartbeats(): void {
|
|
853
|
-
const now = Date.now();
|
|
854
|
-
const heartbeatMessage = new Uint8Array([MessageType.HEARTBEAT]);
|
|
855
|
-
|
|
856
|
-
for (const [peerId, peer] of this.peers.entries()) {
|
|
857
|
-
// Check if peer has timed out
|
|
858
|
-
const timeSinceLastMessage = now - peer.lastMessageReceivedAt;
|
|
859
|
-
if (timeSinceLastMessage > this.config.heartbeatTimeout) {
|
|
860
|
-
this.log(`Peer ${peerId} timed out (no message for ${timeSinceLastMessage}ms)`);
|
|
861
|
-
// Close the connection - this will trigger handleDisconnection
|
|
862
|
-
peer.transport.close();
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Send heartbeat to peer
|
|
867
|
-
try {
|
|
868
|
-
peer.transport.send(heartbeatMessage);
|
|
869
|
-
this.trackBandwidth(peer, heartbeatMessage.byteLength);
|
|
870
|
-
} catch (error) {
|
|
871
|
-
this.log(`Failed to send heartbeat to peer ${peerId}: ${error}`);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Handle new peer connection
|
|
878
|
-
*/
|
|
879
|
-
private handleConnection(peer: TPeer, peerId: string): void {
|
|
880
|
-
this.log(`Peer connected: ${peerId}`);
|
|
881
|
-
|
|
882
|
-
// Create peer state
|
|
883
|
-
const now = Date.now();
|
|
884
|
-
const peerState: PeerState = {
|
|
885
|
-
peerId,
|
|
886
|
-
transport: peer,
|
|
887
|
-
lastSentTick: 0,
|
|
888
|
-
connectedAt: now,
|
|
889
|
-
metadata: {},
|
|
890
|
-
messageCount: 0,
|
|
891
|
-
messageCountWindow: now,
|
|
892
|
-
sendQueue: [],
|
|
893
|
-
bytesSent: 0,
|
|
894
|
-
bandwidthWindow: now,
|
|
895
|
-
isBackpressured: false,
|
|
896
|
-
lastMessageReceivedAt: now,
|
|
897
|
-
};
|
|
898
|
-
|
|
899
|
-
this.peers.set(peerId, peerState);
|
|
900
|
-
|
|
901
|
-
// Create per-peer snapshot registry
|
|
902
|
-
const snapshotRegistry = this.createPeerSnapshotRegistry();
|
|
903
|
-
this.peerSnapshotRegistries.set(peerId, snapshotRegistry);
|
|
904
|
-
|
|
905
|
-
// Initialize client tick tracking (for client-side prediction)
|
|
906
|
-
this.lastProcessedClientTick.set(peerId, 0);
|
|
907
|
-
|
|
908
|
-
// Setup message handler for this peer
|
|
909
|
-
peer.onMessage((data) => {
|
|
910
|
-
this.handlePeerMessage(peerId, data);
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
// Notify handlers
|
|
914
|
-
for (const handler of this.connectionHandlers) {
|
|
915
|
-
try {
|
|
916
|
-
handler(peerId);
|
|
917
|
-
} catch (error) {
|
|
918
|
-
// Don't call log here as it might throw, use console.error directly
|
|
919
|
-
if (this.config.debug) {
|
|
920
|
-
console.error(`[ServerNetwork] Error in connection handler: ${error}`);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Handle peer disconnection
|
|
928
|
-
*/
|
|
929
|
-
private handleDisconnection(peerId: string): void {
|
|
930
|
-
this.log(`Peer disconnected: ${peerId}`);
|
|
931
|
-
|
|
932
|
-
this.peers.delete(peerId);
|
|
933
|
-
this.peerSnapshotRegistries.delete(peerId);
|
|
934
|
-
this.lastProcessedClientTick.delete(peerId);
|
|
935
|
-
this.lastSnapshotHashes.delete(peerId);
|
|
936
|
-
|
|
937
|
-
// Notify handlers
|
|
938
|
-
for (const handler of this.disconnectionHandlers) {
|
|
939
|
-
try {
|
|
940
|
-
handler(peerId);
|
|
941
|
-
} catch (error) {
|
|
942
|
-
// Don't call log here as it might throw, use console.error directly
|
|
943
|
-
if (this.config.debug) {
|
|
944
|
-
console.error(`[ServerNetwork] Error in disconnection handler: ${error}`);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Handle incoming message from a peer
|
|
952
|
-
*/
|
|
953
|
-
private handlePeerMessage(peerId: string, data: Uint8Array): void {
|
|
954
|
-
// Update last message received timestamp
|
|
955
|
-
const peer = this.peers.get(peerId);
|
|
956
|
-
if (peer) {
|
|
957
|
-
peer.lastMessageReceivedAt = Date.now();
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
if (data.byteLength === 0) {
|
|
961
|
-
this.log(`Received empty message from peer ${peerId}`);
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
if (data.byteLength > this.config.maxMessageSize) {
|
|
966
|
-
this.log(`Message from peer ${peerId} exceeds max size: ${data.byteLength} > ${this.config.maxMessageSize}`);
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const messageType = data[0];
|
|
971
|
-
const payload = data.subarray(1);
|
|
972
|
-
|
|
973
|
-
switch (messageType) {
|
|
974
|
-
case MessageType.INTENT:
|
|
975
|
-
this.handleIntent(peerId, payload);
|
|
976
|
-
break;
|
|
977
|
-
case MessageType.HEARTBEAT:
|
|
978
|
-
// Heartbeat received - already updated lastMessageReceivedAt above
|
|
979
|
-
this.log(`Received heartbeat from peer ${peerId}`);
|
|
980
|
-
break;
|
|
981
|
-
case MessageType.CUSTOM:
|
|
982
|
-
this.handleRPC(peerId, payload);
|
|
983
|
-
break;
|
|
984
|
-
default:
|
|
985
|
-
this.log(`Unknown message type ${messageType} from peer ${peerId}`);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
/**
|
|
990
|
-
* Decode and handle an intent from a peer
|
|
991
|
-
*/
|
|
992
|
-
private handleIntent(peerId: string, data: Uint8Array): void {
|
|
993
|
-
// Rate limiting check
|
|
994
|
-
if (!this.checkRateLimit(peerId)) {
|
|
995
|
-
this.log(`Rate limit exceeded for peer ${peerId}, dropping intent`);
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
try {
|
|
1000
|
-
// Decode using intent registry (extracts kind from first byte internally)
|
|
1001
|
-
const intent = this.intentRegistry.decode(data);
|
|
1002
|
-
|
|
1003
|
-
this.log(`Received intent (kind: ${intent.kind}) from peer ${peerId}`);
|
|
1004
|
-
|
|
1005
|
-
// Automatically track client tick for client-side prediction
|
|
1006
|
-
// All intents have a 'tick' field added by defineIntent()
|
|
1007
|
-
this.lastProcessedClientTick.set(peerId, intent.tick);
|
|
1008
|
-
|
|
1009
|
-
// Call global intent handlers first (e.g., for tracking pending responses)
|
|
1010
|
-
for (const handler of this.anyIntentHandlers) {
|
|
1011
|
-
try {
|
|
1012
|
-
handler(peerId, intent);
|
|
1013
|
-
} catch (error) {
|
|
1014
|
-
this.log(`Error in global intent handler: ${error}`);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const handlers = this.intentHandlers.get(intent.kind);
|
|
1019
|
-
if (handlers && handlers.length > 0) {
|
|
1020
|
-
// Call all registered handlers
|
|
1021
|
-
for (const handler of handlers) {
|
|
1022
|
-
try {
|
|
1023
|
-
handler(peerId, intent);
|
|
1024
|
-
} catch (error) {
|
|
1025
|
-
this.log(`Error in intent handler: ${error}`);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
} else {
|
|
1029
|
-
this.log(`No handler registered for intent kind: ${intent.kind}`);
|
|
1030
|
-
}
|
|
1031
|
-
} catch (error) {
|
|
1032
|
-
this.log(`Failed to decode intent from peer ${peerId}: ${error}`);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Handle incoming RPC message from a peer
|
|
1038
|
-
*/
|
|
1039
|
-
private handleRPC(peerId: string, data: Uint8Array): void {
|
|
1040
|
-
if (!this.rpcRegistry) {
|
|
1041
|
-
this.log("Received RPC but RpcRegistry not configured");
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// Rate limiting check
|
|
1046
|
-
if (!this.checkRateLimit(peerId)) {
|
|
1047
|
-
this.log(`Rate limit exceeded for peer ${peerId}, dropping RPC`);
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
try {
|
|
1052
|
-
// Decode using RPC registry (returns { method, data })
|
|
1053
|
-
const decoded = this.rpcRegistry.decode(data);
|
|
1054
|
-
|
|
1055
|
-
this.log(`Received RPC (method: ${decoded.method}) from peer ${peerId}`);
|
|
1056
|
-
|
|
1057
|
-
// Call all method-specific handlers if registered
|
|
1058
|
-
const handlers = this.rpcHandlers.get(decoded.method);
|
|
1059
|
-
if (handlers && handlers.length > 0) {
|
|
1060
|
-
for (const handler of handlers) {
|
|
1061
|
-
try {
|
|
1062
|
-
handler(peerId, decoded.data);
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
this.log(`Error in RPC handler: ${error}`);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
} else {
|
|
1068
|
-
this.log(`No handler registered for RPC method: ${decoded.method}`);
|
|
1069
|
-
}
|
|
1070
|
-
} catch (error) {
|
|
1071
|
-
this.log(`Failed to decode RPC from peer ${peerId}: ${error}`);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
/**
|
|
1076
|
-
* Check rate limit for a peer
|
|
1077
|
-
* Returns true if message should be processed, false if rate limit exceeded
|
|
1078
|
-
*/
|
|
1079
|
-
private checkRateLimit(peerId: string): boolean {
|
|
1080
|
-
if (this.config.maxMessagesPerSecond === 0) {
|
|
1081
|
-
return true; // Rate limiting disabled
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const peer = this.peers.get(peerId);
|
|
1085
|
-
if (!peer) {
|
|
1086
|
-
return false;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const now = Date.now();
|
|
1090
|
-
const windowStart = Math.floor(now / 1000) * 1000; // Start of current second
|
|
1091
|
-
|
|
1092
|
-
// Reset counter if we're in a new time window
|
|
1093
|
-
if (peer.messageCountWindow !== windowStart) {
|
|
1094
|
-
peer.messageCountWindow = windowStart;
|
|
1095
|
-
peer.messageCount = 0;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Check if limit would be exceeded BEFORE incrementing
|
|
1099
|
-
if (peer.messageCount >= this.config.maxMessagesPerSecond) {
|
|
1100
|
-
return false;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Only increment if we're allowing this message
|
|
1104
|
-
peer.messageCount++;
|
|
1105
|
-
return true;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/**
|
|
1109
|
-
* Debug logging
|
|
1110
|
-
*/
|
|
1111
|
-
private log(message: string): void {
|
|
1112
|
-
if (this.config.debug) {
|
|
1113
|
-
console.log(`[ServerNetwork] ${message}`);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
}
|