@woosh/meep-engine 3.46.0 → 3.47.0

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.
Files changed (230) hide show
  1. package/editor/actions/concrete/TransformModifyAction.d.ts.map +1 -1
  2. package/editor/actions/concrete/TransformModifyAction.js +10 -2
  3. package/editor/ecs/component/FieldDescriptor.js +15 -0
  4. package/editor/ecs/component/createObjectEditor.js +41 -12
  5. package/editor/ecs/component/editors/ecs/Transform64Editor.d.ts.map +1 -1
  6. package/editor/ecs/component/editors/ecs/Transform64Editor.js +77 -70
  7. package/editor/tools/TransformToolV2.d.ts.map +1 -1
  8. package/editor/tools/TransformToolV2.js +4 -0
  9. package/editor/tools/v2/TransformControls.d.ts.map +1 -1
  10. package/editor/tools/v2/TransformControls.js +7 -0
  11. package/editor/view/ecs/EntityEditor.d.ts.map +1 -1
  12. package/editor/view/ecs/EntityEditor.js +4 -2
  13. package/package.json +1 -1
  14. package/src/core/collection/array/array_insert_at.d.ts +16 -0
  15. package/src/core/collection/array/array_insert_at.d.ts.map +1 -0
  16. package/src/core/collection/array/array_insert_at.js +32 -0
  17. package/src/core/collection/array/array_remove_at.d.ts +15 -0
  18. package/src/core/collection/array/array_remove_at.d.ts.map +1 -0
  19. package/src/core/collection/array/array_remove_at.js +30 -0
  20. package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts +37 -0
  21. package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts.map +1 -0
  22. package/src/core/geom/3d/quaternion/quat3_look_rotation.js +172 -0
  23. package/src/core/geom/Quaternion.d.ts +1 -0
  24. package/src/core/geom/Quaternion.d.ts.map +1 -1
  25. package/src/core/geom/Quaternion.js +12 -48
  26. package/src/engine/Engine.d.ts.map +1 -1
  27. package/src/engine/Engine.js +9 -4
  28. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  29. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +10 -0
  30. package/src/engine/ecs/transform/t64_look_rotation.d.ts +4 -4
  31. package/src/engine/ecs/transform/t64_look_rotation.d.ts.map +1 -1
  32. package/src/engine/ecs/transform/t64_look_rotation.js +7 -18
  33. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  34. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +30 -1
  35. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +6 -0
  36. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  37. package/src/engine/graphics3/GPUParticleEmitterSystem.js +72 -29
  38. package/src/engine/input/devices/KeyboardDevice.d.ts +1 -0
  39. package/src/engine/input/devices/KeyboardDevice.d.ts.map +1 -1
  40. package/src/engine/input/devices/KeyboardDevice.js +7 -6
  41. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts.map +1 -1
  42. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +15 -2
  43. package/src/engine/network/CONGESTION_CONTROL.md +9 -9
  44. package/src/engine/network/MESSAGE_TRANSPORT.md +364 -0
  45. package/src/engine/network/NetworkSession.d.ts +31 -11
  46. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  47. package/src/engine/network/NetworkSession.js +453 -286
  48. package/src/engine/network/README.md +77 -27
  49. package/src/engine/network/SMOOTHNESS_DESIGN.md +13 -5
  50. package/src/engine/network/diagnostics/BandwidthMeter.d.ts +1 -1
  51. package/src/engine/network/diagnostics/BandwidthMeter.js +1 -1
  52. package/src/engine/network/orchestrator/HostJoin.d.ts +111 -0
  53. package/src/engine/network/orchestrator/HostJoin.d.ts.map +1 -0
  54. package/src/engine/network/orchestrator/HostJoin.js +271 -0
  55. package/src/engine/network/orchestrator/NetworkPeer.d.ts +151 -112
  56. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  57. package/src/engine/network/orchestrator/NetworkPeer.js +450 -499
  58. package/src/engine/network/orchestrator/PendingSlices.d.ts +30 -0
  59. package/src/engine/network/orchestrator/PendingSlices.d.ts.map +1 -0
  60. package/src/engine/network/orchestrator/PendingSlices.js +83 -0
  61. package/src/engine/network/orchestrator/ReliableFrames.d.ts +34 -0
  62. package/src/engine/network/orchestrator/ReliableFrames.d.ts.map +1 -0
  63. package/src/engine/network/orchestrator/ReliableFrames.js +96 -0
  64. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +77 -10
  65. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  66. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +236 -61
  67. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +20 -6
  68. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  69. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +33 -8
  70. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  71. package/src/engine/network/replication/Replicator.js +7 -1
  72. package/src/engine/network/sim/ActionLog.d.ts +32 -5
  73. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  74. package/src/engine/network/sim/ActionLog.js +47 -5
  75. package/src/engine/network/sim/SimActionExecutor.d.ts +42 -2
  76. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  77. package/src/engine/network/sim/SimActionExecutor.js +100 -4
  78. package/src/engine/network/sim/Snapshotter.d.ts +12 -0
  79. package/src/engine/network/sim/Snapshotter.d.ts.map +1 -1
  80. package/src/engine/network/sim/Snapshotter.js +43 -0
  81. package/src/engine/network/state/Baseline.d.ts +1 -1
  82. package/src/engine/network/state/Baseline.js +1 -1
  83. package/src/engine/network/state/FrameBufferRing.d.ts +25 -6
  84. package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
  85. package/src/engine/network/state/FrameBufferRing.js +51 -6
  86. package/src/engine/network/state/InputRing.d.ts +4 -0
  87. package/src/engine/network/state/InputRing.d.ts.map +1 -1
  88. package/src/engine/network/state/InputRing.js +7 -0
  89. package/src/engine/network/transport/DeferredTransport.d.ts.map +1 -1
  90. package/src/engine/network/transport/DeferredTransport.js +1 -0
  91. package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -1
  92. package/src/engine/network/transport/LoopbackTransport.js +4 -2
  93. package/src/engine/network/transport/Transport.d.ts +7 -0
  94. package/src/engine/network/transport/Transport.d.ts.map +1 -1
  95. package/src/engine/network/transport/Transport.js +8 -0
  96. package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -1
  97. package/src/engine/network/transport/adapters/NodeUDPTransport.js +20 -5
  98. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts.map +1 -1
  99. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +8 -0
  100. package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -1
  101. package/src/engine/network/transport/adapters/WebTransportTransport.js +6 -0
  102. package/src/engine/network/transport/connection/Connection.d.ts +191 -0
  103. package/src/engine/network/transport/connection/Connection.d.ts.map +1 -0
  104. package/src/engine/network/transport/connection/Connection.js +1262 -0
  105. package/src/engine/network/transport/connection/RangeSet.d.ts +94 -0
  106. package/src/engine/network/transport/connection/RangeSet.d.ts.map +1 -0
  107. package/src/engine/network/transport/connection/RangeSet.js +260 -0
  108. package/src/engine/network/transport/connection/RxStream.d.ts +48 -0
  109. package/src/engine/network/transport/connection/RxStream.d.ts.map +1 -0
  110. package/src/engine/network/transport/connection/RxStream.js +211 -0
  111. package/src/engine/network/transport/connection/TxStream.d.ts +90 -0
  112. package/src/engine/network/transport/connection/TxStream.d.ts.map +1 -0
  113. package/src/engine/network/transport/connection/TxStream.js +243 -0
  114. package/src/engine/network/transport/connection/bytes_copy.d.ts +17 -0
  115. package/src/engine/network/transport/connection/bytes_copy.d.ts.map +1 -0
  116. package/src/engine/network/transport/connection/bytes_copy.js +34 -0
  117. package/src/engine/network/transport/connection/varint.d.ts +46 -0
  118. package/src/engine/network/transport/connection/varint.d.ts.map +1 -0
  119. package/src/engine/network/transport/connection/varint.js +107 -0
  120. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  121. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +38 -7
  122. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -1
  123. package/src/engine/sound/ecs/audio/AudioEmitter.js +35 -7
  124. package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.d.ts.map +1 -1
  125. package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.js +5 -1
  126. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts +18 -0
  127. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts.map +1 -0
  128. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.js +48 -0
  129. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +12 -2
  130. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
  131. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +173 -9
  132. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +9 -3
  133. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
  134. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +28 -8
  135. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts +8 -0
  136. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts.map +1 -1
  137. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.d.ts.map +1 -1
  138. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.js +5 -1
  139. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts +18 -0
  140. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts.map +1 -0
  141. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.js +51 -0
  142. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.d.ts.map +1 -1
  143. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.js +15 -1
  144. package/src/engine/sound/sopra/README.md +768 -644
  145. package/src/engine/sound/sopra/SopraEngine.d.ts +104 -8
  146. package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -1
  147. package/src/engine/sound/sopra/SopraEngine.js +327 -12
  148. package/src/engine/sound/sopra/definition/EventDescription.d.ts +28 -2
  149. package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -1
  150. package/src/engine/sound/sopra/definition/EventDescription.js +55 -5
  151. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +10 -0
  152. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -1
  153. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +77 -55
  154. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts +31 -0
  155. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts.map +1 -0
  156. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.js +75 -0
  157. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +38 -9
  158. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -1
  159. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +34 -5
  160. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +7 -2
  161. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -1
  162. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +137 -122
  163. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +3 -1
  164. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -1
  165. package/src/engine/sound/sopra/definition/effect/EqEffect.js +146 -131
  166. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +1 -0
  167. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -1
  168. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +6 -1
  169. package/src/engine/sound/sopra/runtime/BusGraph.d.ts +44 -2
  170. package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -1
  171. package/src/engine/sound/sopra/runtime/BusGraph.js +83 -43
  172. package/src/engine/sound/sopra/runtime/EffectChain.d.ts +96 -0
  173. package/src/engine/sound/sopra/runtime/EffectChain.d.ts.map +1 -0
  174. package/src/engine/sound/sopra/runtime/EffectChain.js +286 -0
  175. package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts +123 -0
  176. package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts.map +1 -0
  177. package/src/engine/sound/sopra/runtime/EffectParamOverrides.js +320 -0
  178. package/src/engine/sound/sopra/runtime/EventInstance.d.ts +55 -5
  179. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  180. package/src/engine/sound/sopra/runtime/EventInstance.js +201 -26
  181. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +2 -1
  182. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -1
  183. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +6 -1
  184. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +15 -0
  185. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -1
  186. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +32 -0
  187. package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -1
  188. package/src/engine/sound/sopra/util/MockAudioContext.js +3 -0
  189. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +13 -3
  190. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -1
  191. package/src/engine/sound/sopra/util/fadeOutAndStop.js +25 -13
  192. package/src/shade/RENDERER_CONTRACT.md +6 -3
  193. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts +24 -0
  194. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts.map +1 -0
  195. package/src/shade/device/SHADE_DEVICE_FLOOR.js +34 -0
  196. package/src/shade/device/ShadeDeviceFailure.d.ts +12 -5
  197. package/src/shade/device/ShadeDeviceFailure.d.ts.map +1 -1
  198. package/src/shade/device/ShadeDeviceFailure.js +17 -8
  199. package/src/shade/device/ShadeDeviceFailureReason.d.ts +1 -1
  200. package/src/shade/device/ShadeDeviceFailureReason.js +2 -2
  201. package/src/shade/device/shade_device_floor_unmet.d.ts +13 -0
  202. package/src/shade/device/shade_device_floor_unmet.d.ts.map +1 -0
  203. package/src/shade/device/shade_device_floor_unmet.js +32 -0
  204. package/src/shade/renderer/Renderer.d.ts +2 -0
  205. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  206. package/src/shade/renderer/Renderer.js +29 -55
  207. package/src/shade/renderer/buffer/GPUBufferAllocator.d.ts.map +1 -1
  208. package/src/shade/renderer/buffer/GPUBufferAllocator.js +9 -0
  209. package/src/shade/renderer/buffer/buffer_pool_ops.d.ts +21 -12
  210. package/src/shade/renderer/buffer/buffer_pool_ops.d.ts.map +1 -1
  211. package/src/shade/renderer/buffer/buffer_pool_ops.js +18 -19
  212. package/src/shade/renderer/gpu_primitive/test/gpu_test.d.ts.map +1 -1
  213. package/src/shade/renderer/gpu_primitive/test/gpu_test.js +8 -11
  214. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  215. package/src/shade/renderer/pool/TransientResourcePool.d.ts +24 -0
  216. package/src/shade/renderer/pool/TransientResourcePool.d.ts.map +1 -1
  217. package/src/shade/renderer/pool/TransientResourcePool.js +41 -10
  218. package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
  219. package/src/shade/renderer/shader/ComputeShader.js +18 -2
  220. package/src/shade/renderer/shader/ImageShader.d.ts.map +1 -1
  221. package/src/shade/renderer/shader/ImageShader.js +230 -218
  222. package/src/view/graphics/GraphicsContextFailureView.d.ts +1 -2
  223. package/src/view/graphics/GraphicsContextFailureView.d.ts.map +1 -1
  224. package/src/view/graphics/GraphicsContextFailureView.js +1 -2
  225. package/src/engine/network/transport/Channel.js +0 -336
  226. package/src/engine/network/transport/ReliableCommandPipeline.js +0 -335
  227. package/src/engine/network/transport/fragments/FragmentAssembler.js +0 -350
  228. package/src/engine/network/transport/fragments/FragmentRetention.js +0 -191
  229. package/src/engine/network/transport/fragments/fragment_send.js +0 -157
  230. package/src/engine/network/transport/fragments/packet_size.js +0 -107
