@woosh/meep-engine 3.14.3 → 3.14.5
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/package.json +1 -1
- package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +733 -726
- package/src/engine/network/NetworkSession.d.ts +24 -4
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +28 -4
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +96 -7
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +392 -68
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +105 -13
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +306 -59
- package/src/engine/network/replication/Replicator.d.ts +43 -7
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +309 -111
- package/src/engine/network/sim/ActionLog.d.ts +23 -3
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +26 -3
- package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
- package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
- package/src/engine/network/sim/ActionRecordCursor.js +120 -0
- package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
- package/src/engine/network/sim/RewindEngine.js +13 -29
- package/src/engine/network/sim/SimAction.d.ts +15 -0
- package/src/engine/network/sim/SimAction.d.ts.map +1 -1
- package/src/engine/network/sim/SimAction.js +15 -0
- package/src/engine/network/sim/SimActionExecutor.d.ts +51 -2
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +74 -7
- package/src/engine/network/transport/Channel.d.ts +1 -1
- package/src/engine/network/transport/Channel.js +1 -1
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +4 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +6 -0
- package/src/shade/playground/particle_system/README.md +98 -0
- package/src/shade/playground/particle_system/index.html +124 -0
- package/src/shade/playground/particle_system/main.d.ts +2 -0
- package/src/shade/playground/particle_system/main.d.ts.map +1 -0
- package/src/shade/playground/particle_system/main.js +180 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts +52 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -0
- package/src/shade/playground/particle_system/particle_scene.js +553 -0
- package/src/shade/playground/skinned_mesh_soup/README.md +143 -0
- package/src/shade/playground/skinned_mesh_soup/index.html +202 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts +7 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/main.js +567 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +29 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +428 -0
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
- package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
- package/src/shade/renderer/particles/DESIGN.md +5 -5
- package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +1 -1
- package/src/shade/renderer/particles/cull/chunk_particle_cull.js +3 -1
- package/src/shade/renderer/particles/data/chunk_particle_emitter.js +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts +89 -0
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -0
- package/src/shade/renderer/particles/isa/InstructionStream.js +162 -0
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +1 -48
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/ParticleAssembler.js +25 -114
- package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
- package/src/shade/renderer/particles/particle_prototype.js +181 -180
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
- package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +4 -2
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +4 -4
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +7 -5
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +5 -2
- package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_render.js +9 -2
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -1
- package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
- package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
- package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts +0 -2
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts.map +0 -1
- package/src/shade/renderer/particles/prototypeParticleSystem.js +0 -448
|
@@ -7,7 +7,7 @@ import { Replicator } from "../replication/Replicator.js";
|
|
|
7
7
|
import { AlwaysRelevantScope } from "../replication/ScopeFilter.js";
|
|
8
8
|
import { ActionLog } from "../sim/ActionLog.js";
|
|
9
9
|
import { ReplicatedComponentRegistry } from "../sim/ReplicatedComponentRegistry.js";
|
|
10
|
-
import { SimActionExecutor } from "../sim/SimActionExecutor.js";
|
|
10
|
+
import { MAX_PEER_ID, SimActionExecutor } from "../sim/SimActionExecutor.js";
|
|
11
11
|
import { SimActionRegistry } from "../sim/SimActionRegistry.js";
|
|
12
12
|
import { snapshotter_emit } from "../sim/Snapshotter.js";
|
|
13
13
|
import { Baseline } from "../state/Baseline.js";
|
|
@@ -27,6 +27,19 @@ import { ReliableCommandPipeline } from "../transport/ReliableCommandPipeline.js
|
|
|
27
27
|
const DISCONNECT_TEXT_ENCODER = new TextEncoder();
|
|
28
28
|
const DISCONNECT_TEXT_DECODER = new TextDecoder();
|
|
29
29
|
|
|
30
|
+
// ACTION_STREAM slice header, after the packet-type byte: uint32 frame_start,
|
|
31
|
+
// uint32 frame_end, uint8 head. See NetworkPacketType.ACTION_STREAM.
|
|
32
|
+
const ACTION_STREAM_SLICE_HEADER_BYTES = 9;
|
|
33
|
+
|
|
34
|
+
// Widest slice a header may claim. A slice covers frames from the pack start
|
|
35
|
+
// to the last frame packed, so it never spans more than the sender's action
|
|
36
|
+
// log holds — a ring of tens or hundreds of frames. The receiver raises its
|
|
37
|
+
// apply watermark to a head's frame_end, so a header claiming 2^32 frames
|
|
38
|
+
// would have it skip every frame the sender ever sends after; that is a
|
|
39
|
+
// sender bug or a sender lying about its own stream, and either is dropped as
|
|
40
|
+
// malformed rather than obeyed.
|
|
41
|
+
const MAX_ACTION_STREAM_SLICE_FRAMES = 65536;
|
|
42
|
+
|
|
30
43
|
/**
|
|
31
44
|
* Wire packet types. The first byte of every NetworkPeer-routed payload picks
|
|
32
45
|
* the dispatch path. New types can be added without breaking the action stream
|
|
@@ -36,6 +49,20 @@ const DISCONNECT_TEXT_DECODER = new TextDecoder();
|
|
|
36
49
|
* @enum {number}
|
|
37
50
|
*/
|
|
38
51
|
export const NetworkPacketType = {
|
|
52
|
+
/**
|
|
53
|
+
* A slice of the action stream. Wire layout (after this type byte):
|
|
54
|
+
*
|
|
55
|
+
* uint32 frame_start first frame the slice covers
|
|
56
|
+
* uint32 frame_end last frame the slice covers
|
|
57
|
+
* uint8 head 1 if this is the first slice of the sender's tick
|
|
58
|
+
* bytes groups {@link Replicator} wire format
|
|
59
|
+
*
|
|
60
|
+
* A tick may send several slices, and the receiver applies them in frame
|
|
61
|
+
* order: a slice that is not a head and starts past what the receiver has
|
|
62
|
+
* applied is held until the slices before it land. A head's start is the
|
|
63
|
+
* earliest frame the sender will send again, so it is applied at once.
|
|
64
|
+
* A packet of just the type byte carries nothing and is ignored.
|
|
65
|
+
*/
|
|
39
66
|
ACTION_STREAM: 0,
|
|
40
67
|
RECOVERY_REQUEST: 1,
|
|
41
68
|
STATE_BURST: 2,
|
|
@@ -168,14 +195,40 @@ export const ResumeRejectReason = Object.freeze({
|
|
|
168
195
|
NoSessionToken: 5,
|
|
169
196
|
});
|
|
170
197
|
|
|
198
|
+
/**
|
|
199
|
+
* One outbound action-stream send, awaiting confirmation.
|
|
200
|
+
*
|
|
201
|
+
* A send occupies one transport seq when it fits the MTU and N consecutive
|
|
202
|
+
* seqs when it has to be fragmented. All of those seqs map to the SAME record:
|
|
203
|
+
* the receiver reassembles a fragmented message all-or-nothing, so the frame
|
|
204
|
+
* range is confirmed only when `unacked` reaches zero without `abandoned`
|
|
205
|
+
* having been set by a loss along the way.
|
|
206
|
+
*
|
|
207
|
+
* A tick may make several sends to one peer, each carrying the next slice of
|
|
208
|
+
* the owed range (see {@link NetworkPeer#flush_outbound}). They confirm
|
|
209
|
+
* independently, and the baseline advances only through slices confirmed in
|
|
210
|
+
* order — a later slice landing says nothing about an earlier one still in
|
|
211
|
+
* flight. `head` marks the first slice of a tick, whose start is the pack
|
|
212
|
+
* start itself; see {@link NetworkPeer#__credit_confirmed}.
|
|
213
|
+
*
|
|
214
|
+
* @typedef {{frame_start: number, frame_end: number, head: boolean, unacked: number, abandoned: boolean}} PendingAck
|
|
215
|
+
* @property {number} frame_start first frame in the packed range
|
|
216
|
+
* @property {number} frame_end last frame in the packed range
|
|
217
|
+
* @property {boolean} head this send began at the pack start, so nothing
|
|
218
|
+
* sendable lies before it and its confirmation credits outright
|
|
219
|
+
* @property {number} unacked seqs of this send not yet acked or declared lost
|
|
220
|
+
* @property {boolean} abandoned at least one seq was declared lost, so the
|
|
221
|
+
* message is forfeit and must never advance the baseline
|
|
222
|
+
*/
|
|
223
|
+
|
|
171
224
|
/**
|
|
172
225
|
* Per-tick orchestrator that wires the action infrastructure to one or more
|
|
173
226
|
* peers over a Transport.
|
|
174
227
|
*
|
|
175
228
|
* Holds a single world (`EntityComponentDataset`) plus the action machinery
|
|
176
229
|
* (executor, replicator, action log). For each connected peer it owns a
|
|
177
|
-
* `Channel` and a `seq →
|
|
178
|
-
* `Baseline.last_acked(peer_id)` watermark.
|
|
230
|
+
* `Channel` and a `seq` → {@link PendingAck} map so that ack notifications
|
|
231
|
+
* advance the `Baseline.last_acked(peer_id)` watermark.
|
|
179
232
|
*
|
|
180
233
|
* Use as both server (many connected peers) and client (one peer = the server).
|
|
181
234
|
* The asymmetry is in *who* connects — the orchestrator itself is symmetric.
|
|
@@ -222,6 +275,25 @@ export class NetworkPeer {
|
|
|
222
275
|
*/
|
|
223
276
|
#malformed_packets_dropped = 0;
|
|
224
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Scratch list of the transport seqs one send produced — one entry for a
|
|
280
|
+
* single-packet send, one per fragment for a fragmented one. Reused across
|
|
281
|
+
* sends so the common path allocates nothing; only valid until the next
|
|
282
|
+
* `__send_to_peer` call.
|
|
283
|
+
* @type {number[]}
|
|
284
|
+
* @private
|
|
285
|
+
*/
|
|
286
|
+
#sent_seqs = [];
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Scratch for the slice header of the ACTION_STREAM packet being
|
|
290
|
+
* dispatched, handed to `Replicator.unpack_from_peer`, which copies what
|
|
291
|
+
* it keeps. Only valid for the duration of that call.
|
|
292
|
+
* @type {{frame_start: number, frame_end: number, head: boolean}}
|
|
293
|
+
* @private
|
|
294
|
+
*/
|
|
295
|
+
#slice = { frame_start: 0, frame_end: 0, head: false };
|
|
296
|
+
|
|
225
297
|
/**
|
|
226
298
|
* @param {{
|
|
227
299
|
* world: EntityComponentDataset,
|
|
@@ -233,6 +305,7 @@ export class NetworkPeer {
|
|
|
233
305
|
* initial_buffer_size?: number,
|
|
234
306
|
* mutation_ledger?: MutationLedger|null,
|
|
235
307
|
* changed_set_capacity?: number,
|
|
308
|
+
* max_packets_per_tick?: number,
|
|
236
309
|
* }} options
|
|
237
310
|
*/
|
|
238
311
|
constructor({
|
|
@@ -245,11 +318,13 @@ export class NetworkPeer {
|
|
|
245
318
|
initial_buffer_size = 1024,
|
|
246
319
|
mutation_ledger = null,
|
|
247
320
|
changed_set_capacity = 256,
|
|
321
|
+
max_packets_per_tick = 8,
|
|
248
322
|
}) {
|
|
249
323
|
assert.ok(world && typeof world.createEntity === 'function', 'world must be an EntityComponentDataset');
|
|
250
324
|
assert.ok(binary_registry, 'binary_registry required');
|
|
251
325
|
assert.isFunction(replicated_components.forEach, 'replicated_components must be iterable');
|
|
252
326
|
assert.isFunction(action_classes.forEach, 'action_classes must be iterable');
|
|
327
|
+
assert.isPositiveInteger(max_packets_per_tick, 'max_packets_per_tick');
|
|
253
328
|
|
|
254
329
|
/** @type {EntityComponentDataset} */
|
|
255
330
|
this.world = world;
|
|
@@ -272,6 +347,33 @@ export class NetworkPeer {
|
|
|
272
347
|
/** @type {ActionLog} */
|
|
273
348
|
this.action_log = new ActionLog({ frame_capacity, initial_buffer_size });
|
|
274
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Most action-stream packets {@link flush_outbound} may send one peer
|
|
352
|
+
* in a tick. Each carries the next slice of what the peer is owed and
|
|
353
|
+
* is confirmed on its own, so this caps how fast a peer that has
|
|
354
|
+
* fallen behind catches up: a slice is re-sent every tick until its ack
|
|
355
|
+
* returns, so a peer is confirmed at most this many packets of frames
|
|
356
|
+
* per round trip, against the one frame per tick the simulation
|
|
357
|
+
* produces. One packet a tick keeps up only while a round trip's frames
|
|
358
|
+
* fit one packet; past that the owed range grows until it meets the
|
|
359
|
+
* ring floor and frames are lost.
|
|
360
|
+
*
|
|
361
|
+
* It is also the bandwidth ceiling under catch-up — this many MTUs per
|
|
362
|
+
* tick per peer — and it has to stay inside the channel's ack window.
|
|
363
|
+
* A peer's ack header covers its latest 33 seqs, so with a peer that
|
|
364
|
+
* replies once a tick, `33 / max_packets_per_tick` consecutive lost
|
|
365
|
+
* replies leave a delivered packet outside the window and it is
|
|
366
|
+
* declared lost: a wasted re-send rather than a defect, since the
|
|
367
|
+
* receiver skips frames it has already applied. Eight tolerates three
|
|
368
|
+
* such losses in a row (one tick in sixteen at 50% loss, one in ten
|
|
369
|
+
* thousand at 10%), and lets a 60 Hz stream at 100 ms RTT carry a full
|
|
370
|
+
* packet of actions per frame. Lower it for peers that reply less often
|
|
371
|
+
* than every tick.
|
|
372
|
+
*
|
|
373
|
+
* @type {number}
|
|
374
|
+
*/
|
|
375
|
+
this.max_packets_per_tick = max_packets_per_tick;
|
|
376
|
+
|
|
275
377
|
/**
|
|
276
378
|
* Optional per-tick mutation tracker. When a `MutationLedger` is supplied,
|
|
277
379
|
* this peer records each tick's affected network IDs into a fresh
|
|
@@ -317,8 +419,8 @@ export class NetworkPeer {
|
|
|
317
419
|
this.baseline = new Baseline();
|
|
318
420
|
|
|
319
421
|
/**
|
|
320
|
-
* peer_id → { channel, transport,
|
|
321
|
-
* @type {Map<number, { channel: Channel, transport: Object,
|
|
422
|
+
* peer_id → { channel, transport, seq_to_pending, confirmed, on_payload, on_acked }
|
|
423
|
+
* @type {Map<number, { channel: Channel, transport: Object, seq_to_pending: Map<number, PendingAck>, confirmed: Int32Array, on_payload: Function, on_acked: Function }>}
|
|
322
424
|
* @private
|
|
323
425
|
*/
|
|
324
426
|
this.#peers = new Map();
|
|
@@ -463,13 +565,38 @@ export class NetworkPeer {
|
|
|
463
565
|
*/
|
|
464
566
|
connect_peer(peer_id, transport) {
|
|
465
567
|
assert.isNonNegativeInteger(peer_id, 'peer_id');
|
|
568
|
+
// The action log records a peer's id as the provenance of its actions,
|
|
569
|
+
// in one byte whose top two values are reserved for locally-authored
|
|
570
|
+
// and sim-derived records. A peer holding one of those would have its
|
|
571
|
+
// input read back as the local sim's own output and dropped by the
|
|
572
|
+
// first rollback replay of its frame, so refuse the connection rather
|
|
573
|
+
// than lose its input later. Real runtime check — asserts strip in prod.
|
|
574
|
+
if (peer_id > MAX_PEER_ID) {
|
|
575
|
+
throw new Error(`NetworkPeer.connect_peer: peer id ${peer_id} is out of range; must be in [0, ${MAX_PEER_ID}] — 254 and 255 are reserved provenance markers in the action log`);
|
|
576
|
+
}
|
|
466
577
|
if (this.#peers.has(peer_id)) {
|
|
467
578
|
throw new Error(`NetworkPeer.connect_peer: peer ${peer_id} already connected`);
|
|
468
579
|
}
|
|
469
580
|
|
|
470
581
|
const channel = new Channel({ transport });
|
|
471
|
-
const
|
|
472
|
-
|
|
582
|
+
const seq_to_pending = new Map();
|
|
583
|
+
// Frames this peer has confirmed that the baseline has not reached yet,
|
|
584
|
+
// because a slice before them is still in flight. A ring indexed by
|
|
585
|
+
// frame modulo capacity holding the frame number — see
|
|
586
|
+
// __credit_confirmed.
|
|
587
|
+
const confirmed = new Int32Array(this.action_log.frame_capacity).fill(-1);
|
|
588
|
+
// The reassembly window is sized from the burst. A frame too big for
|
|
589
|
+
// one packet goes out as a fragmented slice, and a tick may send
|
|
590
|
+
// `max_packets_per_tick` of them; jitter that holds a message's later
|
|
591
|
+
// fragments behind the next ticks' first fragments leaves that many
|
|
592
|
+
// partial messages per tick in flight at once. The assembler evicts
|
|
593
|
+
// the oldest partial when its window is full, both fragments of the
|
|
594
|
+
// evicted message are still acked, and the sender credits the slice —
|
|
595
|
+
// a silent loss. Four ticks of burst keeps the window ahead of any
|
|
596
|
+
// jitter the NACK timer would not already have caught.
|
|
597
|
+
const fragment_assembler = new FragmentAssembler({
|
|
598
|
+
max_pending_messages: Math.max(8, 4 * this.max_packets_per_tick),
|
|
599
|
+
});
|
|
473
600
|
const fragment_retention = new FragmentRetention();
|
|
474
601
|
const reliable_pipeline = new ReliableCommandPipeline({
|
|
475
602
|
channel,
|
|
@@ -513,11 +640,25 @@ export class NetworkPeer {
|
|
|
513
640
|
const packet_type = this.#recv_buffer.readUint8();
|
|
514
641
|
|
|
515
642
|
switch (packet_type) {
|
|
516
|
-
case NetworkPacketType.ACTION_STREAM:
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
643
|
+
case NetworkPacketType.ACTION_STREAM: {
|
|
644
|
+
// Just the type byte: an empty stream, nothing to apply.
|
|
645
|
+
if (length === 1) break;
|
|
646
|
+
if (length < 1 + ACTION_STREAM_SLICE_HEADER_BYTES) {
|
|
647
|
+
throw new MalformedPacketError(`NetworkPeer.dispatch: ACTION_STREAM truncated before its slice header`);
|
|
648
|
+
}
|
|
649
|
+
const slice = this.#slice;
|
|
650
|
+
slice.frame_start = this.#recv_buffer.readUint32();
|
|
651
|
+
slice.frame_end = this.#recv_buffer.readUint32();
|
|
652
|
+
slice.head = this.#recv_buffer.readUint8() !== 0;
|
|
653
|
+
if (slice.frame_end < slice.frame_start
|
|
654
|
+
|| slice.frame_end - slice.frame_start >= MAX_ACTION_STREAM_SLICE_FRAMES) {
|
|
655
|
+
throw new MalformedPacketError(`NetworkPeer.dispatch: ACTION_STREAM slice [${slice.frame_start}, ${slice.frame_end}] is inverted or wider than any action log`);
|
|
656
|
+
}
|
|
657
|
+
// The Replicator opens a frame on the action log per group
|
|
658
|
+
// (one per sender frame), applying slices in frame order.
|
|
659
|
+
this.replicator.unpack_from_peer(peer_id, this.#recv_buffer, length, slice);
|
|
520
660
|
break;
|
|
661
|
+
}
|
|
521
662
|
case NetworkPacketType.RECOVERY_REQUEST: {
|
|
522
663
|
const start_tick = this.#recv_buffer.readUintVar();
|
|
523
664
|
const end_tick = this.#recv_buffer.readUintVar();
|
|
@@ -639,22 +780,43 @@ export class NetworkPeer {
|
|
|
639
780
|
}
|
|
640
781
|
};
|
|
641
782
|
|
|
642
|
-
// Ack:
|
|
783
|
+
// Ack: a send is confirmed once every seq it went out under has been
|
|
784
|
+
// acked. For a single-packet send that is the one seq. For a
|
|
785
|
+
// fragmented send it is all N fragments, because the receiver applies
|
|
786
|
+
// nothing until it has reassembled the whole message; advancing on the
|
|
787
|
+
// first fragment's ack would retire a frame range the receiver never
|
|
788
|
+
// saw, from the only mechanism that could still re-send it. What a
|
|
789
|
+
// confirmed send credits to the baseline is decided in order, by
|
|
790
|
+
// __credit_confirmed.
|
|
643
791
|
const on_acked = (seq) => {
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
792
|
+
const pending = seq_to_pending.get(seq);
|
|
793
|
+
if (pending === undefined) return;
|
|
794
|
+
seq_to_pending.delete(seq);
|
|
795
|
+
pending.unacked--;
|
|
796
|
+
if (pending.unacked === 0 && !pending.abandoned) {
|
|
797
|
+
this.#credit_confirmed(peer_id, confirmed, pending);
|
|
648
798
|
}
|
|
649
799
|
};
|
|
650
800
|
|
|
651
801
|
// Loss: a packet that aged out of the ack window without being acked
|
|
652
|
-
// leaves a dead seq→
|
|
653
|
-
// frame was never confirmed; back-fill
|
|
654
|
-
//
|
|
655
|
-
//
|
|
802
|
+
// leaves a dead seq→record entry. The baseline must NOT advance (the
|
|
803
|
+
// frame range was never confirmed; the back-fill re-sends it under a
|
|
804
|
+
// fresh seq), and the record is marked abandoned so that siblings
|
|
805
|
+
// acking afterwards cannot complete it — one lost fragment forfeits the
|
|
806
|
+
// whole message. The mapping is reclaimed either way, otherwise
|
|
807
|
+
// lost-packet entries accumulate across the session until the seq value
|
|
808
|
+
// is reused on wrap.
|
|
809
|
+
//
|
|
810
|
+
// Every outstanding seq is resolved by Channel as exactly one of acked
|
|
811
|
+
// or lost — via the ack bitfield, the 33-seq window age-out, or the
|
|
812
|
+
// max_in_flight eviction — so no record can be stranded by a seq that
|
|
813
|
+
// never reports.
|
|
656
814
|
const on_lost = (seq) => {
|
|
657
|
-
|
|
815
|
+
const pending = seq_to_pending.get(seq);
|
|
816
|
+
if (pending === undefined) return;
|
|
817
|
+
seq_to_pending.delete(seq);
|
|
818
|
+
pending.unacked--;
|
|
819
|
+
pending.abandoned = true;
|
|
658
820
|
};
|
|
659
821
|
|
|
660
822
|
// Untrusted-input boundary. A structurally-malformed or truncated
|
|
@@ -688,7 +850,7 @@ export class NetworkPeer {
|
|
|
688
850
|
channel.onPacketLost.add(on_lost);
|
|
689
851
|
|
|
690
852
|
this.#peers.set(peer_id, {
|
|
691
|
-
channel, transport,
|
|
853
|
+
channel, transport, seq_to_pending, confirmed,
|
|
692
854
|
on_payload: payload_handler, on_acked, on_lost, on_nack,
|
|
693
855
|
fragment_assembler, fragment_retention,
|
|
694
856
|
reliable_pipeline,
|
|
@@ -764,6 +926,21 @@ export class NetworkPeer {
|
|
|
764
926
|
return conn === undefined ? 0 : conn.inbound_count;
|
|
765
927
|
}
|
|
766
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Number of outbound action-stream seqs for `peer_id` still awaiting an
|
|
931
|
+
* ack-or-lost verdict. Returns 0 for an unknown peer.
|
|
932
|
+
*
|
|
933
|
+
* Diagnostics: this is bounded by the channel's in-flight window and must
|
|
934
|
+
* fall back to 0 once a stream of acks has resolved everything sent. A
|
|
935
|
+
* value that only grows is a leak in the ack accounting.
|
|
936
|
+
* @param {number} peer_id
|
|
937
|
+
* @returns {number}
|
|
938
|
+
*/
|
|
939
|
+
pending_ack_count(peer_id) {
|
|
940
|
+
const conn = this.#peers.get(peer_id);
|
|
941
|
+
return conn === undefined ? 0 : conn.seq_to_pending.size;
|
|
942
|
+
}
|
|
943
|
+
|
|
767
944
|
/**
|
|
768
945
|
* Direct access to a peer's channel (for sending acks-only packets, raw payloads, etc.).
|
|
769
946
|
* @param {number} peer_id
|
|
@@ -797,8 +974,8 @@ export class NetworkPeer {
|
|
|
797
974
|
/**
|
|
798
975
|
* Close the current tick and send pending action records to every peer.
|
|
799
976
|
* For each peer, packs frames `[Baseline.last_acked(peer) + 1, current_frame]`
|
|
800
|
-
* and sends via the channel; records the
|
|
801
|
-
*
|
|
977
|
+
* and sends via the channel; records the seqs the send went out under so
|
|
978
|
+
* that acks for all of them advance the baseline.
|
|
802
979
|
*
|
|
803
980
|
* If a {@link MutationLedger} was supplied at construction, this tick's
|
|
804
981
|
* accumulated changed-entity set is also compacted into the ledger.
|
|
@@ -835,8 +1012,10 @@ export class NetworkPeer {
|
|
|
835
1012
|
* by the caller. Records the tick's accumulated mutations into the
|
|
836
1013
|
* MutationLedger (if configured), then packs frames
|
|
837
1014
|
* `[last_acked+1, current_frame]` for each peer via
|
|
838
|
-
* `Replicator.pack_for_peer
|
|
839
|
-
*
|
|
1015
|
+
* `Replicator.pack_for_peer` into up to {@link max_packets_per_tick}
|
|
1016
|
+
* channel packets, each the next slice of that range. Empty packs (no
|
|
1017
|
+
* in-scope actions) are not sent. Clears the per-tick changed-entity set
|
|
1018
|
+
* after recording.
|
|
840
1019
|
*
|
|
841
1020
|
* @param {number} current_frame
|
|
842
1021
|
*/
|
|
@@ -853,15 +1032,14 @@ export class NetworkPeer {
|
|
|
853
1032
|
|
|
854
1033
|
for (const [peer_id, conn] of this.#peers) {
|
|
855
1034
|
const last_acked = this.baseline.last_acked(peer_id);
|
|
856
|
-
// Pack-range start
|
|
1035
|
+
// Pack-range start: whichever is newer of "catch up since you
|
|
1036
|
+
// last confirmed receipt" and "the oldest frame the log still
|
|
1037
|
+
// holds."
|
|
857
1038
|
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
// For freshly-connected peers (no ack yet), we cover the
|
|
863
|
-
// action_log's full ring: max(0, current_frame -
|
|
864
|
-
// frame_capacity + 1). Two competing constraints:
|
|
1039
|
+
// last_acked+1 is the ordinary catch-up point. A freshly-
|
|
1040
|
+
// connected peer has no ack, so its -1+1 falls through to the
|
|
1041
|
+
// ring floor and it covers the log's full content. Two competing
|
|
1042
|
+
// constraints shaped that floor:
|
|
865
1043
|
//
|
|
866
1044
|
// - Don't pack `current_frame` only: under any non-zero
|
|
867
1045
|
// latency, A's actions tagged at frame X land on the
|
|
@@ -877,28 +1055,120 @@ export class NetworkPeer {
|
|
|
877
1055
|
//
|
|
878
1056
|
// `current_frame - frame_capacity + 1` is the oldest frame
|
|
879
1057
|
// the ring could still hold, so we naturally cap at the
|
|
880
|
-
// ring's actual content.
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1058
|
+
// ring's actual content.
|
|
1059
|
+
//
|
|
1060
|
+
// The floor binds acked peers too, not just fresh ones: a peer
|
|
1061
|
+
// that acked once and then stopped leaves last_acked pinned while
|
|
1062
|
+
// current_frame runs on, and without the floor the loop walks
|
|
1063
|
+
// every frame since that ack to emit at most a ring's worth —
|
|
1064
|
+
// work that grows without bound the longer the peer stays quiet.
|
|
1065
|
+
// Skipping those frames costs nothing, because they are already
|
|
1066
|
+
// gone from the log and can never be sent whatever the ack says;
|
|
1067
|
+
// `pack_for_peer` credits them as covered the moment it reaches
|
|
1068
|
+
// them anyway, so the frame this pack confirms is the same
|
|
1069
|
+
// either way.
|
|
1070
|
+
//
|
|
1071
|
+
// Actions referencing entities the peer's slot_table doesn't yet
|
|
1072
|
+
// know about silently no-op (Replicator translates via
|
|
1073
|
+
// network_for → -1; SimAction apply hits the `if (local < 0)
|
|
1074
|
+
// return` guard). For first-connect scenarios the app is still
|
|
1075
|
+
// expected to send a state-burst seed; this just makes the
|
|
1076
|
+
// action-stream bootstrap work in addition to that.
|
|
1077
|
+
const start = Math.max(
|
|
1078
|
+
last_acked + 1,
|
|
1079
|
+
current_frame - this.action_log.frame_capacity + 1,
|
|
1080
|
+
0,
|
|
1081
|
+
);
|
|
890
1082
|
if (start > current_frame) continue;
|
|
891
1083
|
|
|
892
|
-
|
|
893
|
-
//
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
//
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1084
|
+
// Up to `max_packets_per_tick` sends, each bounded to one channel
|
|
1085
|
+
// packet and each taking the next slice of the owed range.
|
|
1086
|
+
//
|
|
1087
|
+
// Bounding a send to one packet is load-bearing. A fragmented
|
|
1088
|
+
// action stream is confirmed only if EVERY fragment survives, so a
|
|
1089
|
+
// send that grows with the owed range is self-defeating under
|
|
1090
|
+
// loss: one lost fragment pins the baseline, which widens the next
|
|
1091
|
+
// pack, which raises the fragment count N, which drops
|
|
1092
|
+
// P(all arrive) = (1-p)^N geometrically — the range then widens
|
|
1093
|
+
// faster than it drains and delivery collapses. Split into whole
|
|
1094
|
+
// packets instead, each slice is confirmed on its own and a loss
|
|
1095
|
+
// forfeits one slice, not the tick.
|
|
1096
|
+
//
|
|
1097
|
+
// Sending more than one is what keeps the stream up with the
|
|
1098
|
+
// simulation. A slice is re-sent every tick until its ack returns,
|
|
1099
|
+
// so the baseline moves by at most a tick's worth of slices per
|
|
1100
|
+
// round trip, while the sim produces a frame per tick. One packet
|
|
1101
|
+
// a tick therefore keeps up only while a round trip's frames fit
|
|
1102
|
+
// one packet; past that the owed range grows until it meets the
|
|
1103
|
+
// ring floor, a ring of frames is lost crossing it, and everything
|
|
1104
|
+
// after arrives a ring late — on a link that loses nothing. The
|
|
1105
|
+
// pack is oldest-owed-first, which is what the range order already
|
|
1106
|
+
// gives.
|
|
1107
|
+
//
|
|
1108
|
+
// The budget is what is left of one payload after the prefix and
|
|
1109
|
+
// the slice header, read from the buffer rather than written as a
|
|
1110
|
+
// constant: `pack_for_peer` measures its budget from the current
|
|
1111
|
+
// position, so a hard-coded allowance would have to be edited in
|
|
1112
|
+
// step with every byte added here, and getting that wrong puts
|
|
1113
|
+
// each full pack one byte over the limit and quietly fragments it
|
|
1114
|
+
// again.
|
|
1115
|
+
let from = start;
|
|
1116
|
+
for (let n = 0; n < this.max_packets_per_tick && from <= current_frame; n++) {
|
|
1117
|
+
this.#send_buffer.position = 0;
|
|
1118
|
+
// Packet-type prefix, then the slice header: the range this
|
|
1119
|
+
// packet covers and whether it is the tick's first, which is
|
|
1120
|
+
// what lets the receiver apply slices in frame order (see
|
|
1121
|
+
// Replicator.unpack_from_peer). frame_end is back-patched once
|
|
1122
|
+
// the pack has decided how far it got.
|
|
1123
|
+
this.#send_buffer.writeUint8(NetworkPacketType.ACTION_STREAM);
|
|
1124
|
+
this.#send_buffer.writeUint32(from);
|
|
1125
|
+
const frame_end_position = this.#send_buffer.position;
|
|
1126
|
+
this.#send_buffer.writeUint32(0);
|
|
1127
|
+
this.#send_buffer.writeUint8(n === 0 ? 1 : 0);
|
|
1128
|
+
const packed_through = this.replicator.pack_for_peer(
|
|
1129
|
+
peer_id, from, current_frame, this.#send_buffer,
|
|
1130
|
+
MAX_CHANNEL_PAYLOAD_BYTES - this.#send_buffer.position,
|
|
1131
|
+
);
|
|
1132
|
+
const length = this.#send_buffer.position;
|
|
1133
|
+
// Just the prefix and header → nothing in scope left to send
|
|
1134
|
+
// this peer.
|
|
1135
|
+
if (length === 1 + ACTION_STREAM_SLICE_HEADER_BYTES) break;
|
|
1136
|
+
this.#send_buffer.position = frame_end_position;
|
|
1137
|
+
this.#send_buffer.writeUint32(packed_through);
|
|
1138
|
+
this.#send_buffer.position = length;
|
|
1139
|
+
|
|
1140
|
+
this.#send_to_peer(conn, this.#send_buffer.raw_bytes, length, this.#sent_seqs);
|
|
1141
|
+
|
|
1142
|
+
// One record shared by every seq the send produced. Frames
|
|
1143
|
+
// [from, packed_through] are confirmed only when all of those
|
|
1144
|
+
// seqs ack; a loss on any of them abandons the record and
|
|
1145
|
+
// leaves the range owed, so a later tick's pack picks it up
|
|
1146
|
+
// again.
|
|
1147
|
+
//
|
|
1148
|
+
// frame_end is what the pack actually held, NOT current_frame:
|
|
1149
|
+
// the budget may have stopped it short, and crediting frames
|
|
1150
|
+
// that were left out of the packet would retire them from the
|
|
1151
|
+
// back-fill unsent — the same defect as crediting a whole
|
|
1152
|
+
// message to its first fragment, one layer up.
|
|
1153
|
+
//
|
|
1154
|
+
// Only the first slice starts at the pack start. The rest start
|
|
1155
|
+
// where the previous slice stopped, and their confirmation is
|
|
1156
|
+
// held until everything before them is confirmed too — see
|
|
1157
|
+
// __credit_confirmed.
|
|
1158
|
+
const seq_count = this.#sent_seqs.length;
|
|
1159
|
+
const pending = {
|
|
1160
|
+
frame_start: from,
|
|
1161
|
+
frame_end: packed_through,
|
|
1162
|
+
head: n === 0,
|
|
1163
|
+
unacked: seq_count,
|
|
1164
|
+
abandoned: false,
|
|
1165
|
+
};
|
|
1166
|
+
for (let i = 0; i < seq_count; i++) {
|
|
1167
|
+
conn.seq_to_pending.set(this.#sent_seqs[i], pending);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
from = packed_through + 1;
|
|
1171
|
+
}
|
|
902
1172
|
}
|
|
903
1173
|
|
|
904
1174
|
// Reset the per-tick changed-entity set so the next tick starts clean.
|
|
@@ -906,28 +1176,84 @@ export class NetworkPeer {
|
|
|
906
1176
|
if (this.#changed_entities !== null) this.#changed_entities.clear();
|
|
907
1177
|
}
|
|
908
1178
|
|
|
1179
|
+
/**
|
|
1180
|
+
* A send has been confirmed in full — every seq it went out under acked,
|
|
1181
|
+
* none lost — so the receiver has applied every frame it carried. Record
|
|
1182
|
+
* those frames, then advance the peer's baseline as far as the frames
|
|
1183
|
+
* confirmed so far run contiguously.
|
|
1184
|
+
*
|
|
1185
|
+
* The order matters because a tick may send several slices and they
|
|
1186
|
+
* confirm independently. A later slice landing while an earlier one is
|
|
1187
|
+
* still in flight must not credit the earlier one's frames: they are
|
|
1188
|
+
* still owed, and crediting them would retire them from the back-fill
|
|
1189
|
+
* unsent. So a slice is credited only once the baseline has reached the
|
|
1190
|
+
* frame before it. The exception is the head of a tick's burst. Its start
|
|
1191
|
+
* IS the pack start — `last_acked + 1` or the ring floor, whichever is
|
|
1192
|
+
* newer — so nothing sendable lies before it and its confirmation credits
|
|
1193
|
+
* outright. That is also how the baseline gets past frames the ring has
|
|
1194
|
+
* already retired, which no ack could ever confirm.
|
|
1195
|
+
*
|
|
1196
|
+
* `confirmed` is a ring indexed by frame modulo capacity that stores the
|
|
1197
|
+
* frame number, so a slot whose frame has since been overwritten reads as
|
|
1198
|
+
* unconfirmed rather than as its successor (the {@link FrameBufferRing}
|
|
1199
|
+
* trick). A mark can only be overwritten by a frame a full ring later,
|
|
1200
|
+
* and a frame that far behind is below the ring floor — no longer owed,
|
|
1201
|
+
* and no longer needed to advance the baseline, which a head crosses it
|
|
1202
|
+
* with.
|
|
1203
|
+
*
|
|
1204
|
+
* @param {number} peer_id
|
|
1205
|
+
* @param {Int32Array} confirmed the peer's confirmed-frame ring
|
|
1206
|
+
* @param {PendingAck} pending the record that just drained
|
|
1207
|
+
* @private
|
|
1208
|
+
*/
|
|
1209
|
+
#credit_confirmed(peer_id, confirmed, pending) {
|
|
1210
|
+
const capacity = confirmed.length;
|
|
1211
|
+
for (let f = pending.frame_start; f <= pending.frame_end; f++) {
|
|
1212
|
+
confirmed[f % capacity] = f;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const before = this.baseline.last_acked(peer_id);
|
|
1216
|
+
let last_acked = before;
|
|
1217
|
+
if (pending.head && pending.frame_end > last_acked) {
|
|
1218
|
+
last_acked = pending.frame_end;
|
|
1219
|
+
}
|
|
1220
|
+
while (confirmed[(last_acked + 1) % capacity] === last_acked + 1) {
|
|
1221
|
+
last_acked++;
|
|
1222
|
+
}
|
|
1223
|
+
if (last_acked > before) {
|
|
1224
|
+
this.baseline.set_acked(peer_id, last_acked);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
909
1228
|
/**
|
|
910
1229
|
* Send a logical payload to a connected peer, fragmenting transparently
|
|
911
1230
|
* if it exceeds the channel's single-packet capacity.
|
|
912
1231
|
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
* back-fill
|
|
920
|
-
*
|
|
1232
|
+
* Fragments either all arrive (and the receiver reassembles) or the
|
|
1233
|
+
* message is lost — there is no partial delivery, and the receiver never
|
|
1234
|
+
* applies a message it could not reassemble.
|
|
1235
|
+
*
|
|
1236
|
+
* A caller that ties delivery state to the ack must therefore wait on
|
|
1237
|
+
* EVERY seq the send produced, not just the first. The action stream's
|
|
1238
|
+
* back-fill does recover a lost message, but only for as long as the
|
|
1239
|
+
* frames stay owed: crediting the range to the first fragment's ack
|
|
1240
|
+
* retires it from the back-fill, and a message whose later fragments were
|
|
1241
|
+
* dropped is then never re-sent and never applied. Pass `seqs_out` to
|
|
1242
|
+
* collect the seqs; {@link NetworkPeer#flush_outbound} does exactly that.
|
|
921
1243
|
*
|
|
922
1244
|
* @param {object} conn entry from __peers
|
|
923
1245
|
* @param {Uint8Array} payload starts with the packet-type byte
|
|
924
1246
|
* @param {number} length
|
|
925
|
-
* @
|
|
1247
|
+
* @param {number[]} [seqs_out] when given, cleared and then filled with the
|
|
1248
|
+
* seq of every transport packet emitted, in send order
|
|
926
1249
|
* @private
|
|
927
1250
|
*/
|
|
928
|
-
#send_to_peer(conn, payload, length) {
|
|
1251
|
+
#send_to_peer(conn, payload, length, seqs_out = null) {
|
|
1252
|
+
if (seqs_out !== null) seqs_out.length = 0;
|
|
929
1253
|
if (length <= MAX_CHANNEL_PAYLOAD_BYTES) {
|
|
930
|
-
|
|
1254
|
+
const seq = conn.channel.send(payload, length);
|
|
1255
|
+
if (seqs_out !== null) seqs_out.push(seq);
|
|
1256
|
+
return;
|
|
931
1257
|
}
|
|
932
1258
|
const message_id = this.#next_fragment_message_id;
|
|
933
1259
|
this.#next_fragment_message_id = seq16_advance(this.#next_fragment_message_id);
|
|
@@ -935,16 +1261,14 @@ export class NetworkPeer {
|
|
|
935
1261
|
// Copy happens inside retain(); `payload` is the orchestrator's
|
|
936
1262
|
// shared send buffer and will be overwritten by the next call.
|
|
937
1263
|
conn.fragment_retention.retain(message_id, payload, length, performance.now());
|
|
938
|
-
let first_seq = -1;
|
|
939
1264
|
send_fragmented(
|
|
940
1265
|
payload, length, message_id,
|
|
941
1266
|
NetworkPacketType.FRAGMENT, this.#fragment_scratch,
|
|
942
1267
|
(chunk_bytes, chunk_length) => {
|
|
943
1268
|
const seq = conn.channel.send(chunk_bytes, chunk_length);
|
|
944
|
-
if (
|
|
1269
|
+
if (seqs_out !== null) seqs_out.push(seq);
|
|
945
1270
|
},
|
|
946
1271
|
);
|
|
947
|
-
return first_seq;
|
|
948
1272
|
}
|
|
949
1273
|
|
|
950
1274
|
/**
|