@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.
- package/editor/actions/concrete/TransformModifyAction.d.ts.map +1 -1
- package/editor/actions/concrete/TransformModifyAction.js +10 -2
- package/editor/ecs/component/FieldDescriptor.js +15 -0
- package/editor/ecs/component/createObjectEditor.js +41 -12
- package/editor/ecs/component/editors/ecs/Transform64Editor.d.ts.map +1 -1
- package/editor/ecs/component/editors/ecs/Transform64Editor.js +77 -70
- package/editor/tools/TransformToolV2.d.ts.map +1 -1
- package/editor/tools/TransformToolV2.js +4 -0
- package/editor/tools/v2/TransformControls.d.ts.map +1 -1
- package/editor/tools/v2/TransformControls.js +7 -0
- package/editor/view/ecs/EntityEditor.d.ts.map +1 -1
- package/editor/view/ecs/EntityEditor.js +4 -2
- package/package.json +1 -1
- package/src/core/collection/array/array_insert_at.d.ts +16 -0
- package/src/core/collection/array/array_insert_at.d.ts.map +1 -0
- package/src/core/collection/array/array_insert_at.js +32 -0
- package/src/core/collection/array/array_remove_at.d.ts +15 -0
- package/src/core/collection/array/array_remove_at.d.ts.map +1 -0
- package/src/core/collection/array/array_remove_at.js +30 -0
- package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts +37 -0
- package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_look_rotation.js +172 -0
- package/src/core/geom/Quaternion.d.ts +1 -0
- package/src/core/geom/Quaternion.d.ts.map +1 -1
- package/src/core/geom/Quaternion.js +12 -48
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +9 -4
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +10 -0
- package/src/engine/ecs/transform/t64_look_rotation.d.ts +4 -4
- package/src/engine/ecs/transform/t64_look_rotation.d.ts.map +1 -1
- package/src/engine/ecs/transform/t64_look_rotation.js +7 -18
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +30 -1
- package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +6 -0
- package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/GPUParticleEmitterSystem.js +72 -29
- package/src/engine/input/devices/KeyboardDevice.d.ts +1 -0
- package/src/engine/input/devices/KeyboardDevice.d.ts.map +1 -1
- package/src/engine/input/devices/KeyboardDevice.js +7 -6
- package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts.map +1 -1
- package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +15 -2
- package/src/engine/network/CONGESTION_CONTROL.md +9 -9
- package/src/engine/network/MESSAGE_TRANSPORT.md +364 -0
- package/src/engine/network/NetworkSession.d.ts +31 -11
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +453 -286
- package/src/engine/network/README.md +77 -27
- package/src/engine/network/SMOOTHNESS_DESIGN.md +13 -5
- package/src/engine/network/diagnostics/BandwidthMeter.d.ts +1 -1
- package/src/engine/network/diagnostics/BandwidthMeter.js +1 -1
- package/src/engine/network/orchestrator/HostJoin.d.ts +111 -0
- package/src/engine/network/orchestrator/HostJoin.d.ts.map +1 -0
- package/src/engine/network/orchestrator/HostJoin.js +271 -0
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +151 -112
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +450 -499
- package/src/engine/network/orchestrator/PendingSlices.d.ts +30 -0
- package/src/engine/network/orchestrator/PendingSlices.d.ts.map +1 -0
- package/src/engine/network/orchestrator/PendingSlices.js +83 -0
- package/src/engine/network/orchestrator/ReliableFrames.d.ts +34 -0
- package/src/engine/network/orchestrator/ReliableFrames.d.ts.map +1 -0
- package/src/engine/network/orchestrator/ReliableFrames.js +96 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +77 -10
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +236 -61
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +20 -6
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +33 -8
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +7 -1
- package/src/engine/network/sim/ActionLog.d.ts +32 -5
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +47 -5
- package/src/engine/network/sim/SimActionExecutor.d.ts +42 -2
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +100 -4
- package/src/engine/network/sim/Snapshotter.d.ts +12 -0
- package/src/engine/network/sim/Snapshotter.d.ts.map +1 -1
- package/src/engine/network/sim/Snapshotter.js +43 -0
- package/src/engine/network/state/Baseline.d.ts +1 -1
- package/src/engine/network/state/Baseline.js +1 -1
- package/src/engine/network/state/FrameBufferRing.d.ts +25 -6
- package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
- package/src/engine/network/state/FrameBufferRing.js +51 -6
- package/src/engine/network/state/InputRing.d.ts +4 -0
- package/src/engine/network/state/InputRing.d.ts.map +1 -1
- package/src/engine/network/state/InputRing.js +7 -0
- package/src/engine/network/transport/DeferredTransport.d.ts.map +1 -1
- package/src/engine/network/transport/DeferredTransport.js +1 -0
- package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -1
- package/src/engine/network/transport/LoopbackTransport.js +4 -2
- package/src/engine/network/transport/Transport.d.ts +7 -0
- package/src/engine/network/transport/Transport.d.ts.map +1 -1
- package/src/engine/network/transport/Transport.js +8 -0
- package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/NodeUDPTransport.js +20 -5
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +8 -0
- package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/WebTransportTransport.js +6 -0
- package/src/engine/network/transport/connection/Connection.d.ts +191 -0
- package/src/engine/network/transport/connection/Connection.d.ts.map +1 -0
- package/src/engine/network/transport/connection/Connection.js +1262 -0
- package/src/engine/network/transport/connection/RangeSet.d.ts +94 -0
- package/src/engine/network/transport/connection/RangeSet.d.ts.map +1 -0
- package/src/engine/network/transport/connection/RangeSet.js +260 -0
- package/src/engine/network/transport/connection/RxStream.d.ts +48 -0
- package/src/engine/network/transport/connection/RxStream.d.ts.map +1 -0
- package/src/engine/network/transport/connection/RxStream.js +211 -0
- package/src/engine/network/transport/connection/TxStream.d.ts +90 -0
- package/src/engine/network/transport/connection/TxStream.d.ts.map +1 -0
- package/src/engine/network/transport/connection/TxStream.js +243 -0
- package/src/engine/network/transport/connection/bytes_copy.d.ts +17 -0
- package/src/engine/network/transport/connection/bytes_copy.d.ts.map +1 -0
- package/src/engine/network/transport/connection/bytes_copy.js +34 -0
- package/src/engine/network/transport/connection/varint.d.ts +46 -0
- package/src/engine/network/transport/connection/varint.d.ts.map +1 -0
- package/src/engine/network/transport/connection/varint.js +107 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +38 -7
- package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -1
- package/src/engine/sound/ecs/audio/AudioEmitter.js +35 -7
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.d.ts.map +1 -1
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.js +5 -1
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts +18 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.js +48 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +12 -2
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +173 -9
- package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +9 -3
- package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
- package/src/engine/sound/ecs/audio/LiveEmitterSet.js +28 -8
- package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts +8 -0
- package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts.map +1 -1
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.d.ts.map +1 -1
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.js +5 -1
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts +18 -0
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.js +51 -0
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.d.ts.map +1 -1
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.js +15 -1
- package/src/engine/sound/sopra/README.md +768 -644
- package/src/engine/sound/sopra/SopraEngine.d.ts +104 -8
- package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -1
- package/src/engine/sound/sopra/SopraEngine.js +327 -12
- package/src/engine/sound/sopra/definition/EventDescription.d.ts +28 -2
- package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/EventDescription.js +55 -5
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +10 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +77 -55
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts +31 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.js +75 -0
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +38 -9
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +34 -5
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +7 -2
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +137 -122
- package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +3 -1
- package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/EqEffect.js +146 -131
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +1 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +6 -1
- package/src/engine/sound/sopra/runtime/BusGraph.d.ts +44 -2
- package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -1
- package/src/engine/sound/sopra/runtime/BusGraph.js +83 -43
- package/src/engine/sound/sopra/runtime/EffectChain.d.ts +96 -0
- package/src/engine/sound/sopra/runtime/EffectChain.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/EffectChain.js +286 -0
- package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts +123 -0
- package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/EffectParamOverrides.js +320 -0
- package/src/engine/sound/sopra/runtime/EventInstance.d.ts +55 -5
- package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
- package/src/engine/sound/sopra/runtime/EventInstance.js +201 -26
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +2 -1
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -1
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +6 -1
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +15 -0
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -1
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +32 -0
- package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -1
- package/src/engine/sound/sopra/util/MockAudioContext.js +3 -0
- package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +13 -3
- package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -1
- package/src/engine/sound/sopra/util/fadeOutAndStop.js +25 -13
- package/src/shade/RENDERER_CONTRACT.md +6 -3
- package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts +24 -0
- package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts.map +1 -0
- package/src/shade/device/SHADE_DEVICE_FLOOR.js +34 -0
- package/src/shade/device/ShadeDeviceFailure.d.ts +12 -5
- package/src/shade/device/ShadeDeviceFailure.d.ts.map +1 -1
- package/src/shade/device/ShadeDeviceFailure.js +17 -8
- package/src/shade/device/ShadeDeviceFailureReason.d.ts +1 -1
- package/src/shade/device/ShadeDeviceFailureReason.js +2 -2
- package/src/shade/device/shade_device_floor_unmet.d.ts +13 -0
- package/src/shade/device/shade_device_floor_unmet.d.ts.map +1 -0
- package/src/shade/device/shade_device_floor_unmet.js +32 -0
- package/src/shade/renderer/Renderer.d.ts +2 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +29 -55
- package/src/shade/renderer/buffer/GPUBufferAllocator.d.ts.map +1 -1
- package/src/shade/renderer/buffer/GPUBufferAllocator.js +9 -0
- package/src/shade/renderer/buffer/buffer_pool_ops.d.ts +21 -12
- package/src/shade/renderer/buffer/buffer_pool_ops.d.ts.map +1 -1
- package/src/shade/renderer/buffer/buffer_pool_ops.js +18 -19
- package/src/shade/renderer/gpu_primitive/test/gpu_test.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/test/gpu_test.js +8 -11
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/pool/TransientResourcePool.d.ts +24 -0
- package/src/shade/renderer/pool/TransientResourcePool.d.ts.map +1 -1
- package/src/shade/renderer/pool/TransientResourcePool.js +41 -10
- package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
- package/src/shade/renderer/shader/ComputeShader.js +18 -2
- package/src/shade/renderer/shader/ImageShader.d.ts.map +1 -1
- package/src/shade/renderer/shader/ImageShader.js +230 -218
- package/src/view/graphics/GraphicsContextFailureView.d.ts +1 -2
- package/src/view/graphics/GraphicsContextFailureView.d.ts.map +1 -1
- package/src/view/graphics/GraphicsContextFailureView.js +1 -2
- package/src/engine/network/transport/Channel.js +0 -336
- package/src/engine/network/transport/ReliableCommandPipeline.js +0 -335
- package/src/engine/network/transport/fragments/FragmentAssembler.js +0 -350
- package/src/engine/network/transport/fragments/FragmentRetention.js +0 -191
- package/src/engine/network/transport/fragments/fragment_send.js +0 -157
- 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
|
-
}
|