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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-realtime",
3
- "version": "0.6.0-next.75",
3
+ "version": "0.6.0-next.78",
4
4
  "publishConfig": {
5
5
  "tag": "next"
6
6
  },
@@ -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 from `catalogByKey`
1053
- * (first-sight catch-up), and a subscriber's own entity is never caught up to itself
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 {Map<string, any>} catalogByKey full per-entity state, for catch-up
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, catalogByKey, moved, t) {
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 = catalogByKey.get(key);
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
- _smoothSendTo(rec, ws, 'update', { key, data: state });
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
- _smoothPublish(rec, 'update', { key: u.key, data: u.state }, localAuthor);
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 (above, or
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
- _smoothPublish(rec, 'update', { key: u.key, data: u.state }, rec.noEcho && u.commanded ? u.ws : undefined);
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 `catalogByKey`. Only a
1326
- // remote frame advances the lag-comp send-cadence estimator (the client discards
1327
- // its own entity frame before measuring its interpolation delay).
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, catalogByKey, moved, t);
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];