styly-netsync-server 0.13.0__tar.gz → 0.14.0__tar.gz
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.
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/PKG-INFO +1 -1
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/pyproject.toml +1 -1
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/binary_serializer.py +8 -31
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/client.py +0 -3
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/client_simulator.py +0 -1
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/server.py +64 -59
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/PKG-INFO +1 -1
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/SOURCES.txt +1 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_binary_serializer.py +7 -7
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_client_variable_device_store.py +9 -11
- styly_netsync_server-0.14.0/tests/test_nv_server_ordering.py +147 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_object_sync.py +1 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/LICENSE +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/README.md +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/setup.cfg +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/__init__.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/__main__.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/adapters.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/cli.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/config.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/default.toml +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/events.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/logging_utils.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/network_utils.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/nv_sync.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/rest_bridge.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/types.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/dependency_links.txt +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/entry_points.txt +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/requires.txt +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/top_level.txt +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_all_run_methods.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_config.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_discovery_probe.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_logging_cli.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_multi_nic.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_nv_protocol.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_port_error_message.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_python_client.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_reconnect_identity.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_rest_bridge.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_room_expiry.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_stealth_heartbeat.py +0 -0
- {styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_timing_monotonic.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: styly-netsync-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: STYLY NetSync Server - Multiplayer framework for Location-Based Entertainment VR/MR experiences
|
|
5
5
|
Author-email: "STYLY, Inc." <info@styly.inc>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "styly-netsync-server"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.14.0"
|
|
8
8
|
description = "STYLY NetSync Server - Multiplayer framework for Location-Based Entertainment VR/MR experiences"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
{styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/binary_serializer.py
RENAMED
|
@@ -6,7 +6,10 @@ from typing import Any
|
|
|
6
6
|
logger = logging.getLogger(__name__)
|
|
7
7
|
|
|
8
8
|
# Message type identifiers
|
|
9
|
-
|
|
9
|
+
# v6: Network Variable wire messages dropped the per-write timestamp field.
|
|
10
|
+
# Bumped so mixed old/new builds fail the handshake instead of silently
|
|
11
|
+
# misparsing NV traffic (the version byte rides on transform/object messages).
|
|
12
|
+
PROTOCOL_VERSION = 6
|
|
10
13
|
MSG_CLIENT_TRANSFORM = 1
|
|
11
14
|
MSG_ROOM_TRANSFORM = 2 # Legacy room transform with short IDs only
|
|
12
15
|
MSG_RPC = 3 # Remote procedure call
|
|
@@ -686,7 +689,7 @@ def serialize_global_var_set(data: dict[str, Any]) -> bytes:
|
|
|
686
689
|
"""Serialize global variable set message
|
|
687
690
|
|
|
688
691
|
Args:
|
|
689
|
-
data: Dictionary with senderClientNo, variableName, variableValue
|
|
692
|
+
data: Dictionary with senderClientNo, variableName, variableValue
|
|
690
693
|
"""
|
|
691
694
|
buffer = bytearray()
|
|
692
695
|
|
|
@@ -704,9 +707,6 @@ def serialize_global_var_set(data: dict[str, Any]) -> bytes:
|
|
|
704
707
|
value = data.get("variableValue", "")[:1024]
|
|
705
708
|
_pack_string(buffer, value, use_ushort=True)
|
|
706
709
|
|
|
707
|
-
# Timestamp (8 bytes double)
|
|
708
|
-
buffer.extend(struct.pack("<d", data.get("timestamp", 0.0)))
|
|
709
|
-
|
|
710
710
|
return bytes(buffer)
|
|
711
711
|
|
|
712
712
|
|
|
@@ -729,7 +729,6 @@ def serialize_global_var_sync(data: dict[str, Any]) -> bytes:
|
|
|
729
729
|
for var in variables:
|
|
730
730
|
_pack_string(buffer, var.get("name", "")[:64])
|
|
731
731
|
_pack_string(buffer, var.get("value", "")[:1024], use_ushort=True)
|
|
732
|
-
buffer.extend(struct.pack("<d", var.get("timestamp", 0.0)))
|
|
733
732
|
buffer.extend(struct.pack("<H", var.get("lastWriterClientNo", 0)))
|
|
734
733
|
|
|
735
734
|
return bytes(buffer)
|
|
@@ -739,7 +738,8 @@ def serialize_client_var_set(data: dict[str, Any]) -> bytes:
|
|
|
739
738
|
"""Serialize client variable set message
|
|
740
739
|
|
|
741
740
|
Args:
|
|
742
|
-
data: Dictionary with senderClientNo, targetClientNo, variableName,
|
|
741
|
+
data: Dictionary with senderClientNo, targetClientNo, variableName,
|
|
742
|
+
variableValue
|
|
743
743
|
"""
|
|
744
744
|
buffer = bytearray()
|
|
745
745
|
|
|
@@ -760,9 +760,6 @@ def serialize_client_var_set(data: dict[str, Any]) -> bytes:
|
|
|
760
760
|
value = data.get("variableValue", "")[:1024]
|
|
761
761
|
_pack_string(buffer, value, use_ushort=True)
|
|
762
762
|
|
|
763
|
-
# Timestamp (8 bytes double)
|
|
764
|
-
buffer.extend(struct.pack("<d", data.get("timestamp", 0.0)))
|
|
765
|
-
|
|
766
763
|
return bytes(buffer)
|
|
767
764
|
|
|
768
765
|
|
|
@@ -770,7 +767,7 @@ def serialize_client_var_clear(data: dict[str, Any]) -> bytes:
|
|
|
770
767
|
"""Serialize client variable clear message.
|
|
771
768
|
|
|
772
769
|
Args:
|
|
773
|
-
data: Dictionary with senderClientNo
|
|
770
|
+
data: Dictionary with senderClientNo.
|
|
774
771
|
"""
|
|
775
772
|
buffer = bytearray()
|
|
776
773
|
|
|
@@ -780,9 +777,6 @@ def serialize_client_var_clear(data: dict[str, Any]) -> bytes:
|
|
|
780
777
|
# Sender client number (2 bytes)
|
|
781
778
|
buffer.extend(struct.pack("<H", data.get("senderClientNo", 0)))
|
|
782
779
|
|
|
783
|
-
# Timestamp (8 bytes double)
|
|
784
|
-
buffer.extend(struct.pack("<d", data.get("timestamp", 0.0)))
|
|
785
|
-
|
|
786
780
|
return bytes(buffer)
|
|
787
781
|
|
|
788
782
|
|
|
@@ -811,7 +805,6 @@ def serialize_client_var_sync(data: dict[str, Any]) -> bytes:
|
|
|
811
805
|
for var in variables:
|
|
812
806
|
_pack_string(buffer, var.get("name", "")[:64])
|
|
813
807
|
_pack_string(buffer, var.get("value", "")[:1024], use_ushort=True)
|
|
814
|
-
buffer.extend(struct.pack("<d", var.get("timestamp", 0.0)))
|
|
815
808
|
buffer.extend(struct.pack("<H", var.get("lastWriterClientNo", 0)))
|
|
816
809
|
|
|
817
810
|
return bytes(buffer)
|
|
@@ -1247,10 +1240,6 @@ def _deserialize_global_var_set(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1247
1240
|
# Variable value
|
|
1248
1241
|
result["variableValue"], offset = _unpack_string(data, offset, use_ushort=True)
|
|
1249
1242
|
|
|
1250
|
-
# Timestamp (8 bytes double)
|
|
1251
|
-
result["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1252
|
-
offset += 8
|
|
1253
|
-
|
|
1254
1243
|
return result
|
|
1255
1244
|
|
|
1256
1245
|
|
|
@@ -1267,8 +1256,6 @@ def _deserialize_global_var_sync(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1267
1256
|
var = {}
|
|
1268
1257
|
var["name"], offset = _unpack_string(data, offset)
|
|
1269
1258
|
var["value"], offset = _unpack_string(data, offset, use_ushort=True)
|
|
1270
|
-
var["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1271
|
-
offset += 8
|
|
1272
1259
|
var["lastWriterClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1273
1260
|
offset += 2
|
|
1274
1261
|
result["variables"].append(var)
|
|
@@ -1294,10 +1281,6 @@ def _deserialize_client_var_set(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1294
1281
|
# Variable value
|
|
1295
1282
|
result["variableValue"], offset = _unpack_string(data, offset, use_ushort=True)
|
|
1296
1283
|
|
|
1297
|
-
# Timestamp (8 bytes double)
|
|
1298
|
-
result["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1299
|
-
offset += 8
|
|
1300
|
-
|
|
1301
1284
|
return result
|
|
1302
1285
|
|
|
1303
1286
|
|
|
@@ -1307,10 +1290,6 @@ def _deserialize_client_var_clear(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1307
1290
|
|
|
1308
1291
|
# Sender client number (2 bytes)
|
|
1309
1292
|
result["senderClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1310
|
-
offset += 2
|
|
1311
|
-
|
|
1312
|
-
# Timestamp (8 bytes double)
|
|
1313
|
-
result["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1314
1293
|
|
|
1315
1294
|
return result
|
|
1316
1295
|
|
|
@@ -1336,8 +1315,6 @@ def _deserialize_client_var_sync(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1336
1315
|
var = {}
|
|
1337
1316
|
var["name"], offset = _unpack_string(data, offset)
|
|
1338
1317
|
var["value"], offset = _unpack_string(data, offset, use_ushort=True)
|
|
1339
|
-
var["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1340
|
-
offset += 8
|
|
1341
1318
|
var["lastWriterClientNo"] = struct.unpack("<H", data[offset : offset + 2])[
|
|
1342
1319
|
0
|
|
1343
1320
|
]
|
|
@@ -1222,7 +1222,6 @@ class net_sync_manager:
|
|
|
1222
1222
|
"senderClientNo": self._client_no,
|
|
1223
1223
|
"variableName": name,
|
|
1224
1224
|
"variableValue": value,
|
|
1225
|
-
"timestamp": time.time(),
|
|
1226
1225
|
}
|
|
1227
1226
|
message = binary_serializer.serialize_global_var_set(var_data)
|
|
1228
1227
|
return self._enqueue_control(
|
|
@@ -1244,7 +1243,6 @@ class net_sync_manager:
|
|
|
1244
1243
|
"targetClientNo": target_client_no,
|
|
1245
1244
|
"variableName": name,
|
|
1246
1245
|
"variableValue": value,
|
|
1247
|
-
"timestamp": time.time(),
|
|
1248
1246
|
}
|
|
1249
1247
|
message = binary_serializer.serialize_client_var_set(var_data)
|
|
1250
1248
|
return self._enqueue_control(
|
|
@@ -1263,7 +1261,6 @@ class net_sync_manager:
|
|
|
1263
1261
|
try:
|
|
1264
1262
|
clear_data = {
|
|
1265
1263
|
"senderClientNo": self._client_no,
|
|
1266
|
-
"timestamp": time.time(),
|
|
1267
1264
|
}
|
|
1268
1265
|
message = binary_serializer.serialize_client_var_clear(clear_data)
|
|
1269
1266
|
sent = self._enqueue_control(
|
{styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/client_simulator.py
RENAMED
|
@@ -648,7 +648,6 @@ class NetworkTransport:
|
|
|
648
648
|
"targetClientNo": sender_client_no, # Set variable for ourselves
|
|
649
649
|
"variableName": var_name,
|
|
650
650
|
"variableValue": value,
|
|
651
|
-
"timestamp": time.time(),
|
|
652
651
|
}
|
|
653
652
|
binary_data = serialize_client_var_set(var_data)
|
|
654
653
|
self.socket.send_multipart(
|
|
@@ -309,18 +309,24 @@ class NetSyncServer:
|
|
|
309
309
|
# Network Variables storage
|
|
310
310
|
self.global_variables: dict[str, dict[str, Any]] = (
|
|
311
311
|
{}
|
|
312
|
-
) # room_id -> {var_name: {value,
|
|
312
|
+
) # room_id -> {var_name: {value, version, lastWriterClientNo}}
|
|
313
313
|
self.client_variables: dict[str, dict[str, dict[str, Any]]] = (
|
|
314
314
|
{}
|
|
315
|
-
) # room_id -> {device_id -> {var_name: {value,
|
|
315
|
+
) # room_id -> {device_id -> {var_name: {value, version, lastWriterClientNo}}}
|
|
316
|
+
|
|
317
|
+
# Per-room monotonic write sequence. The server assigns the ordering for
|
|
318
|
+
# Network Variable last-writer-wins instead of trusting client-supplied
|
|
319
|
+
# timestamps, whose device clocks can drift on offline LAN deployments.
|
|
320
|
+
# The assigned sequence is stored in each variable's "version" field.
|
|
321
|
+
self.nv_write_seq: dict[str, int] = {} # room_id -> last assigned write seq
|
|
316
322
|
|
|
317
323
|
# NV Pending buffers for coalescing (latest-wins per key)
|
|
318
324
|
self.pending_global_nv: dict[str, dict[str, tuple]] = (
|
|
319
325
|
{}
|
|
320
|
-
) # room_id -> {var_name: (sender_client_no, value
|
|
326
|
+
) # room_id -> {var_name: (sender_client_no, value)}
|
|
321
327
|
self.pending_client_nv: dict[str, dict[tuple, tuple]] = (
|
|
322
328
|
{}
|
|
323
|
-
) # room_id -> {(target_client_no, var_name): (sender_client_no, value
|
|
329
|
+
) # room_id -> {(target_client_no, var_name): (sender_client_no, value)}
|
|
324
330
|
|
|
325
331
|
# NV flush cadence configuration (from config)
|
|
326
332
|
self.nv_flush_interval = config.nv_flush_interval
|
|
@@ -758,6 +764,7 @@ class NetSyncServer:
|
|
|
758
764
|
# Initialize Network Variables for the room
|
|
759
765
|
self.global_variables[room_id] = {}
|
|
760
766
|
self.client_variables[room_id] = {}
|
|
767
|
+
self.nv_write_seq[room_id] = 0
|
|
761
768
|
|
|
762
769
|
# Initialize NV pending buffers
|
|
763
770
|
self.pending_global_nv[room_id] = {}
|
|
@@ -1486,12 +1493,22 @@ class NetSyncServer:
|
|
|
1486
1493
|
f"High NV request rate in room {room_id}: {len(self.nv_monitor_window[room_id])} req/s"
|
|
1487
1494
|
)
|
|
1488
1495
|
|
|
1496
|
+
def _next_nv_seq(self, room_id: str) -> int:
|
|
1497
|
+
"""Return the next per-room monotonic NV write sequence.
|
|
1498
|
+
|
|
1499
|
+
Caller must hold ``_rooms_lock``. This sequence is the server-assigned
|
|
1500
|
+
ordering authority for last-writer-wins, replacing client timestamps so
|
|
1501
|
+
that skewed device clocks cannot freeze or steal Network Variables.
|
|
1502
|
+
"""
|
|
1503
|
+
seq = self.nv_write_seq.get(room_id, 0) + 1
|
|
1504
|
+
self.nv_write_seq[room_id] = seq
|
|
1505
|
+
return seq
|
|
1506
|
+
|
|
1489
1507
|
def _buffer_global_var_set(self, room_id: str, data: dict[str, Any]) -> None:
|
|
1490
1508
|
"""Buffer global variable set request for later processing"""
|
|
1491
1509
|
sender_client_no = data.get("senderClientNo", 0)
|
|
1492
1510
|
var_name = data.get("variableName", "")[: self.MAX_VAR_NAME_LENGTH]
|
|
1493
1511
|
var_value = data.get("variableValue", "")[: self.MAX_VAR_VALUE_LENGTH]
|
|
1494
|
-
timestamp = data.get("timestamp", time.monotonic())
|
|
1495
1512
|
|
|
1496
1513
|
if not var_name:
|
|
1497
1514
|
return
|
|
@@ -1503,7 +1520,6 @@ class NetSyncServer:
|
|
|
1503
1520
|
self.pending_global_nv[room_id][var_name] = (
|
|
1504
1521
|
sender_client_no,
|
|
1505
1522
|
var_value,
|
|
1506
|
-
timestamp,
|
|
1507
1523
|
)
|
|
1508
1524
|
|
|
1509
1525
|
def _apply_global_var_set(
|
|
@@ -1512,7 +1528,6 @@ class NetSyncServer:
|
|
|
1512
1528
|
sender_client_no: int,
|
|
1513
1529
|
var_name: str,
|
|
1514
1530
|
var_value: str,
|
|
1515
|
-
timestamp: float,
|
|
1516
1531
|
) -> bool:
|
|
1517
1532
|
"""Apply global variable update (used by flush, returns True if applied)"""
|
|
1518
1533
|
with self._rooms_lock:
|
|
@@ -1522,25 +1537,19 @@ class NetSyncServer:
|
|
|
1522
1537
|
logger.warning(f"Global variable limit reached in room {room_id}")
|
|
1523
1538
|
return False
|
|
1524
1539
|
|
|
1525
|
-
#
|
|
1540
|
+
# Skip if value unchanged (no-op)
|
|
1526
1541
|
if var_name in global_vars:
|
|
1527
|
-
|
|
1528
|
-
# Skip if value unchanged (no-op)
|
|
1529
|
-
if existing.get("value") == var_value:
|
|
1542
|
+
if global_vars[var_name].get("value") == var_value:
|
|
1530
1543
|
return False
|
|
1531
|
-
if timestamp < existing["timestamp"] or (
|
|
1532
|
-
timestamp == existing["timestamp"]
|
|
1533
|
-
and sender_client_no < existing["lastWriterClientNo"]
|
|
1534
|
-
):
|
|
1535
|
-
return False # Ignore older or lower priority update
|
|
1536
1544
|
|
|
1537
1545
|
# Store old value for logging
|
|
1538
1546
|
old_value = global_vars.get(var_name, {}).get("value", None)
|
|
1539
1547
|
|
|
1540
|
-
#
|
|
1548
|
+
# Last-writer-wins ordered by the server-assigned write sequence,
|
|
1549
|
+
# not client timestamps (device clocks can drift offline).
|
|
1541
1550
|
global_vars[var_name] = {
|
|
1542
1551
|
"value": var_value,
|
|
1543
|
-
"
|
|
1552
|
+
"version": self._next_nv_seq(room_id),
|
|
1544
1553
|
"lastWriterClientNo": sender_client_no,
|
|
1545
1554
|
}
|
|
1546
1555
|
|
|
@@ -1554,14 +1563,11 @@ class NetSyncServer:
|
|
|
1554
1563
|
sender_client_no = data.get("senderClientNo", 0)
|
|
1555
1564
|
var_name = data.get("variableName", "")[: self.MAX_VAR_NAME_LENGTH]
|
|
1556
1565
|
var_value = data.get("variableValue", "")[: self.MAX_VAR_VALUE_LENGTH]
|
|
1557
|
-
timestamp = data.get("timestamp", time.monotonic())
|
|
1558
1566
|
|
|
1559
1567
|
if not var_name:
|
|
1560
1568
|
return
|
|
1561
1569
|
|
|
1562
|
-
if self._apply_global_var_set(
|
|
1563
|
-
room_id, sender_client_no, var_name, var_value, timestamp
|
|
1564
|
-
):
|
|
1570
|
+
if self._apply_global_var_set(room_id, sender_client_no, var_name, var_value):
|
|
1565
1571
|
# Broadcast sync to all clients
|
|
1566
1572
|
self._broadcast_global_var_sync(room_id)
|
|
1567
1573
|
|
|
@@ -1571,7 +1577,6 @@ class NetSyncServer:
|
|
|
1571
1577
|
target_client_no = data.get("targetClientNo", 0)
|
|
1572
1578
|
var_name = data.get("variableName", "")[: self.MAX_VAR_NAME_LENGTH]
|
|
1573
1579
|
var_value = data.get("variableValue", "")[: self.MAX_VAR_VALUE_LENGTH]
|
|
1574
|
-
timestamp = data.get("timestamp", time.monotonic())
|
|
1575
1580
|
|
|
1576
1581
|
if not var_name:
|
|
1577
1582
|
return
|
|
@@ -1584,7 +1589,6 @@ class NetSyncServer:
|
|
|
1584
1589
|
self.pending_client_nv[room_id][key] = (
|
|
1585
1590
|
sender_client_no,
|
|
1586
1591
|
var_value,
|
|
1587
|
-
timestamp,
|
|
1588
1592
|
)
|
|
1589
1593
|
|
|
1590
1594
|
def _apply_client_var_set(
|
|
@@ -1594,7 +1598,6 @@ class NetSyncServer:
|
|
|
1594
1598
|
target_client_no: int,
|
|
1595
1599
|
var_name: str,
|
|
1596
1600
|
var_value: str,
|
|
1597
|
-
timestamp: float,
|
|
1598
1601
|
) -> bool:
|
|
1599
1602
|
"""Apply a client variable update addressed by volatile client number."""
|
|
1600
1603
|
with self._rooms_lock:
|
|
@@ -1613,7 +1616,6 @@ class NetSyncServer:
|
|
|
1613
1616
|
target_device_id,
|
|
1614
1617
|
var_name,
|
|
1615
1618
|
var_value,
|
|
1616
|
-
timestamp,
|
|
1617
1619
|
)
|
|
1618
1620
|
|
|
1619
1621
|
def _apply_client_var_set_for_device(
|
|
@@ -1623,7 +1625,6 @@ class NetSyncServer:
|
|
|
1623
1625
|
target_device_id: str,
|
|
1624
1626
|
var_name: str,
|
|
1625
1627
|
var_value: str,
|
|
1626
|
-
timestamp: float,
|
|
1627
1628
|
) -> bool:
|
|
1628
1629
|
"""Apply a client variable update to the authoritative device-keyed store."""
|
|
1629
1630
|
with self._rooms_lock:
|
|
@@ -1641,25 +1642,19 @@ class NetSyncServer:
|
|
|
1641
1642
|
)
|
|
1642
1643
|
return False
|
|
1643
1644
|
|
|
1644
|
-
#
|
|
1645
|
+
# Skip if value unchanged (no-op)
|
|
1645
1646
|
if var_name in client_vars:
|
|
1646
|
-
|
|
1647
|
-
# Skip if value unchanged (no-op)
|
|
1648
|
-
if existing.get("value") == var_value:
|
|
1647
|
+
if client_vars[var_name].get("value") == var_value:
|
|
1649
1648
|
return False
|
|
1650
|
-
if timestamp < existing["timestamp"] or (
|
|
1651
|
-
timestamp == existing["timestamp"]
|
|
1652
|
-
and sender_client_no < existing["lastWriterClientNo"]
|
|
1653
|
-
):
|
|
1654
|
-
return False # Ignore older or lower priority update
|
|
1655
1649
|
|
|
1656
1650
|
# Store old value for logging
|
|
1657
1651
|
old_value = client_vars.get(var_name, {}).get("value", None)
|
|
1658
1652
|
|
|
1659
|
-
#
|
|
1653
|
+
# Last-writer-wins ordered by the server-assigned write sequence,
|
|
1654
|
+
# not client timestamps (device clocks can drift offline).
|
|
1660
1655
|
client_vars[var_name] = {
|
|
1661
1656
|
"value": var_value,
|
|
1662
|
-
"
|
|
1657
|
+
"version": self._next_nv_seq(room_id),
|
|
1663
1658
|
"lastWriterClientNo": sender_client_no,
|
|
1664
1659
|
}
|
|
1665
1660
|
|
|
@@ -1672,7 +1667,6 @@ class NetSyncServer:
|
|
|
1672
1667
|
self, room_id: str, device_id: str, variables: dict[str, str]
|
|
1673
1668
|
) -> tuple[int | None, dict[str, str]]:
|
|
1674
1669
|
"""Upsert REST-provided client variables into the device-keyed store."""
|
|
1675
|
-
timestamp = time.time()
|
|
1676
1670
|
statuses: dict[str, str] = {}
|
|
1677
1671
|
changed = False
|
|
1678
1672
|
|
|
@@ -1703,12 +1697,20 @@ class NetSyncServer:
|
|
|
1703
1697
|
device_id,
|
|
1704
1698
|
var_name,
|
|
1705
1699
|
var_value,
|
|
1706
|
-
timestamp,
|
|
1707
1700
|
)
|
|
1708
1701
|
after = self.client_variables[room_id].get(device_id, {}).get(var_name)
|
|
1709
1702
|
changed = changed or applied or before != after
|
|
1710
1703
|
statuses[name] = "applied" if client_no is not None else "queued"
|
|
1711
1704
|
|
|
1705
|
+
# This REST write is now the authoritative latest value for the
|
|
1706
|
+
# key. Drop any older live write still buffered for the same
|
|
1707
|
+
# (client_no, var_name) so the next flush cannot resurrect a
|
|
1708
|
+
# stale value over it (mirrors delete/clear pruning).
|
|
1709
|
+
if client_no is not None:
|
|
1710
|
+
self._remove_pending_client_vars_locked(
|
|
1711
|
+
room_id, client_no, var_name
|
|
1712
|
+
)
|
|
1713
|
+
|
|
1712
1714
|
if client_no is not None and changed:
|
|
1713
1715
|
self._broadcast_client_var_sync(room_id, {client_no})
|
|
1714
1716
|
|
|
@@ -1823,13 +1825,12 @@ class NetSyncServer:
|
|
|
1823
1825
|
target_client_no = data.get("targetClientNo", 0)
|
|
1824
1826
|
var_name = data.get("variableName", "")[: self.MAX_VAR_NAME_LENGTH]
|
|
1825
1827
|
var_value = data.get("variableValue", "")[: self.MAX_VAR_VALUE_LENGTH]
|
|
1826
|
-
timestamp = data.get("timestamp", time.monotonic())
|
|
1827
1828
|
|
|
1828
1829
|
if not var_name:
|
|
1829
1830
|
return
|
|
1830
1831
|
|
|
1831
1832
|
if self._apply_client_var_set(
|
|
1832
|
-
room_id, sender_client_no, target_client_no, var_name, var_value
|
|
1833
|
+
room_id, sender_client_no, target_client_no, var_name, var_value
|
|
1833
1834
|
):
|
|
1834
1835
|
# Broadcast sync to all clients
|
|
1835
1836
|
self._broadcast_client_var_sync(room_id)
|
|
@@ -1850,7 +1851,6 @@ class NetSyncServer:
|
|
|
1850
1851
|
{
|
|
1851
1852
|
"name": var_name,
|
|
1852
1853
|
"value": var_data["value"],
|
|
1853
|
-
"timestamp": var_data["timestamp"],
|
|
1854
1854
|
"lastWriterClientNo": var_data["lastWriterClientNo"],
|
|
1855
1855
|
}
|
|
1856
1856
|
)
|
|
@@ -1899,7 +1899,6 @@ class NetSyncServer:
|
|
|
1899
1899
|
{
|
|
1900
1900
|
"name": var_name,
|
|
1901
1901
|
"value": var_data["value"],
|
|
1902
|
-
"timestamp": var_data["timestamp"],
|
|
1903
1902
|
"lastWriterClientNo": var_data["lastWriterClientNo"],
|
|
1904
1903
|
}
|
|
1905
1904
|
)
|
|
@@ -1970,25 +1969,30 @@ class NetSyncServer:
|
|
|
1970
1969
|
"""Drain all pending NV updates for a room in one go."""
|
|
1971
1970
|
start = time.perf_counter()
|
|
1972
1971
|
|
|
1972
|
+
applied_globals: list[str] = []
|
|
1973
|
+
applied_client_nos: set[int] = set()
|
|
1974
|
+
|
|
1975
|
+
# Drain and apply under a single lock hold. _rooms_lock is an RLock, so
|
|
1976
|
+
# the nested acquisitions inside _apply_* are reentrant. Holding the lock
|
|
1977
|
+
# across both steps prevents an immediate write (e.g. a REST upsert) from
|
|
1978
|
+
# interleaving in the drain/apply gap, where a stale buffered write would
|
|
1979
|
+
# otherwise overwrite the newer immediate one (last-writer-wins is now
|
|
1980
|
+
# ordered by application order, not by client timestamps).
|
|
1973
1981
|
with self._rooms_lock:
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
globals_to_apply = list(pending_globals.items())
|
|
1977
|
-
clients_to_apply = list(pending_clients.items())
|
|
1982
|
+
globals_to_apply = list(self.pending_global_nv.get(room_id, {}).items())
|
|
1983
|
+
clients_to_apply = list(self.pending_client_nv.get(room_id, {}).items())
|
|
1978
1984
|
self.pending_global_nv[room_id] = {}
|
|
1979
1985
|
self.pending_client_nv[room_id] = {}
|
|
1980
1986
|
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
applied_globals.append(var_name)
|
|
1987
|
+
for var_name, (sender, value) in globals_to_apply:
|
|
1988
|
+
if self._apply_global_var_set(room_id, sender, var_name, value):
|
|
1989
|
+
applied_globals.append(var_name)
|
|
1985
1990
|
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
applied_client_nos.add(target_client_no)
|
|
1991
|
+
for (target_client_no, var_name), (sender, value) in clients_to_apply:
|
|
1992
|
+
if self._apply_client_var_set(
|
|
1993
|
+
room_id, sender, target_client_no, var_name, value
|
|
1994
|
+
):
|
|
1995
|
+
applied_client_nos.add(target_client_no)
|
|
1992
1996
|
|
|
1993
1997
|
# Send NV syncs via ROUTER unicast for reliable delivery
|
|
1994
1998
|
if applied_globals:
|
|
@@ -2000,7 +2004,6 @@ class NetSyncServer:
|
|
|
2000
2004
|
{
|
|
2001
2005
|
"name": name,
|
|
2002
2006
|
"value": d["value"],
|
|
2003
|
-
"timestamp": d["timestamp"],
|
|
2004
2007
|
"lastWriterClientNo": d["lastWriterClientNo"],
|
|
2005
2008
|
}
|
|
2006
2009
|
)
|
|
@@ -2434,6 +2437,8 @@ class NetSyncServer:
|
|
|
2434
2437
|
del self.pending_global_nv[room_id]
|
|
2435
2438
|
if room_id in self.pending_client_nv:
|
|
2436
2439
|
del self.pending_client_nv[room_id]
|
|
2440
|
+
if room_id in self.nv_write_seq:
|
|
2441
|
+
del self.nv_write_seq[room_id]
|
|
2437
2442
|
if room_id in self.room_last_nv_flush:
|
|
2438
2443
|
del self.room_last_nv_flush[room_id]
|
|
2439
2444
|
if room_id in self.nv_monitor_window:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: styly-netsync-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: STYLY NetSync Server - Multiplayer framework for Location-Based Entertainment VR/MR experiences
|
|
5
5
|
Author-email: "STYLY, Inc." <info@styly.inc>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -289,11 +289,11 @@ def _reconstruct_physical_from_head_and_delta(
|
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
class TestTransformSerializationV5:
|
|
292
|
-
"""Tests for protocol
|
|
292
|
+
"""Tests for protocol v6 transform compact serialization."""
|
|
293
293
|
|
|
294
|
-
def
|
|
295
|
-
"""Protocol version constant should be at
|
|
296
|
-
assert binary_serializer.PROTOCOL_VERSION ==
|
|
294
|
+
def test_protocol_version_is_v6(self) -> None:
|
|
295
|
+
"""Protocol version constant should be at v6."""
|
|
296
|
+
assert binary_serializer.PROTOCOL_VERSION == 6
|
|
297
297
|
|
|
298
298
|
def test_client_roundtrip_without_flags_infers_valid_bits(self) -> None:
|
|
299
299
|
"""Serializer should infer valid bits when flags are omitted."""
|
|
@@ -418,7 +418,7 @@ class TestTransformSerializationV5:
|
|
|
418
418
|
|
|
419
419
|
assert msg_type == binary_serializer.MSG_CLIENT_POSE
|
|
420
420
|
assert decoded is not None
|
|
421
|
-
assert decoded["protocolVersion"] ==
|
|
421
|
+
assert decoded["protocolVersion"] == 6
|
|
422
422
|
assert len(raw) > 0
|
|
423
423
|
|
|
424
424
|
o_head = original["head"]
|
|
@@ -822,7 +822,7 @@ class TestTransformSerializationV5:
|
|
|
822
822
|
|
|
823
823
|
assert msg_type == binary_serializer.MSG_ROOM_POSE
|
|
824
824
|
assert decoded is not None
|
|
825
|
-
assert decoded["protocolVersion"] ==
|
|
825
|
+
assert decoded["protocolVersion"] == 6
|
|
826
826
|
assert decoded["roomId"] == "room-v5"
|
|
827
827
|
assert len(decoded["clients"]) == 2
|
|
828
828
|
|
|
@@ -1085,7 +1085,7 @@ class TestClientVariableClearSerialization:
|
|
|
1085
1085
|
|
|
1086
1086
|
def test_roundtrip_client_variable_clear(self) -> None:
|
|
1087
1087
|
"""Client variable clear message round-trips correctly."""
|
|
1088
|
-
data = {"senderClientNo": 7
|
|
1088
|
+
data = {"senderClientNo": 7}
|
|
1089
1089
|
|
|
1090
1090
|
serialized = binary_serializer.serialize_client_var_clear(data)
|
|
1091
1091
|
msg_type, result, _ = binary_serializer.deserialize(serialized)
|
|
@@ -61,9 +61,7 @@ class TestServerClientVariableDeviceStore:
|
|
|
61
61
|
def test_client_no_write_stores_by_device_id(self, server: NetSyncServer) -> None:
|
|
62
62
|
_map_device(server, "room1", "device-a", 7)
|
|
63
63
|
|
|
64
|
-
applied = server._apply_client_var_set(
|
|
65
|
-
"room1", 2, 7, "score", "100", time.time()
|
|
66
|
-
)
|
|
64
|
+
applied = server._apply_client_var_set("room1", 2, 7, "score", "100")
|
|
67
65
|
|
|
68
66
|
assert applied is True
|
|
69
67
|
assert server.client_variables["room1"]["device-a"]["score"]["value"] == "100"
|
|
@@ -75,8 +73,8 @@ class TestServerClientVariableDeviceStore:
|
|
|
75
73
|
_map_device(server, "room1", "device-a", 7)
|
|
76
74
|
_map_device(server, "room1", "device-b", 8)
|
|
77
75
|
|
|
78
|
-
server._apply_client_var_set("room1", 2, 7, "score", "100"
|
|
79
|
-
server._apply_client_var_set("room1", 2, 8, "score", "200"
|
|
76
|
+
server._apply_client_var_set("room1", 2, 7, "score", "100")
|
|
77
|
+
server._apply_client_var_set("room1", 2, 8, "score", "200")
|
|
80
78
|
|
|
81
79
|
assert server.client_variables["room1"]["device-a"]["score"]["value"] == "100"
|
|
82
80
|
assert server.client_variables["room1"]["device-b"]["score"]["value"] == "200"
|
|
@@ -101,7 +99,7 @@ class TestServerClientVariableDeviceStore:
|
|
|
101
99
|
self, server: NetSyncServer
|
|
102
100
|
) -> None:
|
|
103
101
|
_map_device(server, "room1", "device-a", 7)
|
|
104
|
-
server._apply_client_var_set("room1", 2, 7, "score", "100"
|
|
102
|
+
server._apply_client_var_set("room1", 2, 7, "score", "100")
|
|
105
103
|
|
|
106
104
|
payload = server._build_client_var_sync_payload("room1")
|
|
107
105
|
assert payload is not None
|
|
@@ -158,7 +156,7 @@ class TestServerClientVariableDeviceStore:
|
|
|
158
156
|
self, server: NetSyncServer
|
|
159
157
|
) -> None:
|
|
160
158
|
_map_device(server, "room1", "device-a", 7)
|
|
161
|
-
server._apply_client_var_set("room1", 2, 7, "score", "100"
|
|
159
|
+
server._apply_client_var_set("room1", 2, 7, "score", "100")
|
|
162
160
|
server.device_id_last_seen["device-a"] = (
|
|
163
161
|
time.monotonic() - server.DEVICE_ID_EXPIRY_TIME - 1.0
|
|
164
162
|
)
|
|
@@ -174,7 +172,7 @@ class TestServerClientVariableDeviceStore:
|
|
|
174
172
|
) -> None:
|
|
175
173
|
_map_device(server, "room1", "device-a", 7)
|
|
176
174
|
server.client_transform_body_cache[7] = b"old"
|
|
177
|
-
server._apply_client_var_set("room1", 2, 7, "score", "100"
|
|
175
|
+
server._apply_client_var_set("room1", 2, 7, "score", "100")
|
|
178
176
|
server.device_id_last_seen["device-a"] = (
|
|
179
177
|
time.monotonic() - server.DEVICE_ID_EXPIRY_TIME - 1.0
|
|
180
178
|
)
|
|
@@ -193,8 +191,8 @@ class TestServerClientVariableDeviceStore:
|
|
|
193
191
|
server.upsert_client_variables_for_device(
|
|
194
192
|
"room1", "device-a", {"a": "1", "b": "2"}
|
|
195
193
|
)
|
|
196
|
-
server.pending_client_nv["room1"][(7, "a")] = (7, "stale"
|
|
197
|
-
server.pending_client_nv["room1"][(8, "a")] = (8, "other"
|
|
194
|
+
server.pending_client_nv["room1"][(7, "a")] = (7, "stale")
|
|
195
|
+
server.pending_client_nv["room1"][(8, "a")] = (8, "other")
|
|
198
196
|
server._send_ctrl_to_room_via_router.reset_mock()
|
|
199
197
|
|
|
200
198
|
server._handle_client_var_clear(
|
|
@@ -363,7 +361,7 @@ class TestRestClientVariableDeviceStore:
|
|
|
363
361
|
) -> None:
|
|
364
362
|
_map_device(server, "room1", "device-a", 7)
|
|
365
363
|
server.upsert_client_variables_for_device("room1", "device-a", {"a": "1"})
|
|
366
|
-
server.pending_client_nv["room1"][(7, "a")] = (7, "stale"
|
|
364
|
+
server.pending_client_nv["room1"][(7, "a")] = (7, "stale")
|
|
367
365
|
server._send_ctrl_to_room_via_router.reset_mock()
|
|
368
366
|
tc = self._client(server)
|
|
369
367
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Server-authoritative Network Variable ordering (issue #448).
|
|
2
|
+
|
|
3
|
+
The server assigns a per-room monotonic write sequence for last-writer-wins
|
|
4
|
+
instead of trusting client-supplied timestamps, so skewed device clocks on
|
|
5
|
+
offline LAN deployments cannot freeze or steal Network Variables.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from unittest.mock import MagicMock
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from styly_netsync.server import NetSyncServer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture()
|
|
20
|
+
def server() -> Iterator[NetSyncServer]:
|
|
21
|
+
srv = NetSyncServer(enable_server_discovery=False)
|
|
22
|
+
srv._send_ctrl_to_room_via_router = MagicMock() # type: ignore[method-assign]
|
|
23
|
+
yield srv
|
|
24
|
+
srv.context.term()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _map_device(
|
|
28
|
+
srv: NetSyncServer, room_id: str, device_id: str, client_no: int
|
|
29
|
+
) -> None:
|
|
30
|
+
srv._initialize_room(room_id)
|
|
31
|
+
srv.room_device_id_to_client_no[room_id][device_id] = client_no
|
|
32
|
+
srv.room_client_no_to_device_id[room_id][client_no] = device_id
|
|
33
|
+
srv.device_id_last_seen[device_id] = time.monotonic()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestGlobalVariableServerOrdering:
|
|
37
|
+
def test_last_applied_write_wins(self, server: NetSyncServer) -> None:
|
|
38
|
+
server._initialize_room("room1")
|
|
39
|
+
|
|
40
|
+
assert server._apply_global_var_set("room1", 1, "score", "100") is True
|
|
41
|
+
assert server._apply_global_var_set("room1", 2, "score", "200") is True
|
|
42
|
+
|
|
43
|
+
stored = server.global_variables["room1"]["score"]
|
|
44
|
+
assert stored["value"] == "200"
|
|
45
|
+
assert stored["lastWriterClientNo"] == 2
|
|
46
|
+
|
|
47
|
+
def test_write_sequence_is_monotonic_and_server_assigned(
|
|
48
|
+
self, server: NetSyncServer
|
|
49
|
+
) -> None:
|
|
50
|
+
server._initialize_room("room1")
|
|
51
|
+
assert server.nv_write_seq["room1"] == 0
|
|
52
|
+
|
|
53
|
+
server._apply_global_var_set("room1", 1, "a", "1")
|
|
54
|
+
server._apply_global_var_set("room1", 1, "b", "2")
|
|
55
|
+
|
|
56
|
+
assert server.global_variables["room1"]["a"]["version"] == 1
|
|
57
|
+
assert server.global_variables["room1"]["b"]["version"] == 2
|
|
58
|
+
assert server.nv_write_seq["room1"] == 2
|
|
59
|
+
|
|
60
|
+
def test_no_op_value_does_not_consume_sequence(self, server: NetSyncServer) -> None:
|
|
61
|
+
server._initialize_room("room1")
|
|
62
|
+
assert server._apply_global_var_set("room1", 1, "a", "1") is True
|
|
63
|
+
seq_after_first = server.nv_write_seq["room1"]
|
|
64
|
+
|
|
65
|
+
# Same value -> no-op, returns False and must not bump the sequence
|
|
66
|
+
assert server._apply_global_var_set("room1", 2, "a", "1") is False
|
|
67
|
+
assert server.nv_write_seq["room1"] == seq_after_first
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestClientVariableServerOrdering:
|
|
71
|
+
def test_last_applied_write_wins(self, server: NetSyncServer) -> None:
|
|
72
|
+
_map_device(server, "room1", "device-a", 7)
|
|
73
|
+
|
|
74
|
+
assert server._apply_client_var_set("room1", 2, 7, "hp", "10") is True
|
|
75
|
+
assert server._apply_client_var_set("room1", 3, 7, "hp", "20") is True
|
|
76
|
+
|
|
77
|
+
stored = server.client_variables["room1"]["device-a"]["hp"]
|
|
78
|
+
assert stored["value"] == "20"
|
|
79
|
+
assert stored["lastWriterClientNo"] == 3
|
|
80
|
+
|
|
81
|
+
def test_rest_and_live_writes_share_one_sequence_domain(
|
|
82
|
+
self, server: NetSyncServer
|
|
83
|
+
) -> None:
|
|
84
|
+
_map_device(server, "room1", "device-a", 7)
|
|
85
|
+
|
|
86
|
+
server._apply_client_var_set("room1", 2, 7, "hp", "10")
|
|
87
|
+
live_seq = server.client_variables["room1"]["device-a"]["hp"]["version"]
|
|
88
|
+
|
|
89
|
+
server.upsert_client_variables_for_device("room1", "device-a", {"hp": "30"})
|
|
90
|
+
rest_seq = server.client_variables["room1"]["device-a"]["hp"]["version"]
|
|
91
|
+
|
|
92
|
+
assert rest_seq > live_seq
|
|
93
|
+
assert server.client_variables["room1"]["device-a"]["hp"]["value"] == "30"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestLiveVsRestOrderingRegression:
|
|
97
|
+
"""Regression: a newer REST write must not be clobbered by an older live
|
|
98
|
+
write that was still buffered when the REST write arrived.
|
|
99
|
+
|
|
100
|
+
Live socket writes are coalesced into a pending buffer and applied later by
|
|
101
|
+
``_flush_nv_drain``; REST writes apply immediately. Once client timestamps
|
|
102
|
+
were removed, an out-of-order application no longer self-rejects, so the
|
|
103
|
+
REST path must prune any superseded buffered write for the same key.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def test_buffered_live_write_does_not_overwrite_newer_rest_write(
|
|
107
|
+
self, server: NetSyncServer
|
|
108
|
+
) -> None:
|
|
109
|
+
_map_device(server, "room1", "device-a", 7)
|
|
110
|
+
|
|
111
|
+
# An older live client write is buffered, awaiting the next flush.
|
|
112
|
+
server._buffer_client_var_set(
|
|
113
|
+
"room1",
|
|
114
|
+
{
|
|
115
|
+
"senderClientNo": 7,
|
|
116
|
+
"targetClientNo": 7,
|
|
117
|
+
"variableName": "hp",
|
|
118
|
+
"variableValue": "10",
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# A REST upsert applies a newer value immediately.
|
|
123
|
+
server.upsert_client_variables_for_device("room1", "device-a", {"hp": "20"})
|
|
124
|
+
|
|
125
|
+
# Draining must not resurrect the stale buffered "10" over the REST "20".
|
|
126
|
+
server._flush_nv_drain("room1")
|
|
127
|
+
|
|
128
|
+
assert server.client_variables["room1"]["device-a"]["hp"]["value"] == "20"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestRoomCleanupReleasesNvWriteSeq:
|
|
132
|
+
"""Regression: room cleanup must drop nv_write_seq so per-room sequence
|
|
133
|
+
entries do not accumulate under room churn."""
|
|
134
|
+
|
|
135
|
+
def test_nv_write_seq_entry_is_removed_on_room_cleanup(
|
|
136
|
+
self, server: NetSyncServer
|
|
137
|
+
) -> None:
|
|
138
|
+
server._initialize_room("room1")
|
|
139
|
+
server._apply_global_var_set("room1", 1, "a", "1")
|
|
140
|
+
assert "room1" in server.nv_write_seq
|
|
141
|
+
|
|
142
|
+
# Room has been empty long enough to be reclaimed by the real cleanup.
|
|
143
|
+
server.room_empty_since["room1"] = 0.0
|
|
144
|
+
server._cleanup_clients(server.EMPTY_ROOM_EXPIRY_TIME + 100.0)
|
|
145
|
+
|
|
146
|
+
assert "room1" not in server.rooms
|
|
147
|
+
assert "room1" not in server.nv_write_seq
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/logging_utils.py
RENAMED
|
File without changes
|
{styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/network_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/src/styly_netsync/rest_bridge.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_port_error_message.py
RENAMED
|
File without changes
|
|
File without changes
|
{styly_netsync_server-0.13.0 → styly_netsync_server-0.14.0}/tests/test_reconnect_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|