@@ -1,336 +0,0 @@
1
- import { assert } from "../../../core/assert.js";
2
- import Signal from "../../../core/events/signal/Signal.js";
3
- import { ack_bitfield_build, ack_bitfield_for_each, } from "../core/sequence/ack_bitfield.js";
4
- import { seq16_advance, seq16_distance, seq16_greater_than, SEQ16_HALF_RANGE } from "../core/sequence/seq16.js";
5
-
6
- /**
7
- * Reliability-notification layer over a {@link Transport}.
8
- *
9
- * Adds a 9-byte header to every packet:
10
- * ```
11
- * uint16: outgoing_seq
12
- * uint16: ack_latest (most recent seq received from peer; 0 when ack_present=0)
13
- * uint32: ack_bitfield (the previous 32 seqs: bit i set => latest-1-i was received)
14
- * uint8: ack_present (1 if this packet carries ack info; 0 if the sender has
15
- * received nothing yet)
16
- * ```
17
- *
18
- * The `ack_present` flag exists because a sender that has received nothing would
19
- * otherwise emit `(ack_latest=0, ack_bitfield=0)`, which is wire-indistinguishable
20
- * from "I have received exactly seq 0" and would spuriously ack the peer's
21
- * outstanding seq 0 — silently breaking the action-stream back-fill and the
22
- * reliable-command retransmit for that seq. The receiver skips all ack/loss
23
- * processing when `ack_present` is 0.
24
- *
25
- * The header gives positive acknowledgement of up to 33 packets per inbound
26
- * packet — robust enough that a single dropped ack-bearing packet typically
27
- * doesn't matter (a later one carries the same info).
28
- *
29
- * Channel does NOT retransmit. It surfaces:
30
- * - `onPacketAcked(seq)` when a peer's ack indicates an outgoing packet arrived
31
- * - `onPacketLost(seq)` when an outgoing packet falls outside the 33-seq ack
32
- * window without ever being acked (peer has acked 32 newer packets, so this
33
- * one is presumed lost)
34
- * - `onPayload(bytes, length)` for the payload portion of received packets,
35
- * with the header stripped
36
- *
37
- * Reliability-by-retransmission lives in a higher-level pipeline (separate file,
38
- * yet to build). Channel just provides the notification primitive.
39
- *
40
- * @author Alex Goldring
41
- * @copyright Company Named Limited (c) 2026
42
- */
43
-
44
- const HEADER_SIZE = 9;
45
-
46
- export class Channel {
47
- #next_outgoing_seq;
48
- #unacked_outgoing;
49
- #latest_received_seq;
50
- #received_seqs;
51
- #send_buffer;
52
- #send_view;
53
- #lost_scratch;
54
- #max_in_flight;
55
-
56
-
57
- /**
58
- * @param {{
59
- * transport: { send(bytes: Uint8Array, length: number): void, onReceive: Signal },
60
- * max_in_flight?: number,
61
- * }} options
62
- * `max_in_flight` hard-caps the number of unacked outgoing seqs. It exists
63
- * to keep the in-flight span well below the seq16 half-range (32768): once
64
- * an unacked seq falls more than half-range behind the newest sent, the
65
- * signed `seq16_distance` used for loss detection wraps and the entry can
66
- * neither be acked nor declared lost — a permanent leak (and a downstream
67
- * `seq_to_pending` leak in the orchestrator). Capping in-flight makes
68
- * that lap impossible. Default 1024 ≫ any realistic in-flight window
69
- * (RTT × send-rate) yet ≪ 32768. Only reached under a sustained one-way
70
- * stall, where eviction-as-lost is the correct verdict anyway.
71
- */
72
- constructor({ transport, max_in_flight = 1024 }) {
73
- assert.ok(transport && typeof transport.send === 'function', 'transport must implement send()');
74
- assert.ok(transport.onReceive, 'transport must expose onReceive signal');
75
- assert.isPositiveInteger(max_in_flight, 'max_in_flight');
76
- assert.lessThan(max_in_flight, SEQ16_HALF_RANGE, 'max_in_flight must stay below the seq16 half-range');
77
-
78
- /** @type {number} @private */
79
- this.#max_in_flight = max_in_flight;
80
-
81
- /**
82
- * @type {{ send(bytes: Uint8Array, length: number): void, onReceive: Signal }}
83
- */
84
- this.transport = transport;
85
- this.transport.onReceive.add(this.#handle_inbound, this);
86
-
87
- /** @type {number} @private */
88
- this.#next_outgoing_seq = 0;
89
-
90
- /**
91
- * Outgoing seqs waiting on ack from the peer.
92
- * @type {Set<number>}
93
- * @private
94
- */
95
- this.#unacked_outgoing = new Set();
96
-
97
- /**
98
- * Most recent seq we've received from the peer; -1 if none yet.
99
- * @type {number}
100
- * @private
101
- */
102
- this.#latest_received_seq = -1;
103
-
104
- /**
105
- * Seqs we have received. Used to build the ack bitfield piggyback.
106
- * Pruned when entries fall outside the 64-seq window.
107
- * @type {Set<number>}
108
- * @private
109
- */
110
- this.#received_seqs = new Set();
111
-
112
- /**
113
- * Pre-allocated send buffer. Resized only if a payload exceeds capacity.
114
- * @type {Uint8Array}
115
- * @private
116
- */
117
- this.#send_buffer = new Uint8Array(1500);
118
- /** @type {DataView} @private */
119
- this.#send_view = new DataView(this.#send_buffer.buffer);
120
-
121
- /**
122
- * Scratch list for collecting "to be marked lost" outgoing seqs during
123
- * an inbound handler (avoids modifying the Set while iterating).
124
- * @type {number[]}
125
- * @private
126
- */
127
- this.#lost_scratch = [];
128
-
129
- /**
130
- * Fired when an outgoing seq has been positively acknowledged by the peer.
131
- * @type {Signal}
132
- */
133
- this.onPacketAcked = new Signal();
134
-
135
- /**
136
- * Fired when an outgoing seq has aged out of the ack window without being acked.
137
- * @type {Signal}
138
- */
139
- this.onPacketLost = new Signal();
140
-
141
- /**
142
- * Fired when an inbound packet's payload is ready (header already stripped).
143
- * Handler args: (payload: Uint8Array starting at offset 0, length: number).
144
- * The Uint8Array is owned by the Channel and may be reused after the handler returns.
145
- * @type {Signal}
146
- */
147
- this.onPayload = new Signal();
148
- }
149
-
150
- /**
151
- * Wrap and send a payload. Returns the seq number assigned to it.
152
- *
153
- * @param {Uint8Array} payload
154
- * @param {number} length payload length in bytes
155
- * @returns {number} seq
156
- */
157
- send(payload, length) {
158
- assert.isNonNegativeInteger(length, 'length');
159
-
160
- const total_size = HEADER_SIZE + length;
161
- if (total_size > this.#send_buffer.length) {
162
- this.#send_buffer = new Uint8Array(Math.max(total_size, this.#send_buffer.length * 2));
163
- this.#send_view = new DataView(this.#send_buffer.buffer);
164
- }
165
-
166
- const seq = this.#next_outgoing_seq;
167
- this.#next_outgoing_seq = seq16_advance(seq);
168
-
169
- const has_received = this.#latest_received_seq >= 0;
170
- const ack_latest = has_received ? this.#latest_received_seq : 0;
171
- const ack_bitfield = has_received
172
- ? ack_bitfield_build(this.#latest_received_seq, s => this.#received_seqs.has(s))
173
- : 0;
174
-
175
- this.#send_view.setUint16(0, seq, true);
176
- this.#send_view.setUint16(2, ack_latest, true);
177
- this.#send_view.setUint32(4, ack_bitfield, true);
178
- this.#send_view.setUint8(8, has_received ? 1 : 0);
179
- if (length > 0) {
180
- this.#send_buffer.set(payload.subarray(0, length), HEADER_SIZE);
181
- }
182
-
183
- this.transport.send(this.#send_buffer, total_size);
184
- this.#unacked_outgoing.add(seq);
185
-
186
- // Enforce the in-flight bound. `#unacked_outgoing` is a Set, so its
187
- // iteration order is insertion order = send order = ascending seq; the
188
- // first entry is therefore the oldest unacked. When we exceed the cap
189
- // (only possible under a sustained one-way stall where acks have
190
- // stopped advancing the window), evict the oldest as lost. This keeps
191
- // the in-flight span below the seq16 half-range so loss detection can't
192
- // alias, and frees the entry (and its orchestrator-side mapping, via
193
- // the onPacketLost handler) instead of leaking it. In a stall the
194
- // oldest is an action-stream packet, whose loss handler does not
195
- // retransmit; reliable-command retransmits are independently bounded by
196
- // ReliableCommandPipeline.max_unacked (≪ this cap), so this cannot loop.
197
- if (this.#unacked_outgoing.size > this.#max_in_flight) {
198
- const oldest = this.#unacked_outgoing.values().next().value;
199
- this.#unacked_outgoing.delete(oldest);
200
- this.onPacketLost.send1(oldest);
201
- }
202
-
203
- return seq;
204
- }
205
-
206
- /**
207
- * Number of outgoing packets sent but not yet acked or marked lost.
208
- * @returns {number}
209
- */
210
- unacked_count() {
211
- return this.#unacked_outgoing.size;
212
- }
213
-
214
- /**
215
- * Most recent seq received from the peer, or -1 if none yet.
216
- * @returns {number}
217
- */
218
- latest_received_seq() {
219
- return this.#latest_received_seq;
220
- }
221
-
222
- /**
223
- * Test-only: seed the next outgoing seq number. Used to fast-forward
224
- * the seq counter so the seq16 wraparound can be exercised in
225
- * unit tests without sending 65k packets. Do not call from production.
226
- * @param {number} seq
227
- */
228
- seed_outgoing_seq_for_testing(seq) {
229
- this.#next_outgoing_seq = seq;
230
- }
231
-
232
- /**
233
- * @param {Uint8Array} bytes
234
- * @param {number} length
235
- * @private
236
- */
237
- #handle_inbound(bytes, length) {
238
- if (length < HEADER_SIZE) {
239
- // Malformed: too short to contain a header. Drop silently.
240
- return;
241
- }
242
-
243
- const view = new DataView(bytes.buffer, bytes.byteOffset, length);
244
- const seq = view.getUint16(0, true);
245
- const ack_latest = view.getUint16(2, true);
246
- const ack_bitfield = view.getUint32(4, true);
247
- const ack_present = view.getUint8(8);
248
-
249
- // Record receipt of this seq.
250
- if (this.#latest_received_seq < 0 || seq16_greater_than(seq, this.#latest_received_seq)) {
251
- this.#latest_received_seq = seq;
252
- }
253
- this.#received_seqs.add(seq);
254
- this.#prune_received_seqs();
255
-
256
- // Process acks/loss only when the peer actually carries ack info. A peer
257
- // that has received nothing sends ack_present=0 with (ack_latest=0,
258
- // ack_bitfield=0); reading that as an ack would spuriously confirm our
259
- // seq 0 (see the seq-0 ambiguity note in the class header).
260
- if (ack_present === 1) {
261
- this.#process_acks(ack_latest, ack_bitfield);
262
- }
263
-
264
- // Strip header and surface the payload.
265
- const payload_length = length - HEADER_SIZE;
266
- const payload = bytes.subarray(HEADER_SIZE, HEADER_SIZE + payload_length);
267
- this.onPayload.send2(payload, payload_length);
268
- }
269
-
270
- /**
271
- * Apply the ack header of an inbound packet: fire `onPacketAcked` for each
272
- * newly-acknowledged outgoing seq, and `onPacketLost` for any unacked seq
273
- * that has aged out of the 33-seq ack window. Only invoked when the inbound
274
- * header sets `ack_present` (the peer has received at least one packet from
275
- * us), so `ack_latest` is a genuine acknowledgement rather than the
276
- * "nothing received yet" sentinel.
277
- *
278
- * @param {number} ack_latest
279
- * @param {number} ack_bitfield
280
- * @private
281
- */
282
- #process_acks(ack_latest, ack_bitfield) {
283
- // Mark each acknowledged outgoing seq.
284
- ack_bitfield_for_each(ack_latest, ack_bitfield, acked_seq => {
285
- if (this.#unacked_outgoing.delete(acked_seq)) {
286
- this.onPacketAcked.send1(acked_seq);
287
- }
288
- });
289
-
290
- // Anything older than (ack_latest - 32) that is still unacked is presumed lost.
291
- // Sanity: if `d` ever approaches half-range we're flying so many
292
- // unacked packets that distance is about to wrap to negative and the
293
- // loss-detection would silently break. Non-goal to support; assert.
294
- this.#lost_scratch.length = 0;
295
- for (const out_seq of this.#unacked_outgoing) {
296
- const d = seq16_distance(out_seq, ack_latest);
297
- assert.lessThan(d, SEQ16_HALF_RANGE / 2, `Channel: outgoing seq ${out_seq} is ${d} packets behind ack_latest ${ack_latest} — approaching seq16 half-range wraparound`);
298
- if (d > 32) {
299
- this.#lost_scratch.push(out_seq);
300
- }
301
- }
302
- for (let i = 0; i < this.#lost_scratch.length; i++) {
303
- const lost = this.#lost_scratch[i];
304
- this.#unacked_outgoing.delete(lost);
305
- this.onPacketLost.send1(lost);
306
- }
307
- }
308
-
309
- /**
310
- * Detach this Channel from its transport. The transport.onReceive subscription
311
- * is removed so the Channel stops processing inbound bytes; the Channel's own
312
- * Signals are left in place (callers usually hold no references to them after
313
- * dispose, so they GC with the Channel). Required before discarding a Channel
314
- * whose transport will be reused — otherwise the old Channel keeps consuming
315
- * inbound packets in parallel with whatever replaces it.
316
- */
317
- dispose() {
318
- this.transport.onReceive.remove(this.#handle_inbound, this);
319
- }
320
-
321
- /**
322
- * Drop received-seq entries that are too old to matter for the next ack bitfield.
323
- * Keeps a window slightly larger than the 33-seq bitfield to absorb out-of-order arrival.
324
- * @private
325
- */
326
- #prune_received_seqs() {
327
- const latest = this.#latest_received_seq;
328
- if (latest < 0) return;
329
- // Window: 64 seqs back. Anything farther can be dropped.
330
- for (const s of this.#received_seqs) {
331
- if (seq16_distance(s, latest) > 64) {
332
- this.#received_seqs.delete(s);
333
- }
334
- }
335
- }
336
- }
@@ -1,335 +0,0 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
3
- import Signal from "../../../core/events/signal/Signal.js";
4
- import { MalformedPacketError } from "../core/MalformedPacketError.js";
5
- import { MAX_CHANNEL_PAYLOAD_BYTES } from "./fragments/packet_size.js";
6
-
7
- /**
8
- * Largest reliable-command payload that fits in a single channel packet.
9
- *
10
- * A command's wire form is `uint8 packet_type + uintVar logical_seq + payload`.
11
- * The pipeline sends it through {@link Channel#send} directly — it does NOT
12
- * fragment — so the whole thing must fit one channel packet
13
- * ({@link MAX_CHANNEL_PAYLOAD_BYTES}). We reserve 11 bytes for the framing:
14
- * 1 packet-type byte + up to 10 bytes of `uintVar` for `logical_seq` (its
15
- * worst case; `logical_seq` grows over the session, so we budget the maximum
16
- * rather than the current value). Sending a larger payload on a UDP-style
17
- * transport would emit an over-MTU datagram that the network silently drops;
18
- * we reject it loudly instead.
19
- *
20
- * @type {number}
21
- */
22
- export const MAX_RELIABLE_COMMAND_PAYLOAD_BYTES = MAX_CHANNEL_PAYLOAD_BYTES - 11;
23
-
24
- /**
25
- * Reliable, at-least-once delivery layer over a {@link Channel}.
26
- *
27
- * Used for messages that must arrive: chat, lobby/room state changes,
28
- * level transitions, kick / disconnect notifications. Distinct from the
29
- * action-stream traffic the rest of the netcode uses, which is UDP-style
30
- * best-effort with per-frame back-fill compensating for losses.
31
- *
32
- * Wire layout (after the {@link Channel}'s 8-byte header and the
33
- * `packet_type` byte that {@link NetworkPeer}'s dispatcher consumes):
34
- *
35
- * uintVar logical_seq sender-assigned, monotonically increasing
36
- * bytes command_payload
37
- *
38
- * Reliability mechanism: each outgoing command goes out as one
39
- * Channel.send call. The returned channel-level seq is tracked in
40
- * `__unacked`. When the Channel later fires:
41
- * - `onPacketAcked(seq)`: drop the entry — the command was delivered
42
- * (the receiver de-duplicated by logical_seq, so a "duplicate" from
43
- * a previous retransmit is harmless).
44
- * - `onPacketLost(seq)`: the Channel's 33-packet ack window aged out
45
- * this seq without seeing it acked. Re-send the same logical
46
- * command on a fresh channel seq.
47
- *
48
- * Receiver side: a sliding-window de-dupe of recent `logical_seq` values.
49
- * Anything seen within the last `max_received_history` is silently
50
- * dropped; first-time arrivals fire `onCommand` with the payload bytes.
51
- *
52
- * Ordering: NOT guaranteed. Commands may arrive in any order. If you
53
- * need in-order delivery (level transitions before subsequent chat),
54
- * key payloads with a counter and reorder in the application handler.
55
- *
56
- * Lifecycle constraints:
57
- * - `max_unacked` caps outstanding commands. Send throws when full
58
- * (signals real backpressure — receiver gone or path is broken).
59
- * - `max_received_history` caps the de-dupe set. A retransmit older
60
- * than this window will be re-delivered as a duplicate to the
61
- * application; not a problem in practice because the sender's
62
- * `max_unacked` keeps the inflight set bounded too.
63
- *
64
- * Caveats:
65
- * - Loss detection relies on the Channel's seq window, which advances
66
- * when subsequent packets are sent. Under sustained no-traffic
67
- * conditions a single lost reliable command may stay unacked
68
- * indefinitely — by convention the action-stream's per-tick traffic
69
- * keeps the window moving even when the application is otherwise
70
- * idle. Real production code may want a timer-based retransmit
71
- * fallback layered on top of this.
72
- *
73
- * @author Alex Goldring
74
- * @copyright Company Named Limited (c) 2026
75
- */
76
- export class ReliableCommandPipeline {
77
- /**
78
- * Monotonic counter for outgoing logical seqs. Receiver dedups on
79
- * this value; sender retransmits use the SAME logical_seq with a
80
- * new channel_seq.
81
- * @type {number}
82
- * @private
83
- */
84
- #next_send_seq = 0;
85
- #unacked;
86
- #received_seqs;
87
- /**
88
- * FIFO order of received logical_seqs, for pruning when
89
- * `__received_seqs` exceeds `max_received_history`.
90
- * @type {number[]}
91
- * @private
92
- */
93
- #received_order = [];
94
- #send_scratch;
95
- #bound_acked;
96
- #bound_lost;
97
-
98
- /**
99
- * @param {{
100
- * channel: import("./Channel.js").Channel,
101
- * packet_type: number,
102
- * max_unacked?: number,
103
- * max_received_history?: number,
104
- * max_retries?: number,
105
- * }} options
106
- */
107
- constructor({
108
- channel,
109
- packet_type,
110
- max_unacked = 64,
111
- max_received_history = 64,
112
- max_retries = 16,
113
- }) {
114
- assert.ok(channel && typeof channel.send === 'function', 'channel must be a Channel');
115
- assert.isNonNegativeInteger(packet_type, 'packet_type');
116
- assert.ok(packet_type <= 0xFF, 'packet_type must fit in a uint8');
117
- assert.isPositiveInteger(max_unacked, 'max_unacked');
118
- assert.isPositiveInteger(max_received_history, 'max_received_history');
119
- assert.isPositiveInteger(max_retries, 'max_retries');
120
-
121
- /** @type {import("./Channel.js").Channel} */
122
- this.channel = channel;
123
- /** @type {number} @readonly */
124
- this.packet_type = packet_type;
125
- /** @type {number} @readonly */
126
- this.max_unacked = max_unacked;
127
- /** @type {number} @readonly */
128
- this.max_received_history = max_received_history;
129
- /**
130
- * Maximum retransmissions per logical command before it is dropped
131
- * and {@link onCommandAbandoned} fires. Bounded so a permanently-
132
- * black-holed peer can't pin retransmit traffic on the channel
133
- * forever. The default (16) tolerates roughly half a second of
134
- * sustained loss at typical channel cadences before giving up.
135
- * @type {number} @readonly
136
- */
137
- this.max_retries = max_retries;
138
-
139
- /**
140
- * Outstanding commands keyed by their CURRENT channel_seq. On
141
- * retransmit, the entry is moved to the new channel_seq.
142
- * `retries` counts how many times this logical command has been
143
- * resent; the entry is dropped when it exceeds `max_retries`.
144
- * @type {Map<number, {logical_seq: number, payload: Uint8Array, retries: number}>}
145
- * @private
146
- */
147
- this.#unacked = new Map();
148
-
149
- /**
150
- * Logical seqs received recently. Sliding window for dedup.
151
- * @type {Set<number>}
152
- * @private
153
- */
154
- this.#received_seqs = new Set();
155
-
156
- /**
157
- * Scratch buffer for assembling outgoing packets.
158
- * @type {BinaryBuffer}
159
- * @private
160
- */
161
- this.#send_scratch = new BinaryBuffer();
162
- this.#send_scratch.setCapacity(1024);
163
-
164
- /**
165
- * Fired when a brand-new logical command arrives (not a duplicate).
166
- * Args: `(buf, payload_offset, payload_length)`. The buffer is
167
- * shared scratch; consumers must copy bytes if they need them to
168
- * outlive the handler.
169
- * @type {Signal}
170
- */
171
- this.onCommand = new Signal();
172
-
173
- /**
174
- * Fired when a sent command has been retransmitted `max_retries`
175
- * times without an ack and is being given up on. Args:
176
- * `(logical_seq)`. Subscribers typically surface this as a "command
177
- * failed to deliver" event to the application, or treat it as a
178
- * hard signal to tear down the session.
179
- * @type {Signal}
180
- */
181
- this.onCommandAbandoned = new Signal();
182
-
183
- // Channel reliability notifications.
184
- this.#bound_acked = (seq) => this.#on_packet_acked(seq);
185
- this.#bound_lost = (seq) => this.#on_packet_lost(seq);
186
- channel.onPacketAcked.add(this.#bound_acked);
187
- channel.onPacketLost.add(this.#bound_lost);
188
- }
189
-
190
- /**
191
- * Send a logical command. Returns the assigned `logical_seq` (useful
192
- * for application-level diagnostics; the receiver dedups internally
193
- * regardless).
194
- *
195
- * @param {Uint8Array} payload
196
- * @param {number} length
197
- * @returns {number}
198
- */
199
- send(payload, length) {
200
- assert.isNonNegativeInteger(length, 'length');
201
- if (length > MAX_RELIABLE_COMMAND_PAYLOAD_BYTES) {
202
- // The pipeline does not fragment; an over-MTU command would be sent
203
- // as one oversized datagram that UDP-style transports silently drop.
204
- // Fail loudly so the caller chunks at the application layer (or uses
205
- // a stream transport) rather than losing the message on the wire.
206
- throw new Error(
207
- `ReliableCommandPipeline.send: payload of ${length} bytes exceeds the ` +
208
- `${MAX_RELIABLE_COMMAND_PAYLOAD_BYTES}-byte single-packet limit; this layer does not ` +
209
- `fragment — chunk the message at the application layer or send it over a stream transport`
210
- );
211
- }
212
- if (this.#unacked.size >= this.max_unacked) {
213
- throw new Error(
214
- `ReliableCommandPipeline.send: ${this.#unacked.size} commands unacked ` +
215
- `(max ${this.max_unacked}); receiver likely disconnected or path saturated`
216
- );
217
- }
218
- const logical_seq = this.#next_send_seq;
219
- this.#next_send_seq++;
220
-
221
- // Defensive copy: the caller's buffer may be reused after this call.
222
- const payload_copy = new Uint8Array(length);
223
- payload_copy.set(payload.subarray(0, length));
224
-
225
- const channel_seq = this.#transmit(logical_seq, payload_copy);
226
- this.#unacked.set(channel_seq, { logical_seq, payload: payload_copy, retries: 0 });
227
- return logical_seq;
228
- }
229
-
230
- /**
231
- * Called by the orchestrator (typically {@link NetworkPeer}'s
232
- * dispatcher) when a packet of this pipeline's type arrives. `buf`
233
- * is positioned just past the packet-type byte.
234
- *
235
- * @param {BinaryBuffer} buf
236
- * @param {number} total_length full transport-level payload length,
237
- * including the consumed packet-type byte
238
- */
239
- handle_inbound(buf, total_length) {
240
- // Bounds-checked before the read, like every other parse path in the dispatcher.
241
- //
242
- // `#recv_buffer` is a reused scratch: `writeBytes(bytes, 0, length)` overwrites only the
243
- // first `length` bytes, so everything past them is the previous, longer packet's content.
244
- // A truncated RELIABLE_COMMAND - just the type byte, from a peer or a truncating middlebox
245
- // - let `readUintVar` walk into those leftovers and produce a bogus `logical_seq`, which
246
- // was then recorded in the received set, so a genuine later command carrying that seq was
247
- // silently dropped as a duplicate.
248
- if (buf.position >= total_length) {
249
- throw new MalformedPacketError(`ReliableCommandPipeline: RELIABLE_COMMAND truncated before its sequence number`);
250
- }
251
-
252
- const logical_seq = buf.readUintVar();
253
-
254
- // ...and after it: `readUintVar` may have consumed bytes past the packet even when one was
255
- // available to start with. Subtracting gave a negative payload length and an offset into an
256
- // unrelated packet's bytes, handed straight to the application.
257
- if (buf.position > total_length) {
258
- throw new MalformedPacketError(`ReliableCommandPipeline: RELIABLE_COMMAND sequence number runs past the packet`);
259
- }
260
-
261
- if (this.#received_seqs.has(logical_seq)) {
262
- // Duplicate from a retransmit; ignore.
263
- return;
264
- }
265
- this.#received_seqs.add(logical_seq);
266
- this.#received_order.push(logical_seq);
267
- // Prune. Oldest-first.
268
- while (this.#received_order.length > this.max_received_history) {
269
- const oldest = this.#received_order.shift();
270
- this.#received_seqs.delete(oldest);
271
- }
272
-
273
- const payload_offset = buf.position;
274
- const payload_length = total_length - payload_offset;
275
- this.onCommand.send3(buf, payload_offset, payload_length);
276
- }
277
-
278
- /**
279
- * Number of commands sent that haven't yet been acked. Useful for
280
- * backpressure / progress UI.
281
- * @returns {number}
282
- */
283
- unacked_count() {
284
- return this.#unacked.size;
285
- }
286
-
287
- /**
288
- * Drop subscriptions and clear state. Required before discarding a
289
- * pipeline whose channel will outlive it.
290
- */
291
- dispose() {
292
- this.channel.onPacketAcked.remove(this.#bound_acked);
293
- this.channel.onPacketLost.remove(this.#bound_lost);
294
- this.#unacked.clear();
295
- this.#received_seqs.clear();
296
- this.#received_order.length = 0;
297
- }
298
-
299
- /** @private */
300
- #transmit(logical_seq, payload) {
301
- const buf = this.#send_scratch;
302
- // Worst-case capacity: type byte + 10 bytes for varint + payload.
303
- const needed = 1 + 10 + payload.length;
304
- if (buf.raw_bytes.length < needed) buf.setCapacity(needed);
305
- buf.position = 0;
306
- buf.writeUint8(this.packet_type);
307
- buf.writeUintVar(logical_seq);
308
- buf.writeBytes(payload, 0, payload.length);
309
- return this.channel.send(buf.raw_bytes, buf.position);
310
- }
311
-
312
- /** @private */
313
- #on_packet_acked(channel_seq) {
314
- this.#unacked.delete(channel_seq);
315
- }
316
-
317
- /** @private */
318
- #on_packet_lost(channel_seq) {
319
- const entry = this.#unacked.get(channel_seq);
320
- if (entry === undefined) return;
321
- this.#unacked.delete(channel_seq);
322
-
323
- if (entry.retries >= this.max_retries) {
324
- // Permanently black-holed peer or fundamentally broken path —
325
- // surface the failure to the application and stop retrying.
326
- this.onCommandAbandoned.send1(entry.logical_seq);
327
- return;
328
- }
329
-
330
- // Retransmit on a fresh channel_seq. Same logical_seq, same payload.
331
- entry.retries++;
332
- const new_channel_seq = this.#transmit(entry.logical_seq, entry.payload);
333
- this.#unacked.set(new_channel_seq, entry);
334
- }
335
- }