@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,350 +0,0 @@
|
|
|
1
|
-
import { assert } from "../../../../core/assert.js";
|
|
2
|
-
import {
|
|
3
|
-
FRAGMENT_NACK_INITIAL_DELAY_MS,
|
|
4
|
-
FRAGMENT_NACK_MAX_ROUNDS,
|
|
5
|
-
FRAGMENT_NACK_RESEND_INTERVAL_MS,
|
|
6
|
-
MAX_CHUNKS_PER_MESSAGE,
|
|
7
|
-
} from "./packet_size.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Per-peer reassembly buffer for fragmented packets.
|
|
11
|
-
*
|
|
12
|
-
* When a sender's logical payload exceeds the MTU it gets split into
|
|
13
|
-
* multiple {@link NetworkPacketType.FRAGMENT} packets, each carrying a
|
|
14
|
-
* `(message_id, chunk_index, total_chunks, chunk_bytes)` tuple. The
|
|
15
|
-
* receiver feeds those tuples to {@link receive}; once all chunks for a
|
|
16
|
-
* `message_id` have arrived, `receive` returns the reassembled bytes
|
|
17
|
-
* for the upper layer to dispatch.
|
|
18
|
-
*
|
|
19
|
-
* Loss recovery via NACK: a partially-received message that doesn't
|
|
20
|
-
* complete within `nack_initial_delay_ms` triggers a NACK back to the
|
|
21
|
-
* sender listing the missing chunk indices. NACK rounds repeat at
|
|
22
|
-
* `nack_resend_interval_ms` up to `nack_max_rounds`; after that the
|
|
23
|
-
* receiver gives up on the message and the sender's retention ages
|
|
24
|
-
* out independently. Wire the per-tick driver via {@link service}.
|
|
25
|
-
*
|
|
26
|
-
* Loss-of-last-resort model: if even the NACK retries fail (link is
|
|
27
|
-
* dead, sender retention was evicted, etc.) the message never
|
|
28
|
-
* completes and FIFO-evicts when the pending cap is hit. Upper layers
|
|
29
|
-
* are still expected to tolerate occasional missing messages — NACK
|
|
30
|
-
* recovers most loss, not all.
|
|
31
|
-
*
|
|
32
|
-
* @author Alex Goldring
|
|
33
|
-
* @copyright Company Named Limited (c) 2026
|
|
34
|
-
*/
|
|
35
|
-
export class FragmentAssembler {
|
|
36
|
-
#pending;
|
|
37
|
-
/**
|
|
38
|
-
* FIFO of message_ids in insertion order, for eviction when the
|
|
39
|
-
* pending map exceeds `max_pending_messages`.
|
|
40
|
-
* @type {number[]}
|
|
41
|
-
* @private
|
|
42
|
-
*/
|
|
43
|
-
#order = [];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Message ids delivered recently, so a late or retransmitted chunk for one is dropped rather
|
|
47
|
-
* than opening a fresh reassembly slot. See the note in {@link receive}.
|
|
48
|
-
*
|
|
49
|
-
* Bounded by {@link #completed_order}, which spans the same window `#order` does.
|
|
50
|
-
*
|
|
51
|
-
* @type {Set<number>}
|
|
52
|
-
*/
|
|
53
|
-
#completed = new Set();
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Insertion order of {@link #completed}, for FIFO eviction.
|
|
57
|
-
* @type {number[]}
|
|
58
|
-
*/
|
|
59
|
-
#completed_order = [];
|
|
60
|
-
#nack_scratch;
|
|
61
|
-
/**
|
|
62
|
-
* Scratch for message_ids to drop after a {@link service} pass.
|
|
63
|
-
* Collected during iteration so the underlying map isn't mutated
|
|
64
|
-
* mid-iteration.
|
|
65
|
-
* @type {number[]}
|
|
66
|
-
* @private
|
|
67
|
-
*/
|
|
68
|
-
#drop_scratch = [];
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @param {{
|
|
72
|
-
* max_pending_messages?: number,
|
|
73
|
-
* max_message_size?: number,
|
|
74
|
-
* nack_initial_delay_ms?: number,
|
|
75
|
-
* nack_resend_interval_ms?: number,
|
|
76
|
-
* nack_max_rounds?: number,
|
|
77
|
-
* }} [options]
|
|
78
|
-
* `max_pending_messages` caps the number of in-flight reassembly
|
|
79
|
-
* slots; when full, the oldest pending message is evicted to make
|
|
80
|
-
* room. `max_message_size` rejects reassembled messages larger than
|
|
81
|
-
* this many bytes (defensive against a malicious peer or a wedged
|
|
82
|
-
* sender). The `nack_*` knobs control the retransmit-request timer
|
|
83
|
-
* driven by {@link service}.
|
|
84
|
-
*/
|
|
85
|
-
constructor({
|
|
86
|
-
max_pending_messages = 8,
|
|
87
|
-
max_message_size = 65536,
|
|
88
|
-
nack_initial_delay_ms = FRAGMENT_NACK_INITIAL_DELAY_MS,
|
|
89
|
-
nack_resend_interval_ms = FRAGMENT_NACK_RESEND_INTERVAL_MS,
|
|
90
|
-
nack_max_rounds = FRAGMENT_NACK_MAX_ROUNDS,
|
|
91
|
-
} = {}) {
|
|
92
|
-
assert.isPositiveInteger(max_pending_messages, 'max_pending_messages');
|
|
93
|
-
assert.isPositiveInteger(max_message_size, 'max_message_size');
|
|
94
|
-
assert.isPositiveInteger(nack_initial_delay_ms, 'nack_initial_delay_ms');
|
|
95
|
-
assert.isPositiveInteger(nack_resend_interval_ms, 'nack_resend_interval_ms');
|
|
96
|
-
assert.isPositiveInteger(nack_max_rounds, 'nack_max_rounds');
|
|
97
|
-
|
|
98
|
-
/** @type {number} @readonly */
|
|
99
|
-
this.max_pending_messages = max_pending_messages;
|
|
100
|
-
/** @type {number} @readonly */
|
|
101
|
-
this.max_message_size = max_message_size;
|
|
102
|
-
/** @type {number} @readonly */
|
|
103
|
-
this.nack_initial_delay_ms = nack_initial_delay_ms;
|
|
104
|
-
/** @type {number} @readonly */
|
|
105
|
-
this.nack_resend_interval_ms = nack_resend_interval_ms;
|
|
106
|
-
/** @type {number} @readonly */
|
|
107
|
-
this.nack_max_rounds = nack_max_rounds;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Pending reassembly state per `message_id`. Each entry:
|
|
111
|
-
* total_chunks: uint8 — number of chunks expected
|
|
112
|
-
* chunks: (Uint8Array | undefined)[] — chunk_index → bytes
|
|
113
|
-
* received_count: number of distinct chunks present
|
|
114
|
-
* total_bytes: sum of chunk lengths received so far
|
|
115
|
-
* first_seen_at: ms timestamp of the first {@link service} tick
|
|
116
|
-
* that observed this entry; -1 until stamped
|
|
117
|
-
* last_nack_at: ms timestamp of the most recent NACK emitted
|
|
118
|
-
* for this entry; -1 if never
|
|
119
|
-
* nack_rounds: number of NACKs emitted for this entry
|
|
120
|
-
* @type {Map<number, {total_chunks: number, chunks: Uint8Array[], received_count: number, total_bytes: number, first_seen_at: number, last_nack_at: number, nack_rounds: number}>}
|
|
121
|
-
* @private
|
|
122
|
-
*/
|
|
123
|
-
this.#pending = new Map();
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Reusable scratch for the missing-index list passed to the NACK
|
|
127
|
-
* callback. Sized to the wire maximum so the callback can always
|
|
128
|
-
* iterate `count` entries safely.
|
|
129
|
-
* @type {Uint8Array}
|
|
130
|
-
* @private
|
|
131
|
-
*/
|
|
132
|
-
this.#nack_scratch = new Uint8Array(MAX_CHUNKS_PER_MESSAGE);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Process an incoming fragment. Returns the reassembled bytes when
|
|
137
|
-
* the message is complete, or null otherwise.
|
|
138
|
-
*
|
|
139
|
-
* @param {number} message_id sender-assigned message id
|
|
140
|
-
* @param {number} chunk_index 0-based
|
|
141
|
-
* @param {number} total_chunks expected number of chunks (>= 1)
|
|
142
|
-
* @param {Uint8Array} chunk_bytes source byte array
|
|
143
|
-
* @param {number} chunk_offset start offset within `chunk_bytes`
|
|
144
|
-
* @param {number} chunk_length number of bytes from `chunk_offset` to consume
|
|
145
|
-
* @returns {Uint8Array|null}
|
|
146
|
-
*/
|
|
147
|
-
receive(message_id, chunk_index, total_chunks, chunk_bytes, chunk_offset, chunk_length) {
|
|
148
|
-
assert.isNonNegativeInteger(message_id, 'message_id');
|
|
149
|
-
assert.isNonNegativeInteger(chunk_index, 'chunk_index');
|
|
150
|
-
assert.isPositiveInteger(total_chunks, 'total_chunks');
|
|
151
|
-
assert.isNonNegativeInteger(chunk_offset, 'chunk_offset');
|
|
152
|
-
assert.isNonNegativeInteger(chunk_length, 'chunk_length');
|
|
153
|
-
|
|
154
|
-
if (chunk_index >= total_chunks) {
|
|
155
|
-
// Malformed; discard silently.
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (this.#completed.has(message_id)) {
|
|
160
|
-
// Already delivered. Without this the chunk misses the duplicate guard below - which
|
|
161
|
-
// lives INSIDE an entry, and completion erases the entry - and opens a fresh, and
|
|
162
|
-
// therefore incomplete, reassembly slot. `service()` then NACKs the sender for chunks
|
|
163
|
-
// of a message it has already delivered, the sender's retention honours the NACK, and
|
|
164
|
-
// the message completes and is dispatched a second time: STATE_BURST straight to the
|
|
165
|
-
// application handler, INITIAL_SYNC re-applying a whole snapshot. It is the ordinary
|
|
166
|
-
// NACK/late-chunk race, not an adversarial input - `packet_size.js` sizes the delay
|
|
167
|
-
// expecting the receiver to "occasionally NACK chunks still in transit".
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let entry = this.#pending.get(message_id);
|
|
172
|
-
if (entry === undefined) {
|
|
173
|
-
if (this.#pending.size >= this.max_pending_messages) {
|
|
174
|
-
// Evict the oldest pending message (FIFO).
|
|
175
|
-
const oldest = this.#order.shift();
|
|
176
|
-
if (oldest !== undefined) this.#pending.delete(oldest);
|
|
177
|
-
}
|
|
178
|
-
entry = {
|
|
179
|
-
total_chunks,
|
|
180
|
-
chunks: new Array(total_chunks),
|
|
181
|
-
received_count: 0,
|
|
182
|
-
total_bytes: 0,
|
|
183
|
-
first_seen_at: -1,
|
|
184
|
-
last_nack_at: -1,
|
|
185
|
-
nack_rounds: 0,
|
|
186
|
-
};
|
|
187
|
-
this.#pending.set(message_id, entry);
|
|
188
|
-
this.#order.push(message_id);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (entry.total_chunks !== total_chunks) {
|
|
192
|
-
// Conflicting metadata across fragments for the same message_id.
|
|
193
|
-
// Drop the whole assembly — something is very wrong upstream.
|
|
194
|
-
this.#delete(message_id);
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (entry.chunks[chunk_index] !== undefined) {
|
|
199
|
-
// Duplicate fragment; ignore (network may have retransmitted).
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Defensive: refuse to allocate more memory than max_message_size for
|
|
204
|
-
// this assembly. Check incrementally so a malicious sender can't
|
|
205
|
-
// claim 255 chunks of 65 KB each.
|
|
206
|
-
if (entry.total_bytes + chunk_length > this.max_message_size) {
|
|
207
|
-
this.#delete(message_id);
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const copy = new Uint8Array(chunk_length);
|
|
212
|
-
copy.set(chunk_bytes.subarray(chunk_offset, chunk_offset + chunk_length));
|
|
213
|
-
entry.chunks[chunk_index] = copy;
|
|
214
|
-
entry.received_count++;
|
|
215
|
-
entry.total_bytes += chunk_length;
|
|
216
|
-
|
|
217
|
-
if (entry.received_count < entry.total_chunks) return null;
|
|
218
|
-
|
|
219
|
-
// Complete — assemble.
|
|
220
|
-
const reassembled = new Uint8Array(entry.total_bytes);
|
|
221
|
-
let offset = 0;
|
|
222
|
-
for (let i = 0; i < entry.total_chunks; i++) {
|
|
223
|
-
const c = entry.chunks[i];
|
|
224
|
-
reassembled.set(c, offset);
|
|
225
|
-
offset += c.length;
|
|
226
|
-
}
|
|
227
|
-
this.#delete(message_id);
|
|
228
|
-
|
|
229
|
-
this.#remember_completed(message_id);
|
|
230
|
-
|
|
231
|
-
return reassembled;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Record a delivered message id, evicting the oldest when the window is full.
|
|
236
|
-
*
|
|
237
|
-
* The window is the same size as the pending one: a chunk arriving later than that many
|
|
238
|
-
* distinct messages after its own completed is beyond anything the retransmit machinery
|
|
239
|
-
* would still be holding.
|
|
240
|
-
*
|
|
241
|
-
* @param {number} message_id
|
|
242
|
-
*/
|
|
243
|
-
#remember_completed(message_id) {
|
|
244
|
-
if (this.#completed.has(message_id)) {
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
this.#completed.add(message_id);
|
|
249
|
-
this.#completed_order.push(message_id);
|
|
250
|
-
|
|
251
|
-
while (this.#completed_order.length > this.max_pending_messages) {
|
|
252
|
-
const oldest = this.#completed_order.shift();
|
|
253
|
-
|
|
254
|
-
this.#completed.delete(oldest);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Per-tick maintenance: drive NACK emission for pending messages
|
|
260
|
-
* whose initial delay has elapsed and whose resend interval has
|
|
261
|
-
* lapsed, and drop messages whose NACK budget is exhausted.
|
|
262
|
-
*
|
|
263
|
-
* On the first service tick that observes a pending entry, its
|
|
264
|
-
* arrival is timestamped (so the initial delay is measured from
|
|
265
|
-
* `now_ms` of that tick rather than from the actual wall-clock
|
|
266
|
-
* arrival of the first chunk — at typical tick rates the difference
|
|
267
|
-
* is one tick, negligible vs. the 100 ms initial delay).
|
|
268
|
-
*
|
|
269
|
-
* `on_nack` is invoked once per eligible message with
|
|
270
|
-
* `(message_id, indices, count)`: the missing chunks in index order, but
|
|
271
|
-
* never more of them than the message has delivered. The `indices` argument is a
|
|
272
|
-
* reused internal Uint8Array; only `indices[0..count)` is valid
|
|
273
|
-
* and only for the duration of the call.
|
|
274
|
-
*
|
|
275
|
-
* @param {number} now_ms
|
|
276
|
-
* @param {function(number, Uint8Array, number): void} on_nack
|
|
277
|
-
*/
|
|
278
|
-
service(now_ms, on_nack) {
|
|
279
|
-
assert.isNumber(now_ms, 'now_ms');
|
|
280
|
-
assert.isFunction(on_nack, 'on_nack');
|
|
281
|
-
if (this.#pending.size === 0) return;
|
|
282
|
-
|
|
283
|
-
this.#drop_scratch.length = 0;
|
|
284
|
-
|
|
285
|
-
for (const [message_id, entry] of this.#pending) {
|
|
286
|
-
if (entry.first_seen_at < 0) {
|
|
287
|
-
entry.first_seen_at = now_ms;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (now_ms - entry.first_seen_at < this.nack_initial_delay_ms) continue;
|
|
291
|
-
if (entry.last_nack_at >= 0
|
|
292
|
-
&& now_ms - entry.last_nack_at < this.nack_resend_interval_ms) continue;
|
|
293
|
-
|
|
294
|
-
// Build the missing-chunk list, no longer than the chunks the
|
|
295
|
-
// message has delivered. `total_chunks` is the sender's claim, so
|
|
296
|
-
// it cannot bound the list by itself: one empty fragment claiming
|
|
297
|
-
// 255 chunks drew a NACK of the other 254 each round, some fifty
|
|
298
|
-
// times its own size. A message that has lost more than it has
|
|
299
|
-
// delivered is asked for the rest over the following rounds.
|
|
300
|
-
let missing_count = 0;
|
|
301
|
-
for (let i = 0; i < entry.total_chunks && missing_count < entry.received_count; i++) {
|
|
302
|
-
if (entry.chunks[i] === undefined) {
|
|
303
|
-
this.#nack_scratch[missing_count++] = i;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// Defensive: a fully-received entry should have been deleted
|
|
307
|
-
// in `receive`, but a torn-down concurrent state could leave
|
|
308
|
-
// a zero-missing slot here. Skip rather than emit an empty NACK.
|
|
309
|
-
if (missing_count === 0) continue;
|
|
310
|
-
|
|
311
|
-
on_nack(message_id, this.#nack_scratch, missing_count);
|
|
312
|
-
entry.last_nack_at = now_ms;
|
|
313
|
-
entry.nack_rounds++;
|
|
314
|
-
|
|
315
|
-
if (entry.nack_rounds >= this.nack_max_rounds) {
|
|
316
|
-
this.#drop_scratch.push(message_id);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
for (let i = 0; i < this.#drop_scratch.length; i++) {
|
|
321
|
-
this.#delete(this.#drop_scratch[i]);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Number of in-flight reassembly slots currently held.
|
|
327
|
-
* @returns {number}
|
|
328
|
-
*/
|
|
329
|
-
pending_count() {
|
|
330
|
-
return this.#pending.size;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Drop all in-flight reassembly state. Useful on peer disconnect.
|
|
335
|
-
*/
|
|
336
|
-
clear() {
|
|
337
|
-
this.#pending.clear();
|
|
338
|
-
this.#order.length = 0;
|
|
339
|
-
|
|
340
|
-
this.#completed.clear();
|
|
341
|
-
this.#completed_order.length = 0;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** @private */
|
|
345
|
-
#delete(message_id) {
|
|
346
|
-
this.#pending.delete(message_id);
|
|
347
|
-
const idx = this.#order.indexOf(message_id);
|
|
348
|
-
if (idx >= 0) this.#order.splice(idx, 1);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { assert } from "../../../../core/assert.js";
|
|
2
|
-
import {
|
|
3
|
-
FRAGMENT_RETENTION_MAX_AGE_MS,
|
|
4
|
-
FRAGMENT_RETENTION_MAX_MESSAGES,
|
|
5
|
-
FRAGMENT_RETENTION_MAX_RETRIES,
|
|
6
|
-
} from "./packet_size.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Per-peer sender-side retention buffer for fragmented message bytes,
|
|
10
|
-
* used to satisfy NACK retransmits from the peer's {@link FragmentAssembler}.
|
|
11
|
-
*
|
|
12
|
-
* Lifecycle:
|
|
13
|
-
* 1. {@link retain} — called on each fragmented send; copies the source
|
|
14
|
-
* bytes into the retention so they're available to retransmit. If
|
|
15
|
-
* the per-peer slot cap is hit, the oldest retained entry is FIFO-
|
|
16
|
-
* evicted.
|
|
17
|
-
* 2. {@link consume_nack} — looks up retained bytes for a NACK target
|
|
18
|
-
* and bumps the retry counter. Returns `null` if the entry is gone
|
|
19
|
-
* (aged out, evicted, or out of retry budget — in which case the
|
|
20
|
-
* entry is dropped here too).
|
|
21
|
-
* 3. {@link service} — call once per tick to age out entries older
|
|
22
|
-
* than `max_age_ms`.
|
|
23
|
-
* 4. {@link clear} — drop everything; call on peer disconnect.
|
|
24
|
-
*
|
|
25
|
-
* Memory cost: each retained entry holds a fresh copy of its source
|
|
26
|
-
* payload (the upstream send buffer is a reused scratch). With the
|
|
27
|
-
* default 16 slots × up-to-64 KiB cap on the receiver, worst case is
|
|
28
|
-
* 1 MiB per peer; steady-state is empty because steady traffic fits in
|
|
29
|
-
* one packet and bypasses fragmentation.
|
|
30
|
-
*
|
|
31
|
-
* @author Alex Goldring
|
|
32
|
-
* @copyright Company Named Limited (c) 2026
|
|
33
|
-
*/
|
|
34
|
-
export class FragmentRetention {
|
|
35
|
-
#entries;
|
|
36
|
-
/**
|
|
37
|
-
* FIFO of message_ids in insertion order, for eviction on
|
|
38
|
-
* overflow and for monotonic age-out.
|
|
39
|
-
* @type {number[]}
|
|
40
|
-
* @private
|
|
41
|
-
*/
|
|
42
|
-
#order = [];
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {{
|
|
46
|
-
* max_messages?: number,
|
|
47
|
-
* max_age_ms?: number,
|
|
48
|
-
* max_retries?: number,
|
|
49
|
-
* }} [options]
|
|
50
|
-
* `max_messages` caps simultaneously-retained messages; oldest is
|
|
51
|
-
* FIFO-evicted on overflow. `max_age_ms` is the per-entry TTL.
|
|
52
|
-
* `max_retries` caps how many NACK retransmit rounds a single
|
|
53
|
-
* retained message can satisfy before being dropped.
|
|
54
|
-
*/
|
|
55
|
-
constructor({
|
|
56
|
-
max_messages = FRAGMENT_RETENTION_MAX_MESSAGES,
|
|
57
|
-
max_age_ms = FRAGMENT_RETENTION_MAX_AGE_MS,
|
|
58
|
-
max_retries = FRAGMENT_RETENTION_MAX_RETRIES,
|
|
59
|
-
} = {}) {
|
|
60
|
-
assert.isPositiveInteger(max_messages, 'max_messages');
|
|
61
|
-
assert.isPositiveInteger(max_age_ms, 'max_age_ms');
|
|
62
|
-
assert.isNonNegativeInteger(max_retries, 'max_retries');
|
|
63
|
-
|
|
64
|
-
/** @type {number} @readonly */
|
|
65
|
-
this.max_messages = max_messages;
|
|
66
|
-
/** @type {number} @readonly */
|
|
67
|
-
this.max_age_ms = max_age_ms;
|
|
68
|
-
/** @type {number} @readonly */
|
|
69
|
-
this.max_retries = max_retries;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* message_id → retained entry. `payload` is a fresh copy
|
|
73
|
-
* (independent of the caller's send buffer).
|
|
74
|
-
* @type {Map<number, {payload: Uint8Array, length: number, sent_at: number, retries: number}>}
|
|
75
|
-
* @private
|
|
76
|
-
*/
|
|
77
|
-
this.#entries = new Map();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Copy `payload[0..length)` into retention under `message_id`.
|
|
82
|
-
* Evicts the oldest retained entry if `max_messages` is exceeded.
|
|
83
|
-
*
|
|
84
|
-
* If a retained entry already exists for `message_id` (only legitimate
|
|
85
|
-
* cause: uint16 wrap), the old entry is dropped first so the FIFO
|
|
86
|
-
* position and retry budget restart from the new send.
|
|
87
|
-
*
|
|
88
|
-
* @param {number} message_id
|
|
89
|
-
* @param {Uint8Array} payload source bytes (caller may overwrite after return)
|
|
90
|
-
* @param {number} length number of bytes from `payload[0]` to copy
|
|
91
|
-
* @param {number} now_ms monotonic timestamp for age-out
|
|
92
|
-
*/
|
|
93
|
-
retain(message_id, payload, length, now_ms) {
|
|
94
|
-
assert.isNonNegativeInteger(message_id, 'message_id');
|
|
95
|
-
assert.isNonNegativeInteger(length, 'length');
|
|
96
|
-
assert.isNumber(now_ms, 'now_ms');
|
|
97
|
-
|
|
98
|
-
if (this.#entries.has(message_id)) {
|
|
99
|
-
this.#delete(message_id);
|
|
100
|
-
}
|
|
101
|
-
if (this.#entries.size >= this.max_messages) {
|
|
102
|
-
const oldest = this.#order.shift();
|
|
103
|
-
if (oldest !== undefined) this.#entries.delete(oldest);
|
|
104
|
-
}
|
|
105
|
-
const copy = new Uint8Array(length);
|
|
106
|
-
copy.set(payload.subarray(0, length));
|
|
107
|
-
this.#entries.set(message_id, {
|
|
108
|
-
payload: copy,
|
|
109
|
-
length,
|
|
110
|
-
sent_at: now_ms,
|
|
111
|
-
retries: 0,
|
|
112
|
-
});
|
|
113
|
-
this.#order.push(message_id);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Mark a NACK round for `message_id` and return the retained entry
|
|
118
|
-
* so the caller can re-emit the missing chunks. Returns `null` if
|
|
119
|
-
* no entry exists, or if the per-message retry budget is now
|
|
120
|
-
* exhausted (in which case the entry is dropped).
|
|
121
|
-
*
|
|
122
|
-
* @param {number} message_id
|
|
123
|
-
* @returns {{payload: Uint8Array, length: number}|null}
|
|
124
|
-
*/
|
|
125
|
-
consume_nack(message_id) {
|
|
126
|
-
assert.isNonNegativeInteger(message_id, 'message_id');
|
|
127
|
-
const entry = this.#entries.get(message_id);
|
|
128
|
-
if (entry === undefined) return null;
|
|
129
|
-
entry.retries++;
|
|
130
|
-
if (entry.retries > this.max_retries) {
|
|
131
|
-
this.#delete(message_id);
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
return entry;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Per-tick maintenance: evict any entry older than `max_age_ms`.
|
|
139
|
-
*
|
|
140
|
-
* The FIFO is in strict insertion order, so we can stop at the first
|
|
141
|
-
* still-fresh entry rather than scanning the whole map.
|
|
142
|
-
*
|
|
143
|
-
* @param {number} now_ms
|
|
144
|
-
*/
|
|
145
|
-
service(now_ms) {
|
|
146
|
-
assert.isNumber(now_ms, 'now_ms');
|
|
147
|
-
while (this.#order.length > 0) {
|
|
148
|
-
const id = this.#order[0];
|
|
149
|
-
const entry = this.#entries.get(id);
|
|
150
|
-
if (entry === undefined) {
|
|
151
|
-
this.#order.shift();
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (now_ms - entry.sent_at < this.max_age_ms) break;
|
|
155
|
-
this.#order.shift();
|
|
156
|
-
this.#entries.delete(id);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Number of currently-retained messages.
|
|
162
|
-
* @returns {number}
|
|
163
|
-
*/
|
|
164
|
-
size() {
|
|
165
|
-
return this.#entries.size;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Whether `message_id` is currently retained.
|
|
170
|
-
* @param {number} message_id
|
|
171
|
-
* @returns {boolean}
|
|
172
|
-
*/
|
|
173
|
-
has(message_id) {
|
|
174
|
-
return this.#entries.has(message_id);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Drop every retained entry. Use on peer disconnect.
|
|
179
|
-
*/
|
|
180
|
-
clear() {
|
|
181
|
-
this.#entries.clear();
|
|
182
|
-
this.#order.length = 0;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** @private */
|
|
186
|
-
#delete(message_id) {
|
|
187
|
-
this.#entries.delete(message_id);
|
|
188
|
-
const idx = this.#order.indexOf(message_id);
|
|
189
|
-
if (idx >= 0) this.#order.splice(idx, 1);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { assert } from "../../../../core/assert.js";
|
|
2
|
-
import {
|
|
3
|
-
FRAGMENT_HEADER_BYTES,
|
|
4
|
-
MAX_CHANNEL_PAYLOAD_BYTES,
|
|
5
|
-
MAX_CHUNKS_PER_MESSAGE,
|
|
6
|
-
MAX_FRAGMENT_CHUNK_BYTES,
|
|
7
|
-
} from "./packet_size.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Send a logical payload through a callback, splitting into fragment
|
|
11
|
-
* packets if it exceeds the channel's single-packet capacity.
|
|
12
|
-
*
|
|
13
|
-
* Layout of each emitted fragment packet (before the Channel adds its
|
|
14
|
-
* 8-byte header):
|
|
15
|
-
*
|
|
16
|
-
* uint8 FRAGMENT_PACKET_TYPE
|
|
17
|
-
* uint16 message_id (little-endian)
|
|
18
|
-
* uint8 chunk_index
|
|
19
|
-
* uint8 total_chunks
|
|
20
|
-
* bytes chunk_payload
|
|
21
|
-
*
|
|
22
|
-
* The chunk_payload bytes are slices of the original logical payload,
|
|
23
|
-
* concatenated in chunk_index order they reconstruct the original
|
|
24
|
-
* payload byte-for-byte. The logical payload's own packet-type byte is
|
|
25
|
-
* the first byte of chunk 0 and is preserved by the reassembly.
|
|
26
|
-
*
|
|
27
|
-
* Single-packet messages (payload <= MAX_CHANNEL_PAYLOAD_BYTES) are sent
|
|
28
|
-
* as-is with NO fragmentation header — the receiver dispatches them
|
|
29
|
-
* normally via the FIRST byte. Fragmentation overhead only applies when
|
|
30
|
-
* the payload genuinely doesn't fit.
|
|
31
|
-
*
|
|
32
|
-
* @param {Uint8Array} payload
|
|
33
|
-
* @param {number} payload_length
|
|
34
|
-
* @param {number} message_id sender-assigned, unique per logical message
|
|
35
|
-
* @param {number} fragment_packet_type packet-type byte for fragment packets
|
|
36
|
-
* @param {Uint8Array} scratch reusable scratch buffer; must be >= MAX_CHANNEL_PAYLOAD_BYTES
|
|
37
|
-
* @param {function(Uint8Array, number): void} send_fn
|
|
38
|
-
* invoked once per emitted packet — single send for non-fragmented case,
|
|
39
|
-
* N sends for fragmented case
|
|
40
|
-
*/
|
|
41
|
-
export function send_fragmented(
|
|
42
|
-
payload,
|
|
43
|
-
payload_length,
|
|
44
|
-
message_id,
|
|
45
|
-
fragment_packet_type,
|
|
46
|
-
scratch,
|
|
47
|
-
send_fn,
|
|
48
|
-
) {
|
|
49
|
-
assert.isNonNegativeInteger(payload_length, 'payload_length');
|
|
50
|
-
assert.isNonNegativeInteger(message_id, 'message_id');
|
|
51
|
-
assert.ok(message_id <= 0xFFFF, 'message_id must fit in uint16');
|
|
52
|
-
assert.ok(scratch.length >= MAX_CHANNEL_PAYLOAD_BYTES,
|
|
53
|
-
'scratch buffer must be at least MAX_CHANNEL_PAYLOAD_BYTES');
|
|
54
|
-
|
|
55
|
-
if (payload_length <= MAX_CHANNEL_PAYLOAD_BYTES) {
|
|
56
|
-
// Fits in one packet; send as-is.
|
|
57
|
-
send_fn(payload, payload_length);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const total_chunks = Math.ceil(payload_length / MAX_FRAGMENT_CHUNK_BYTES);
|
|
62
|
-
if (total_chunks > MAX_CHUNKS_PER_MESSAGE) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`send_fragmented: payload of ${payload_length} bytes would require ` +
|
|
65
|
-
`${total_chunks} chunks, exceeding the ${MAX_CHUNKS_PER_MESSAGE}-chunk wire limit`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < total_chunks; i++) {
|
|
70
|
-
const chunk_offset = i * MAX_FRAGMENT_CHUNK_BYTES;
|
|
71
|
-
const remaining = payload_length - chunk_offset;
|
|
72
|
-
const chunk_size = remaining < MAX_FRAGMENT_CHUNK_BYTES ? remaining : MAX_FRAGMENT_CHUNK_BYTES;
|
|
73
|
-
|
|
74
|
-
// Build the fragment packet inside scratch.
|
|
75
|
-
scratch[0] = fragment_packet_type;
|
|
76
|
-
scratch[1] = message_id & 0xFF;
|
|
77
|
-
scratch[2] = (message_id >> 8) & 0xFF;
|
|
78
|
-
scratch[3] = i;
|
|
79
|
-
scratch[4] = total_chunks;
|
|
80
|
-
scratch.set(
|
|
81
|
-
payload.subarray(chunk_offset, chunk_offset + chunk_size),
|
|
82
|
-
FRAGMENT_HEADER_BYTES,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
send_fn(scratch, FRAGMENT_HEADER_BYTES + chunk_size);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Number of chunks {@link send_fragmented} splits a payload of `payload_length` bytes into — the
|
|
91
|
-
* chunk indices a receiver may ask to have re-sent are `[0, count)`.
|
|
92
|
-
*
|
|
93
|
-
* @param {number} payload_length
|
|
94
|
-
* @returns {number}
|
|
95
|
-
*/
|
|
96
|
-
export function fragment_chunk_count(payload_length) {
|
|
97
|
-
// Payloads short enough to not fragment in the first place are never
|
|
98
|
-
// retained, so the 1 is defensive consistency rather than a real case.
|
|
99
|
-
return payload_length <= MAX_FRAGMENT_CHUNK_BYTES
|
|
100
|
-
? 1
|
|
101
|
-
: Math.ceil(payload_length / MAX_FRAGMENT_CHUNK_BYTES);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Re-emit a single fragment chunk for a previously-sent fragmented
|
|
106
|
-
* message. Used by the NACK retransmit path: the sender's
|
|
107
|
-
* {@link FragmentRetention} keeps the source bytes so any chunk can be
|
|
108
|
-
* deterministically rebuilt.
|
|
109
|
-
*
|
|
110
|
-
* The wire layout matches what {@link send_fragmented} would have
|
|
111
|
-
* produced for the same `(message_id, chunk_index, total_chunks)` —
|
|
112
|
-
* receivers can't tell a retransmit from an original.
|
|
113
|
-
*
|
|
114
|
-
* @param {Uint8Array} payload retained source bytes
|
|
115
|
-
* @param {number} payload_length
|
|
116
|
-
* @param {number} message_id
|
|
117
|
-
* @param {number} chunk_index must be < derived total_chunks; silently ignored otherwise
|
|
118
|
-
* @param {number} fragment_packet_type
|
|
119
|
-
* @param {Uint8Array} scratch reusable scratch; must be >= MAX_CHANNEL_PAYLOAD_BYTES
|
|
120
|
-
* @param {function(Uint8Array, number): void} send_fn
|
|
121
|
-
*/
|
|
122
|
-
export function resend_fragment_chunk(
|
|
123
|
-
payload,
|
|
124
|
-
payload_length,
|
|
125
|
-
message_id,
|
|
126
|
-
chunk_index,
|
|
127
|
-
fragment_packet_type,
|
|
128
|
-
scratch,
|
|
129
|
-
send_fn,
|
|
130
|
-
) {
|
|
131
|
-
assert.isNonNegativeInteger(payload_length, 'payload_length');
|
|
132
|
-
assert.isNonNegativeInteger(message_id, 'message_id');
|
|
133
|
-
assert.ok(message_id <= 0xFFFF, 'message_id must fit in uint16');
|
|
134
|
-
assert.isNonNegativeInteger(chunk_index, 'chunk_index');
|
|
135
|
-
assert.ok(scratch.length >= MAX_CHANNEL_PAYLOAD_BYTES,
|
|
136
|
-
'scratch buffer must be at least MAX_CHANNEL_PAYLOAD_BYTES');
|
|
137
|
-
|
|
138
|
-
// Recover total_chunks from the retained payload length.
|
|
139
|
-
const total_chunks = fragment_chunk_count(payload_length);
|
|
140
|
-
if (chunk_index >= total_chunks) return;
|
|
141
|
-
|
|
142
|
-
const chunk_offset = chunk_index * MAX_FRAGMENT_CHUNK_BYTES;
|
|
143
|
-
const remaining = payload_length - chunk_offset;
|
|
144
|
-
const chunk_size = remaining < MAX_FRAGMENT_CHUNK_BYTES ? remaining : MAX_FRAGMENT_CHUNK_BYTES;
|
|
145
|
-
|
|
146
|
-
scratch[0] = fragment_packet_type;
|
|
147
|
-
scratch[1] = message_id & 0xFF;
|
|
148
|
-
scratch[2] = (message_id >> 8) & 0xFF;
|
|
149
|
-
scratch[3] = chunk_index;
|
|
150
|
-
scratch[4] = total_chunks;
|
|
151
|
-
scratch.set(
|
|
152
|
-
payload.subarray(chunk_offset, chunk_offset + chunk_size),
|
|
153
|
-
FRAGMENT_HEADER_BYTES,
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
send_fn(scratch, FRAGMENT_HEADER_BYTES + chunk_size);
|
|
157
|
-
}
|