@woosh/meep-engine 3.46.0 → 3.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/editor/actions/concrete/TransformModifyAction.d.ts.map +1 -1
  2. package/editor/actions/concrete/TransformModifyAction.js +10 -2
  3. package/editor/ecs/component/FieldDescriptor.js +15 -0
  4. package/editor/ecs/component/createObjectEditor.js +41 -12
  5. package/editor/ecs/component/editors/ecs/Transform64Editor.d.ts.map +1 -1
  6. package/editor/ecs/component/editors/ecs/Transform64Editor.js +77 -70
  7. package/editor/tools/TransformToolV2.d.ts.map +1 -1
  8. package/editor/tools/TransformToolV2.js +4 -0
  9. package/editor/tools/v2/TransformControls.d.ts.map +1 -1
  10. package/editor/tools/v2/TransformControls.js +7 -0
  11. package/editor/view/ecs/EntityEditor.d.ts.map +1 -1
  12. package/editor/view/ecs/EntityEditor.js +4 -2
  13. package/package.json +1 -1
  14. package/src/core/collection/array/array_insert_at.d.ts +16 -0
  15. package/src/core/collection/array/array_insert_at.d.ts.map +1 -0
  16. package/src/core/collection/array/array_insert_at.js +32 -0
  17. package/src/core/collection/array/array_remove_at.d.ts +15 -0
  18. package/src/core/collection/array/array_remove_at.d.ts.map +1 -0
  19. package/src/core/collection/array/array_remove_at.js +30 -0
  20. package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts +37 -0
  21. package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts.map +1 -0
  22. package/src/core/geom/3d/quaternion/quat3_look_rotation.js +172 -0
  23. package/src/core/geom/Quaternion.d.ts +1 -0
  24. package/src/core/geom/Quaternion.d.ts.map +1 -1
  25. package/src/core/geom/Quaternion.js +12 -48
  26. package/src/engine/Engine.d.ts.map +1 -1
  27. package/src/engine/Engine.js +9 -4
  28. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  29. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +10 -0
  30. package/src/engine/ecs/transform/t64_look_rotation.d.ts +4 -4
  31. package/src/engine/ecs/transform/t64_look_rotation.d.ts.map +1 -1
  32. package/src/engine/ecs/transform/t64_look_rotation.js +7 -18
  33. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  34. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +30 -1
  35. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +6 -0
  36. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  37. package/src/engine/graphics3/GPUParticleEmitterSystem.js +72 -29
  38. package/src/engine/input/devices/KeyboardDevice.d.ts +1 -0
  39. package/src/engine/input/devices/KeyboardDevice.d.ts.map +1 -1
  40. package/src/engine/input/devices/KeyboardDevice.js +7 -6
  41. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts.map +1 -1
  42. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +15 -2
  43. package/src/engine/network/CONGESTION_CONTROL.md +9 -9
  44. package/src/engine/network/MESSAGE_TRANSPORT.md +364 -0
  45. package/src/engine/network/NetworkSession.d.ts +31 -11
  46. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  47. package/src/engine/network/NetworkSession.js +453 -286
  48. package/src/engine/network/README.md +77 -27
  49. package/src/engine/network/SMOOTHNESS_DESIGN.md +13 -5
  50. package/src/engine/network/diagnostics/BandwidthMeter.d.ts +1 -1
  51. package/src/engine/network/diagnostics/BandwidthMeter.js +1 -1
  52. package/src/engine/network/orchestrator/HostJoin.d.ts +111 -0
  53. package/src/engine/network/orchestrator/HostJoin.d.ts.map +1 -0
  54. package/src/engine/network/orchestrator/HostJoin.js +271 -0
  55. package/src/engine/network/orchestrator/NetworkPeer.d.ts +151 -112
  56. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  57. package/src/engine/network/orchestrator/NetworkPeer.js +450 -499
  58. package/src/engine/network/orchestrator/PendingSlices.d.ts +30 -0
  59. package/src/engine/network/orchestrator/PendingSlices.d.ts.map +1 -0
  60. package/src/engine/network/orchestrator/PendingSlices.js +83 -0
  61. package/src/engine/network/orchestrator/ReliableFrames.d.ts +34 -0
  62. package/src/engine/network/orchestrator/ReliableFrames.d.ts.map +1 -0
  63. package/src/engine/network/orchestrator/ReliableFrames.js +96 -0
  64. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +77 -10
  65. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  66. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +236 -61
  67. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +20 -6
  68. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  69. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +33 -8
  70. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  71. package/src/engine/network/replication/Replicator.js +7 -1
  72. package/src/engine/network/sim/ActionLog.d.ts +32 -5
  73. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  74. package/src/engine/network/sim/ActionLog.js +47 -5
  75. package/src/engine/network/sim/SimActionExecutor.d.ts +42 -2
  76. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  77. package/src/engine/network/sim/SimActionExecutor.js +100 -4
  78. package/src/engine/network/sim/Snapshotter.d.ts +12 -0
  79. package/src/engine/network/sim/Snapshotter.d.ts.map +1 -1
  80. package/src/engine/network/sim/Snapshotter.js +43 -0
  81. package/src/engine/network/state/Baseline.d.ts +1 -1
  82. package/src/engine/network/state/Baseline.js +1 -1
  83. package/src/engine/network/state/FrameBufferRing.d.ts +25 -6
  84. package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
  85. package/src/engine/network/state/FrameBufferRing.js +51 -6
  86. package/src/engine/network/state/InputRing.d.ts +4 -0
  87. package/src/engine/network/state/InputRing.d.ts.map +1 -1
  88. package/src/engine/network/state/InputRing.js +7 -0
  89. package/src/engine/network/transport/DeferredTransport.d.ts.map +1 -1
  90. package/src/engine/network/transport/DeferredTransport.js +1 -0
  91. package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -1
  92. package/src/engine/network/transport/LoopbackTransport.js +4 -2
  93. package/src/engine/network/transport/Transport.d.ts +7 -0
  94. package/src/engine/network/transport/Transport.d.ts.map +1 -1
  95. package/src/engine/network/transport/Transport.js +8 -0
  96. package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -1
  97. package/src/engine/network/transport/adapters/NodeUDPTransport.js +20 -5
  98. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts.map +1 -1
  99. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +8 -0
  100. package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -1
  101. package/src/engine/network/transport/adapters/WebTransportTransport.js +6 -0
  102. package/src/engine/network/transport/connection/Connection.d.ts +191 -0
  103. package/src/engine/network/transport/connection/Connection.d.ts.map +1 -0
  104. package/src/engine/network/transport/connection/Connection.js +1262 -0
  105. package/src/engine/network/transport/connection/RangeSet.d.ts +94 -0
  106. package/src/engine/network/transport/connection/RangeSet.d.ts.map +1 -0
  107. package/src/engine/network/transport/connection/RangeSet.js +260 -0
  108. package/src/engine/network/transport/connection/RxStream.d.ts +48 -0
  109. package/src/engine/network/transport/connection/RxStream.d.ts.map +1 -0
  110. package/src/engine/network/transport/connection/RxStream.js +211 -0
  111. package/src/engine/network/transport/connection/TxStream.d.ts +90 -0
  112. package/src/engine/network/transport/connection/TxStream.d.ts.map +1 -0
  113. package/src/engine/network/transport/connection/TxStream.js +243 -0
  114. package/src/engine/network/transport/connection/bytes_copy.d.ts +17 -0
  115. package/src/engine/network/transport/connection/bytes_copy.d.ts.map +1 -0
  116. package/src/engine/network/transport/connection/bytes_copy.js +34 -0
  117. package/src/engine/network/transport/connection/varint.d.ts +46 -0
  118. package/src/engine/network/transport/connection/varint.d.ts.map +1 -0
  119. package/src/engine/network/transport/connection/varint.js +107 -0
  120. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  121. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +38 -7
  122. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -1
  123. package/src/engine/sound/ecs/audio/AudioEmitter.js +35 -7
  124. package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.d.ts.map +1 -1
  125. package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.js +5 -1
  126. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts +18 -0
  127. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts.map +1 -0
  128. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.js +48 -0
  129. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +12 -2
  130. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
  131. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +173 -9
  132. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +9 -3
  133. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
  134. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +28 -8
  135. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts +8 -0
  136. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts.map +1 -1
  137. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.d.ts.map +1 -1
  138. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.js +5 -1
  139. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts +18 -0
  140. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts.map +1 -0
  141. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.js +51 -0
  142. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.d.ts.map +1 -1
  143. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.js +15 -1
  144. package/src/engine/sound/sopra/README.md +768 -644
  145. package/src/engine/sound/sopra/SopraEngine.d.ts +104 -8
  146. package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -1
  147. package/src/engine/sound/sopra/SopraEngine.js +327 -12
  148. package/src/engine/sound/sopra/definition/EventDescription.d.ts +28 -2
  149. package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -1
  150. package/src/engine/sound/sopra/definition/EventDescription.js +55 -5
  151. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +10 -0
  152. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -1
  153. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +77 -55
  154. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts +31 -0
  155. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts.map +1 -0
  156. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.js +75 -0
  157. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +38 -9
  158. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -1
  159. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +34 -5
  160. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +7 -2
  161. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -1
  162. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +137 -122
  163. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +3 -1
  164. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -1
  165. package/src/engine/sound/sopra/definition/effect/EqEffect.js +146 -131
  166. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +1 -0
  167. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -1
  168. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +6 -1
  169. package/src/engine/sound/sopra/runtime/BusGraph.d.ts +44 -2
  170. package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -1
  171. package/src/engine/sound/sopra/runtime/BusGraph.js +83 -43
  172. package/src/engine/sound/sopra/runtime/EffectChain.d.ts +96 -0
  173. package/src/engine/sound/sopra/runtime/EffectChain.d.ts.map +1 -0
  174. package/src/engine/sound/sopra/runtime/EffectChain.js +286 -0
  175. package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts +123 -0
  176. package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts.map +1 -0
  177. package/src/engine/sound/sopra/runtime/EffectParamOverrides.js +320 -0
  178. package/src/engine/sound/sopra/runtime/EventInstance.d.ts +55 -5
  179. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  180. package/src/engine/sound/sopra/runtime/EventInstance.js +201 -26
  181. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +2 -1
  182. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -1
  183. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +6 -1
  184. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +15 -0
  185. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -1
  186. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +32 -0
  187. package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -1
  188. package/src/engine/sound/sopra/util/MockAudioContext.js +3 -0
  189. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +13 -3
  190. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -1
  191. package/src/engine/sound/sopra/util/fadeOutAndStop.js +25 -13
  192. package/src/shade/RENDERER_CONTRACT.md +6 -3
  193. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts +24 -0
  194. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts.map +1 -0
  195. package/src/shade/device/SHADE_DEVICE_FLOOR.js +34 -0
  196. package/src/shade/device/ShadeDeviceFailure.d.ts +12 -5
  197. package/src/shade/device/ShadeDeviceFailure.d.ts.map +1 -1
  198. package/src/shade/device/ShadeDeviceFailure.js +17 -8
  199. package/src/shade/device/ShadeDeviceFailureReason.d.ts +1 -1
  200. package/src/shade/device/ShadeDeviceFailureReason.js +2 -2
  201. package/src/shade/device/shade_device_floor_unmet.d.ts +13 -0
  202. package/src/shade/device/shade_device_floor_unmet.d.ts.map +1 -0
  203. package/src/shade/device/shade_device_floor_unmet.js +32 -0
  204. package/src/shade/renderer/Renderer.d.ts +2 -0
  205. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  206. package/src/shade/renderer/Renderer.js +29 -55
  207. package/src/shade/renderer/buffer/GPUBufferAllocator.d.ts.map +1 -1
  208. package/src/shade/renderer/buffer/GPUBufferAllocator.js +9 -0
  209. package/src/shade/renderer/buffer/buffer_pool_ops.d.ts +21 -12
  210. package/src/shade/renderer/buffer/buffer_pool_ops.d.ts.map +1 -1
  211. package/src/shade/renderer/buffer/buffer_pool_ops.js +18 -19
  212. package/src/shade/renderer/gpu_primitive/test/gpu_test.d.ts.map +1 -1
  213. package/src/shade/renderer/gpu_primitive/test/gpu_test.js +8 -11
  214. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  215. package/src/shade/renderer/pool/TransientResourcePool.d.ts +24 -0
  216. package/src/shade/renderer/pool/TransientResourcePool.d.ts.map +1 -1
  217. package/src/shade/renderer/pool/TransientResourcePool.js +41 -10
  218. package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
  219. package/src/shade/renderer/shader/ComputeShader.js +18 -2
  220. package/src/shade/renderer/shader/ImageShader.d.ts.map +1 -1
  221. package/src/shade/renderer/shader/ImageShader.js +230 -218
  222. package/src/view/graphics/GraphicsContextFailureView.d.ts +1 -2
  223. package/src/view/graphics/GraphicsContextFailureView.d.ts.map +1 -1
  224. package/src/view/graphics/GraphicsContextFailureView.js +1 -2
  225. package/src/engine/network/transport/Channel.js +0 -336
  226. package/src/engine/network/transport/ReliableCommandPipeline.js +0 -335
  227. package/src/engine/network/transport/fragments/FragmentAssembler.js +0 -350
  228. package/src/engine/network/transport/fragments/FragmentRetention.js +0 -191
  229. package/src/engine/network/transport/fragments/fragment_send.js +0 -157
  230. package/src/engine/network/transport/fragments/packet_size.js +0 -107
@@ -1,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
- }