svelte-realtime 0.6.0-next.75 → 0.6.0-next.78
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/server/smooth.js +80 -16
package/package.json
CHANGED
package/src/server/smooth.js
CHANGED
|
@@ -712,6 +712,53 @@ function _smoothSendTo(rec, ws, event, data) {
|
|
|
712
712
|
}
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Broadcast one tick's update frames. On a platform with the batched wire
|
|
717
|
+
* fan-out, every update travels in ONE frame per capable viewer (per-entry
|
|
718
|
+
* author exclusion and the per-entry JSON envelopes handled inside the
|
|
719
|
+
* platform walk); an older platform, a single update, or a codec-less record
|
|
720
|
+
* take the per-entity path unchanged. Entries: { key, state, excludeWs? }.
|
|
721
|
+
*/
|
|
722
|
+
function _smoothPublishUpdates(rec, list) {
|
|
723
|
+
if (list.length === 0) return;
|
|
724
|
+
const platform = rec.platform;
|
|
725
|
+
if (rec.codec && list.length > 1 && typeof platform.publishWireBatch === 'function') {
|
|
726
|
+
const entries = new Array(list.length);
|
|
727
|
+
for (let i = 0; i < list.length; i++) {
|
|
728
|
+
entries[i] = {
|
|
729
|
+
data: _smoothWireFrame(rec, 'update', { key: list[i].key, data: list[i].state }),
|
|
730
|
+
excludeWs: list[i].excludeWs
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
platform.publishWireBatch(rec.wireTopic, 'update', entries, rec.codec);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
for (let i = 0; i < list.length; i++) {
|
|
737
|
+
_smoothPublish(rec, 'update', { key: list[i].key, data: list[i].state }, list[i].excludeWs);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Deliver one subscriber's culled update set. On a platform with the batched
|
|
743
|
+
* single-target wire send, the whole set is one frame; otherwise the
|
|
744
|
+
* per-entity sends this call replaces. Entries: { key, state }.
|
|
745
|
+
*/
|
|
746
|
+
function _smoothSendUpdatesTo(rec, ws, list) {
|
|
747
|
+
if (list.length === 0) return;
|
|
748
|
+
const platform = rec.platform;
|
|
749
|
+
if (rec.codec && list.length > 1 && typeof platform.sendWireBatch === 'function') {
|
|
750
|
+
const entries = new Array(list.length);
|
|
751
|
+
for (let i = 0; i < list.length; i++) {
|
|
752
|
+
entries[i] = { data: _smoothWireFrame(rec, 'update', { key: list[i].key, data: list[i].state }) };
|
|
753
|
+
}
|
|
754
|
+
platform.sendWireBatch(ws, rec.wireTopic, 'update', entries, rec.codec);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
for (let i = 0; i < list.length; i++) {
|
|
758
|
+
_smoothSendTo(rec, ws, 'update', { key: list[i].key, data: list[i].state });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
715
762
|
function _armSmoothTick(rec) {
|
|
716
763
|
if (rec.timer !== null) return;
|
|
717
764
|
rec.timer = setTimer(() => {
|
|
@@ -1049,21 +1096,25 @@ function _smoothJoinSnapshot(rec, identity) {
|
|
|
1049
1096
|
* `moved` carries the entities that changed this tick, each with the author identity
|
|
1050
1097
|
* to suppress for that frame (echo suppression): on the owner that is the commanded
|
|
1051
1098
|
* author under noEcho, on the non-owner it is the exclude identity the owner stamped
|
|
1052
|
-
* onto the relay. An entity in range that did NOT move is delivered
|
|
1053
|
-
* (first-sight catch-up
|
|
1099
|
+
* onto the relay. An entity in range that did NOT move is delivered via `lookup`
|
|
1100
|
+
* (first-sight catch-up: the owner reads the authority, the non-owner its shadow),
|
|
1101
|
+
* and a subscriber's own entity is never caught up to itself
|
|
1054
1102
|
* under noEcho. Only a remote frame advances the send-cadence estimator (a client
|
|
1055
1103
|
* discards its own entity frame before measuring its interpolation delay).
|
|
1056
1104
|
* @param {any} rec
|
|
1057
1105
|
* @param {Map<string, Set<string>>} relevancy identity -> entity keys to deliver
|
|
1058
|
-
* @param {
|
|
1106
|
+
* @param {(key: string) => any} lookup current per-entity state, for catch-up
|
|
1059
1107
|
* @param {Map<string, { state: any, exclude: string | undefined }>} moved entities that changed this tick
|
|
1060
1108
|
* @param {number} t wall stamp for the send-cadence estimator (hitTest only)
|
|
1061
1109
|
*/
|
|
1062
|
-
function _smoothDeliverCulled(rec, relevancy,
|
|
1110
|
+
function _smoothDeliverCulled(rec, relevancy, lookup, moved, t) {
|
|
1063
1111
|
for (const [identity, ws] of rec.registry) {
|
|
1064
1112
|
const relSet = relevancy.get(identity);
|
|
1065
1113
|
if (relSet === undefined) continue;
|
|
1066
1114
|
let delivered = false;
|
|
1115
|
+
// This subscriber's whole culled set collects and flushes as ONE
|
|
1116
|
+
// batched frame (per-entity on an older platform).
|
|
1117
|
+
const toSend = [];
|
|
1067
1118
|
for (const key of relSet) {
|
|
1068
1119
|
const m = moved.get(key);
|
|
1069
1120
|
let state;
|
|
@@ -1073,15 +1124,16 @@ function _smoothDeliverCulled(rec, relevancy, catalogByKey, moved, t) {
|
|
|
1073
1124
|
exclude = m.exclude;
|
|
1074
1125
|
} else {
|
|
1075
1126
|
// First-sight catch-up: in range, no update of its own this tick.
|
|
1076
|
-
state =
|
|
1127
|
+
state = lookup(key);
|
|
1077
1128
|
exclude = rec.noEcho ? key : undefined;
|
|
1078
1129
|
}
|
|
1079
1130
|
if (exclude !== undefined && identity === exclude) continue;
|
|
1080
1131
|
if (state !== undefined) {
|
|
1081
|
-
|
|
1132
|
+
toSend.push({ key, state });
|
|
1082
1133
|
if (key !== identity) delivered = true;
|
|
1083
1134
|
}
|
|
1084
1135
|
}
|
|
1136
|
+
_smoothSendUpdatesTo(rec, ws, toSend);
|
|
1085
1137
|
// The cadence seed is the WIRE interval (tickMs widened by the broadcast
|
|
1086
1138
|
// gate), not the sim interval - the estimator's cold-start guess must
|
|
1087
1139
|
// match what a subscriber actually receives.
|
|
@@ -1118,7 +1170,7 @@ function _smoothCullTick(rec) {
|
|
|
1118
1170
|
const catalog = [];
|
|
1119
1171
|
for (const [key, state] of rec.shadow) catalog.push({ key, state });
|
|
1120
1172
|
const relevancy = rec.interest.compute(catalog, rec.registry.keys(), rec.interestTick++, rec.budgetOf ?? undefined);
|
|
1121
|
-
_smoothDeliverCulled(rec, relevancy, rec.shadow, rec.pendingRelay, t);
|
|
1173
|
+
_smoothDeliverCulled(rec, relevancy, (key) => rec.shadow.get(key), rec.pendingRelay, t);
|
|
1122
1174
|
rec.pendingRelay.clear();
|
|
1123
1175
|
rec.shadowDirty = false;
|
|
1124
1176
|
rec.interestDirty = false;
|
|
@@ -1256,6 +1308,11 @@ function _smoothTick(rec) {
|
|
|
1256
1308
|
if (!idle || (worldRun !== null && worldRun.dirty)) rec.lagCompLastActive = t;
|
|
1257
1309
|
}
|
|
1258
1310
|
rec.interestDirty = false;
|
|
1311
|
+
// Broadcast updates collect here and flush as ONE batched fan-out after the
|
|
1312
|
+
// loop (per-entry author exclusion rides along); the relay and the cells
|
|
1313
|
+
// routing stay per-entity inside the loop. Allocated on first use: the
|
|
1314
|
+
// interest and cells paths never push (their delivery runs elsewhere).
|
|
1315
|
+
let broadcastUpdates = null;
|
|
1259
1316
|
for (let i = 0; i < wireUpdates.length; i++) {
|
|
1260
1317
|
const u = wireUpdates[i];
|
|
1261
1318
|
// Cells mode: route each changed entity to its cell topic (native shared
|
|
@@ -1288,10 +1345,10 @@ function _smoothTick(rec) {
|
|
|
1288
1345
|
// own subscribers all see the move).
|
|
1289
1346
|
if (!relevancy) {
|
|
1290
1347
|
const localAuthor = rec.noEcho && u.commanded ? rec.registry.get(u.key) : undefined;
|
|
1291
|
-
|
|
1348
|
+
(broadcastUpdates ??= []).push({ key: u.key, state: u.state, excludeWs: localAuthor });
|
|
1292
1349
|
}
|
|
1293
1350
|
// Relay to other instances. Only a remote (surrogate) author needs a
|
|
1294
|
-
// cross-instance exclude - a local author is already excluded (
|
|
1351
|
+
// cross-instance exclude - a local author is already excluded (below, or
|
|
1295
1352
|
// by the relevancy walk's own-update suppression) and is on no other
|
|
1296
1353
|
// instance. The entity key IS the author identity.
|
|
1297
1354
|
const relayExclude = rec.noEcho && u.commanded && _isSmoothSurrogate(u.ws) ? u.key : undefined;
|
|
@@ -1302,9 +1359,13 @@ function _smoothTick(rec) {
|
|
|
1302
1359
|
// Interest-on: updates fan out per subscriber after this loop (the
|
|
1303
1360
|
// relevancy walk below), so nothing broadcasts here.
|
|
1304
1361
|
} else {
|
|
1305
|
-
|
|
1362
|
+
(broadcastUpdates ??= []).push({ key: u.key, state: u.state, excludeWs: rec.noEcho && u.commanded ? u.ws : undefined });
|
|
1306
1363
|
}
|
|
1307
1364
|
}
|
|
1365
|
+
// One tick, one fan-out: every collected update in a single batched frame
|
|
1366
|
+
// per capable viewer (per-entity on an older platform). Acks, events, and
|
|
1367
|
+
// removals keep their own frames below, exactly as before.
|
|
1368
|
+
if (broadcastUpdates !== null) _smoothPublishUpdates(rec, broadcastUpdates);
|
|
1308
1369
|
// Interest-on: deliver each LOCAL subscriber (single-instance, or the owner
|
|
1309
1370
|
// side of a cluster) the CURRENT state of every entity the relevancy pass marked
|
|
1310
1371
|
// for it this tick - the entities inside its area of interest that changed since
|
|
@@ -1318,19 +1379,22 @@ function _smoothTick(rec) {
|
|
|
1318
1379
|
// the cursor viewport cull does. Events and removals stay on the shared broadcast
|
|
1319
1380
|
// path (over-deliver rather than risk a ghost or a dropped one-shot).
|
|
1320
1381
|
if (relevancy) {
|
|
1321
|
-
const catalogByKey = new Map();
|
|
1322
|
-
for (let i = 0; i < catalog.length; i++) catalogByKey.set(catalog[i].key, catalog[i].state);
|
|
1323
1382
|
// `moved` carries each entity that changed this tick with the author to suppress
|
|
1324
1383
|
// for that frame (the commanded author under noEcho). The shared delivery helper
|
|
1325
|
-
// catches up in-range entities that did not move from
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1384
|
+
// catches up in-range entities that did not move straight from the authority
|
|
1385
|
+
// (`get` is O(1) and hands back the same state object the catalog carried -
|
|
1386
|
+
// nothing mutates entries between the post-drain snapshot and this delivery).
|
|
1387
|
+
// Only a remote frame advances the lag-comp send-cadence estimator (the client
|
|
1388
|
+
// discards its own entity frame before measuring its interpolation delay).
|
|
1328
1389
|
const moved = new Map();
|
|
1329
1390
|
for (let i = 0; i < wireUpdates.length; i++) {
|
|
1330
1391
|
const u = wireUpdates[i];
|
|
1331
1392
|
moved.set(u.key, { state: u.state, exclude: rec.noEcho && u.commanded ? u.key : undefined });
|
|
1332
1393
|
}
|
|
1333
|
-
_smoothDeliverCulled(rec, relevancy,
|
|
1394
|
+
_smoothDeliverCulled(rec, relevancy, (key) => {
|
|
1395
|
+
const e = rec.authority.get(key);
|
|
1396
|
+
return e === undefined ? undefined : e.state;
|
|
1397
|
+
}, moved, t);
|
|
1334
1398
|
}
|
|
1335
1399
|
for (let i = 0; i < acks.length; i++) {
|
|
1336
1400
|
const a = acks[i];
|