wse-server 2.0.0__tar.gz → 2.0.2__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.
Files changed (28) hide show
  1. {wse_server-2.0.0 → wse_server-2.0.2}/PKG-INFO +7 -6
  2. {wse_server-2.0.0 → wse_server-2.0.2}/README.md +5 -4
  3. {wse_server-2.0.0 → wse_server-2.0.2}/pyproject.toml +2 -2
  4. {wse_server-2.0.0 → wse_server-2.0.2}/rust/Cargo.lock +1 -1
  5. {wse_server-2.0.0 → wse_server-2.0.2}/rust/Cargo.toml +1 -1
  6. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/server.rs +359 -26
  7. {wse_server-2.0.0 → wse_server-2.0.2}/wse_server/__init__.py +9 -1
  8. wse_server-2.0.2/wse_server/_wse_accel.pyi +455 -0
  9. {wse_server-2.0.0 → wse_server-2.0.2}/LICENSE +0 -0
  10. {wse_server-2.0.0 → wse_server-2.0.2}/rust/.cargo/config.toml +0 -0
  11. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/jwt.rs +0 -0
  12. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/lib.rs +0 -0
  13. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/cluster.rs +0 -0
  14. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/compression.rs +0 -0
  15. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/filters.rs +0 -0
  16. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/mod.rs +0 -0
  17. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/presence.rs +0 -0
  18. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/queue.rs +0 -0
  19. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/rate_limiter.rs +0 -0
  20. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/recovery.rs +0 -0
  21. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/reliability.rs +0 -0
  22. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/security.rs +0 -0
  23. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/sequencer.rs +0 -0
  24. {wse_server-2.0.0 → wse_server-2.0.2}/rust/src/wse/transformer.rs +0 -0
  25. {wse_server-2.0.0 → wse_server-2.0.2}/wse_server/core/__init__.py +0 -0
  26. {wse_server-2.0.0 → wse_server-2.0.2}/wse_server/core/filters.py +0 -0
  27. {wse_server-2.0.0 → wse_server-2.0.2}/wse_server/core/types.py +0 -0
  28. {wse_server-2.0.0 → wse_server-2.0.2}/wse_server/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wse-server
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -27,7 +27,7 @@ Provides-Extra: dev
27
27
  Provides-Extra: msgpack
28
28
  Provides-Extra: prometheus
29
29
  License-File: LICENSE
30
- Summary: Rust-powered WebSocket engine for Python. Up to 14M msg/s JSON, 30M binary (MsgPack/zlib). Zero-GIL JWT. E2E encrypted.
30
+ Summary: Rust-powered WebSocket engine for Python. Up to 5M fan-out del/s. Zero-GIL JWT.
31
31
  Keywords: websocket,engine,real-time,pubsub,cluster,encrypted,rust
32
32
  Author: silvermpx
33
33
  License: MIT
@@ -46,7 +46,7 @@ Project-URL: Repository, https://github.com/silvermpx/wse
46
46
  [![npm](https://img.shields.io/npm/v/wse-client)](https://www.npmjs.com/package/wse-client)
47
47
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
48
48
 
49
- High-performance WebSocket server built in Rust with native clustering, message recovery, presence tracking, and real-time fan-out. Exposed to Python via PyO3 with zero GIL overhead on the data path.
49
+ High-performance WebSocket server built in Rust with native clustering, E2E encryption, message recovery, presence tracking, and real-time fan-out. Exposed to Python via PyO3 with zero GIL overhead on the data path. Ships with Rust-accelerated utilities for application code: priority queues, rate limiters, event sequencing, compression, cryptography, and JWT.
50
50
 
51
51
  ## Features
52
52
 
@@ -504,7 +504,7 @@ client.run_forever()
504
504
 
505
505
  Key features: 4 reconnect strategies (exponential, linear, fibonacci, adaptive), connection pool with health scoring and 3 load balancing strategies, circuit breaker, token bucket rate limiter, event sequencer with dedup and reorder buffering, network quality monitoring (latency/jitter/packet loss).
506
506
 
507
- See [python-client/README.md](python-client/README.md) for full API reference.
507
+ See [python-client/](python-client/) for full source and examples.
508
508
 
509
509
  ### TypeScript / React
510
510
 
@@ -530,13 +530,13 @@ function App() {
530
530
 
531
531
  Key features: offline queue with IndexedDB persistence, adaptive quality management, connection pool with health scoring, E2E encryption (Web Crypto API), message batching, 5 priority levels, Zustand store for external state access.
532
532
 
533
- See [client/README.md](client/README.md) for full API reference.
533
+ See [client/](client/) for full source and examples.
534
534
 
535
535
  ---
536
536
 
537
537
  ## Performance
538
538
 
539
- Benchmarked on AMD EPYC 7502P (64 cores, 128 GB RAM), Ubuntu 24.04.
539
+ Benchmarked on AMD EPYC 7502P (32 cores / 64 threads, 128 GB RAM), Ubuntu 24.04.
540
540
 
541
541
  | Mode | Peak Throughput | Connections | Message Loss |
542
542
  |------|----------------|-------------|--------------|
@@ -570,6 +570,7 @@ Working examples in the [`examples/`](examples/) directory:
570
570
  - [Integration Guide](docs/INTEGRATION.md) - complete setup and API reference
571
571
  - [Wire Protocol](docs/PROTOCOL.md) - message format specification
572
572
  - [Cluster Protocol](docs/CLUSTER_PROTOCOL.md) - TCP mesh, frame format, gossip, interest routing
573
+ - [Rust Utilities](docs/RUST_UTILITIES.md) - priority queues, rate limiters, sequencing, compression, crypto, JWT
573
574
  - [Security](docs/SECURITY.md) - JWT, encryption, mTLS, rate limiting, circuit breaker
574
575
  - [Deployment](docs/DEPLOYMENT.md) - production setup, Docker, Kubernetes, cluster configuration
575
576
  - [Migration Guide](docs/MIGRATION.md) - upgrading from v1.x to v2.0
@@ -5,7 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/v/wse-client)](https://www.npmjs.com/package/wse-client)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
7
 
8
- High-performance WebSocket server built in Rust with native clustering, message recovery, presence tracking, and real-time fan-out. Exposed to Python via PyO3 with zero GIL overhead on the data path.
8
+ High-performance WebSocket server built in Rust with native clustering, E2E encryption, message recovery, presence tracking, and real-time fan-out. Exposed to Python via PyO3 with zero GIL overhead on the data path. Ships with Rust-accelerated utilities for application code: priority queues, rate limiters, event sequencing, compression, cryptography, and JWT.
9
9
 
10
10
  ## Features
11
11
 
@@ -463,7 +463,7 @@ client.run_forever()
463
463
 
464
464
  Key features: 4 reconnect strategies (exponential, linear, fibonacci, adaptive), connection pool with health scoring and 3 load balancing strategies, circuit breaker, token bucket rate limiter, event sequencer with dedup and reorder buffering, network quality monitoring (latency/jitter/packet loss).
465
465
 
466
- See [python-client/README.md](python-client/README.md) for full API reference.
466
+ See [python-client/](python-client/) for full source and examples.
467
467
 
468
468
  ### TypeScript / React
469
469
 
@@ -489,13 +489,13 @@ function App() {
489
489
 
490
490
  Key features: offline queue with IndexedDB persistence, adaptive quality management, connection pool with health scoring, E2E encryption (Web Crypto API), message batching, 5 priority levels, Zustand store for external state access.
491
491
 
492
- See [client/README.md](client/README.md) for full API reference.
492
+ See [client/](client/) for full source and examples.
493
493
 
494
494
  ---
495
495
 
496
496
  ## Performance
497
497
 
498
- Benchmarked on AMD EPYC 7502P (64 cores, 128 GB RAM), Ubuntu 24.04.
498
+ Benchmarked on AMD EPYC 7502P (32 cores / 64 threads, 128 GB RAM), Ubuntu 24.04.
499
499
 
500
500
  | Mode | Peak Throughput | Connections | Message Loss |
501
501
  |------|----------------|-------------|--------------|
@@ -529,6 +529,7 @@ Working examples in the [`examples/`](examples/) directory:
529
529
  - [Integration Guide](docs/INTEGRATION.md) - complete setup and API reference
530
530
  - [Wire Protocol](docs/PROTOCOL.md) - message format specification
531
531
  - [Cluster Protocol](docs/CLUSTER_PROTOCOL.md) - TCP mesh, frame format, gossip, interest routing
532
+ - [Rust Utilities](docs/RUST_UTILITIES.md) - priority queues, rate limiters, sequencing, compression, crypto, JWT
532
533
  - [Security](docs/SECURITY.md) - JWT, encryption, mTLS, rate limiting, circuit breaker
533
534
  - [Deployment](docs/DEPLOYMENT.md) - production setup, Docker, Kubernetes, cluster configuration
534
535
  - [Migration Guide](docs/MIGRATION.md) - upgrading from v1.x to v2.0
@@ -4,8 +4,8 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "wse-server"
7
- version = "2.0.0"
8
- description = "Rust-powered WebSocket engine for Python. Up to 14M msg/s JSON, 30M binary (MsgPack/zlib). Zero-GIL JWT. E2E encrypted."
7
+ version = "2.0.2"
8
+ description = "Rust-powered WebSocket engine for Python. Up to 5M fan-out del/s. Zero-GIL JWT."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  license-files = ["LICENSE"]
@@ -1912,7 +1912,7 @@ dependencies = [
1912
1912
 
1913
1913
  [[package]]
1914
1914
  name = "wse-accel"
1915
- version = "2.0.0"
1915
+ version = "2.0.2"
1916
1916
  dependencies = [
1917
1917
  "aes-gcm",
1918
1918
  "ahash",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "wse-accel"
3
- version = "2.0.0"
3
+ version = "2.0.2"
4
4
  edition = "2024"
5
5
 
6
6
  [lib]
@@ -217,6 +217,8 @@ pub(crate) struct SharedState {
217
217
  pub(crate) recovery: Option<Arc<super::recovery::RecoveryManager>>,
218
218
  // Per-connection last activity tracking for zombie detection
219
219
  conn_last_activity: DashMap<String, std::time::Instant>,
220
+ // Per-connection AES-256-GCM cipher for E2E encryption (optional, client-initiated)
221
+ conn_encryption: DashMap<String, aes_gcm::Aes256Gcm>,
220
222
  // Presence tracking (None when disabled)
221
223
  pub(crate) presence: Option<Arc<super::presence::PresenceManager>>,
222
224
  // Sender for presence broadcasts from disconnect cleanup paths
@@ -261,6 +263,7 @@ impl SharedState {
261
263
  cluster_tls_enabled: AtomicBool::new(false),
262
264
  recovery,
263
265
  conn_last_activity: DashMap::new(),
266
+ conn_encryption: DashMap::new(),
264
267
  presence: if presence_enabled {
265
268
  Some(Arc::new(super::presence::PresenceManager::new(
266
269
  presence_max_data_size,
@@ -520,13 +523,90 @@ fn build_error_json(code: &str, message: &str) -> String {
520
523
  format!("WSE{}", payload)
521
524
  }
522
525
 
526
+ // ---------------------------------------------------------------------------
527
+ // E2E encryption helpers (ECDH P-256 + AES-GCM-256)
528
+ // ---------------------------------------------------------------------------
529
+
530
+ /// Perform ECDH key exchange: generate server keypair, derive AES-256 key.
531
+ /// Returns (server_public_key_base64, Aes256Gcm cipher).
532
+ fn derive_connection_key(client_pubkey_b64: &str) -> Result<(String, aes_gcm::Aes256Gcm), String> {
533
+ use aes_gcm::KeyInit;
534
+ use base64::Engine;
535
+ use hkdf::Hkdf;
536
+ use p256::ecdh::diffie_hellman;
537
+ use p256::elliptic_curve::sec1::ToEncodedPoint;
538
+ use p256::{PublicKey, SecretKey};
539
+ use sha2::Sha256;
540
+
541
+ let client_pubkey_bytes = base64::engine::general_purpose::STANDARD
542
+ .decode(client_pubkey_b64)
543
+ .map_err(|e| format!("Invalid base64: {e}"))?;
544
+
545
+ let client_pk = PublicKey::from_sec1_bytes(&client_pubkey_bytes)
546
+ .map_err(|e| format!("Invalid ECDH public key: {e}"))?;
547
+
548
+ let server_sk = SecretKey::random(&mut aes_gcm::aead::OsRng);
549
+ let server_pk = server_sk.public_key();
550
+
551
+ let shared = diffie_hellman(server_sk.to_nonzero_scalar(), client_pk.as_affine());
552
+
553
+ let hkdf = Hkdf::<Sha256>::new(Some(b"wse-encryption"), shared.raw_secret_bytes());
554
+ let mut aes_key = [0u8; 32];
555
+ hkdf.expand(b"aes-gcm-key", &mut aes_key)
556
+ .map_err(|e| format!("HKDF expand failed: {e}"))?;
557
+
558
+ let cipher = aes_gcm::Aes256Gcm::new_from_slice(&aes_key)
559
+ .map_err(|e| format!("AES key init failed: {e}"))?;
560
+
561
+ let server_pk_point = server_pk.to_encoded_point(false);
562
+ let server_pk_b64 =
563
+ base64::engine::general_purpose::STANDARD.encode(server_pk_point.as_bytes());
564
+
565
+ Ok((server_pk_b64, cipher))
566
+ }
567
+
568
+ /// Encrypt plaintext with AES-GCM-256: returns 12-byte IV + ciphertext + 16-byte tag.
569
+ fn encrypt_outbound(cipher: &aes_gcm::Aes256Gcm, plaintext: &[u8]) -> Result<Vec<u8>, String> {
570
+ use aes_gcm::AeadCore;
571
+ use aes_gcm::aead::Aead;
572
+
573
+ let nonce = aes_gcm::Aes256Gcm::generate_nonce(&mut aes_gcm::aead::OsRng);
574
+ let ciphertext = cipher
575
+ .encrypt(&nonce, plaintext)
576
+ .map_err(|e| format!("AES-GCM encrypt failed: {e}"))?;
577
+ let mut result = Vec::with_capacity(12 + ciphertext.len());
578
+ result.extend_from_slice(&nonce);
579
+ result.extend_from_slice(&ciphertext);
580
+ Ok(result)
581
+ }
582
+
583
+ /// Decrypt an AES-GCM-256 payload (12-byte IV + ciphertext + 16-byte tag).
584
+ fn decrypt_inbound(cipher: &aes_gcm::Aes256Gcm, data: &[u8]) -> Result<Vec<u8>, String> {
585
+ use aes_gcm::aead::Aead;
586
+
587
+ if data.len() < 28 {
588
+ return Err("Encrypted payload too short (min 28 bytes)".into());
589
+ }
590
+ let (nonce_bytes, ciphertext) = data.split_at(12);
591
+ let nonce: [u8; 12] = nonce_bytes.try_into().expect("split_at(12)");
592
+ cipher
593
+ .decrypt(&nonce.into(), ciphertext)
594
+ .map_err(|e| format!("AES-GCM decrypt failed: {e}"))
595
+ }
596
+
523
597
  /// Build server_hello JSON response to client_hello (protocol negotiation).
524
- fn build_server_hello(conn_id: &str, client_proto: u32, state: &SharedState) -> String {
598
+ fn build_server_hello(
599
+ conn_id: &str,
600
+ client_proto: u32,
601
+ state: &SharedState,
602
+ encryption_pubkey: Option<&str>,
603
+ ) -> String {
525
604
  let proto = client_proto.min(2);
526
605
  let recovery_enabled = state.recovery.is_some();
527
606
  let cluster_enabled = state.cluster_cmd_tx.read().unwrap().is_some();
607
+ let encryption_enabled = encryption_pubkey.is_some();
528
608
  let ts = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
529
- let val = serde_json::json!({
609
+ let mut val = serde_json::json!({
530
610
  "t": "server_hello",
531
611
  "p": {
532
612
  "server_version": env!("CARGO_PKG_VERSION"),
@@ -537,7 +617,8 @@ fn build_server_hello(conn_id: &str, client_proto: u32, state: &SharedState) ->
537
617
  "cluster": cluster_enabled,
538
618
  "batching": true,
539
619
  "priority_queue": true,
540
- "msgpack": true
620
+ "msgpack": true,
621
+ "encryption": encryption_enabled
541
622
  },
542
623
  "limits": {
543
624
  "max_message_size": 1_048_576,
@@ -550,6 +631,9 @@ fn build_server_hello(conn_id: &str, client_proto: u32, state: &SharedState) ->
550
631
  },
551
632
  "v": 1
552
633
  });
634
+ if let Some(pubkey) = encryption_pubkey {
635
+ val["p"]["encryption_public_key"] = serde_json::Value::String(pubkey.to_string());
636
+ }
553
637
  format!("WSE{val}")
554
638
  }
555
639
 
@@ -591,6 +675,12 @@ fn build_server_ready(conn_id: &str, user_id: &str, recovery_enabled: bool) -> S
591
675
  // ---------------------------------------------------------------------------
592
676
 
593
677
  async fn handle_connection(stream: TcpStream, addr: SocketAddr, state: Arc<SharedState>) {
678
+ // Early rejection before expensive handshake (best-effort, authoritative check below)
679
+ if state.connection_count.load(Ordering::Relaxed) >= state.max_connections {
680
+ eprintln!("[WSE] Max connections reached (early check), rejecting {addr}");
681
+ return;
682
+ }
683
+
594
684
  // Clone the TCP stream FD for pre-framed raw writes (bypasses tungstenite).
595
685
  let (stream, mut raw_write) = match clone_tcp_stream(stream) {
596
686
  Ok(pair) => pair,
@@ -693,7 +783,9 @@ async fn handle_connection(stream: TcpStream, addr: SocketAddr, state: Arc<Share
693
783
  }
694
784
  }
695
785
  Err(e) => {
696
- reject_ws_auth(&tx, rx, write_half, "AUTH_FAILED", &format!("{e}")).await;
786
+ eprintln!("[WSE] JWT validation failed for {addr}: {e}");
787
+ reject_ws_auth(&tx, rx, write_half, "AUTH_FAILED", "Authentication failed")
788
+ .await;
697
789
  return;
698
790
  }
699
791
  },
@@ -931,7 +1023,47 @@ async fn handle_connection(stream: TcpStream, addr: SocketAddr, state: Arc<Share
931
1023
  p.and_then(|p| p.get("protocol_version"))
932
1024
  .and_then(|v| v.as_u64())
933
1025
  .unwrap_or(1) as u32;
934
- let hello = build_server_hello(&conn_id, client_proto, &state);
1026
+
1027
+ // Check if client requests E2E encryption
1028
+ let wants_encryption = p
1029
+ .and_then(|p| p.get("features"))
1030
+ .and_then(|f| f.get("encryption"))
1031
+ .and_then(|v| v.as_bool())
1032
+ .unwrap_or(false);
1033
+
1034
+ let server_pubkey: Option<String> = if wants_encryption {
1035
+ p.and_then(|p| p.get("encryption_public_key"))
1036
+ .and_then(|v| v.as_str())
1037
+ .and_then(|b64| match derive_connection_key(b64) {
1038
+ Ok((server_pk_b64, cipher)) => {
1039
+ state
1040
+ .conn_encryption
1041
+ .insert((*conn_id).clone(), cipher);
1042
+ Some(server_pk_b64)
1043
+ }
1044
+ Err(e) => {
1045
+ eprintln!(
1046
+ "[WSE] ECDH key exchange failed for {}: {}",
1047
+ conn_id, e
1048
+ );
1049
+ let err_msg = format!(
1050
+ "WSE{{\"t\":\"error\",\"p\":{{\"code\":\"KEY_EXCHANGE_FAILED\",\"message\":\"ECDH key exchange failed: {}\",\"recoverable\":true}}}}",
1051
+ e.replace('\"', "\\\"")
1052
+ );
1053
+ let _ = tx.send(WsFrame::Msg(Message::Text(err_msg.into())));
1054
+ None
1055
+ }
1056
+ })
1057
+ } else {
1058
+ None
1059
+ };
1060
+
1061
+ let hello = build_server_hello(
1062
+ &conn_id,
1063
+ client_proto,
1064
+ &state,
1065
+ server_pubkey.as_deref(),
1066
+ );
935
1067
  let _ = tx.send(WsFrame::Msg(Message::Text(hello.into())));
936
1068
  continue;
937
1069
  }
@@ -947,7 +1079,7 @@ async fn handle_connection(stream: TcpStream, addr: SocketAddr, state: Arc<Share
947
1079
  .as_millis()
948
1080
  as i64;
949
1081
  let pong = format!(
950
- "WSE{{\"t\":\"PONG\",\"p\":{{\"client_timestamp\":{},\"server_timestamp\":{},\"latency\":{}}},\"v\":2}}",
1082
+ "WSE{{\"t\":\"PONG\",\"p\":{{\"client_timestamp\":{},\"server_timestamp\":{},\"latency\":{}}},\"v\":1}}",
951
1083
  timestamp,
952
1084
  server_ts,
953
1085
  server_ts.saturating_sub(timestamp).max(0)
@@ -1010,6 +1142,70 @@ async fn handle_connection(stream: TcpStream, addr: SocketAddr, state: Arc<Share
1010
1142
  state
1011
1143
  .conn_last_activity
1012
1144
  .insert((*conn_id).clone(), std::time::Instant::now());
1145
+
1146
+ // E2E encryption: detect E: prefix and decrypt before processing
1147
+ if data.len() >= 2 && data[0] == b'E' && data[1] == b':' {
1148
+ if let Some(cipher_ref) = state.conn_encryption.get(&*conn_id) {
1149
+ match decrypt_inbound(&cipher_ref, &data[2..]) {
1150
+ Ok(plaintext) => {
1151
+ // Decrypted payload is UTF-8 text (JSON with optional prefix)
1152
+ if let Ok(text) = std::str::from_utf8(&plaintext) {
1153
+ let json_str = if text.starts_with("WSE{") {
1154
+ &text[3..]
1155
+ } else if text.starts_with("S{") || text.starts_with("U{") {
1156
+ &text[1..]
1157
+ } else if text.starts_with('{') {
1158
+ text
1159
+ } else {
1160
+ if drain {
1161
+ state.push_inbound(InboundEvent::RawText {
1162
+ conn_id: (*conn_id).clone(),
1163
+ text: text.to_string(),
1164
+ });
1165
+ }
1166
+ continue;
1167
+ };
1168
+ if let Ok(val) =
1169
+ serde_json::from_str::<serde_json::Value>(json_str)
1170
+ {
1171
+ let val = normalize_json_keys(val);
1172
+ if drain {
1173
+ state.push_inbound(InboundEvent::Message {
1174
+ conn_id: (*conn_id).clone(),
1175
+ value: val,
1176
+ });
1177
+ } else if let Some(ref cb) = cached_on_message {
1178
+ let cb_arc = cb.clone();
1179
+ let cid = Arc::clone(&conn_id);
1180
+ tokio::task::spawn_blocking(move || {
1181
+ Python::try_attach(|py| {
1182
+ let py_obj = json_to_pyobj(py, &val);
1183
+ if let Err(e) =
1184
+ cb_arc.call1(py, (&**cid, py_obj))
1185
+ {
1186
+ eprintln!(
1187
+ "[WSE] on_message (decrypted) error: {e}"
1188
+ );
1189
+ }
1190
+ });
1191
+ });
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ Err(e) => {
1197
+ eprintln!("[WSE] Decryption failed for {}: {}", conn_id, e);
1198
+ }
1199
+ }
1200
+ } else {
1201
+ eprintln!(
1202
+ "[WSE] Received E: frame but no encryption key for {}",
1203
+ conn_id
1204
+ );
1205
+ }
1206
+ continue;
1207
+ }
1208
+
1013
1209
  // Check if this connection uses msgpack (lock-free DashMap read)
1014
1210
  let is_msgpack = state
1015
1211
  .conn_formats
@@ -1097,6 +1293,7 @@ async fn handle_connection(stream: TcpStream, addr: SocketAddr, state: Arc<Share
1097
1293
  state.conn_formats.remove(&*conn_id);
1098
1294
  state.conn_rates.remove(&*conn_id);
1099
1295
  state.conn_last_activity.remove(&*conn_id);
1296
+ state.conn_encryption.remove(&*conn_id);
1100
1297
  // If zombie detection already removed this connection and emitted
1101
1298
  // a disconnect event, skip the rest to avoid duplicate events.
1102
1299
  if already_removed {
@@ -1340,34 +1537,139 @@ async fn process_commands(
1340
1537
  }
1341
1538
  }
1342
1539
  ServerCommand::BroadcastText { data } => {
1343
- let frame = WsFrame::PreFramed(pre_frame_text(&data));
1344
- let senders: Vec<mpsc::UnboundedSender<WsFrame>> = {
1540
+ if state.conn_encryption.is_empty() {
1541
+ // Fast path: no encrypted connections, zero overhead
1542
+ let frame = WsFrame::PreFramed(pre_frame_text(&data));
1543
+ let senders: Vec<mpsc::UnboundedSender<WsFrame>> = {
1544
+ let guard = state.connections.read().await;
1545
+ guard.values().map(|h| h.tx.clone()).collect()
1546
+ };
1547
+ fanout_to_senders(senders, frame);
1548
+ } else {
1549
+ let preframed = pre_frame_text(&data);
1345
1550
  let guard = state.connections.read().await;
1346
- guard.values().map(|h| h.tx.clone()).collect()
1347
- };
1348
- fanout_to_senders(senders, frame);
1551
+ let mut plain_senders = Vec::new();
1552
+ let mut enc_senders = Vec::new();
1553
+ for (cid, h) in guard.iter() {
1554
+ if let Some(cipher_ref) = state.conn_encryption.get(cid) {
1555
+ enc_senders.push((h.tx.clone(), cipher_ref.clone()));
1556
+ } else {
1557
+ plain_senders.push(h.tx.clone());
1558
+ }
1559
+ }
1560
+ drop(guard);
1561
+ if !plain_senders.is_empty() {
1562
+ fanout_to_senders(plain_senders, WsFrame::PreFramed(preframed));
1563
+ }
1564
+ for (tx, cipher) in &enc_senders {
1565
+ if let Ok(enc) = encrypt_outbound(cipher, data.as_bytes()) {
1566
+ let mut buf = Vec::with_capacity(2 + enc.len());
1567
+ buf.extend_from_slice(b"E:");
1568
+ buf.extend_from_slice(&enc);
1569
+ let _ = tx.send(WsFrame::Msg(Message::Binary(buf.into())));
1570
+ }
1571
+ }
1572
+ }
1349
1573
  }
1350
1574
  ServerCommand::BroadcastBytes { data } => {
1351
- let frame = WsFrame::PreFramed(pre_frame_binary(&data));
1352
- let senders: Vec<mpsc::UnboundedSender<WsFrame>> = {
1575
+ if state.conn_encryption.is_empty() {
1576
+ let frame = WsFrame::PreFramed(pre_frame_binary(&data));
1577
+ let senders: Vec<mpsc::UnboundedSender<WsFrame>> = {
1578
+ let guard = state.connections.read().await;
1579
+ guard.values().map(|h| h.tx.clone()).collect()
1580
+ };
1581
+ fanout_to_senders(senders, frame);
1582
+ } else {
1583
+ let preframed = pre_frame_binary(&data);
1353
1584
  let guard = state.connections.read().await;
1354
- guard.values().map(|h| h.tx.clone()).collect()
1355
- };
1356
- fanout_to_senders(senders, frame);
1585
+ let mut plain_senders = Vec::new();
1586
+ let mut enc_senders = Vec::new();
1587
+ for (cid, h) in guard.iter() {
1588
+ if let Some(cipher_ref) = state.conn_encryption.get(cid) {
1589
+ enc_senders.push((h.tx.clone(), cipher_ref.clone()));
1590
+ } else {
1591
+ plain_senders.push(h.tx.clone());
1592
+ }
1593
+ }
1594
+ drop(guard);
1595
+ if !plain_senders.is_empty() {
1596
+ fanout_to_senders(plain_senders, WsFrame::PreFramed(preframed));
1597
+ }
1598
+ for (tx, cipher) in &enc_senders {
1599
+ if let Ok(enc) = encrypt_outbound(cipher, &data) {
1600
+ let mut buf = Vec::with_capacity(2 + enc.len());
1601
+ buf.extend_from_slice(b"E:");
1602
+ buf.extend_from_slice(&enc);
1603
+ let _ = tx.send(WsFrame::Msg(Message::Binary(buf.into())));
1604
+ }
1605
+ }
1606
+ }
1357
1607
  }
1358
1608
  ServerCommand::BroadcastLocal { topic, data } => {
1359
- let frame = WsFrame::PreFramed(pre_frame_text(&data));
1609
+ let preframed = pre_frame_text(&data);
1360
1610
  // Store in recovery buffer (zero-copy: Bytes is Arc-refcounted)
1361
- if let Some(ref recovery) = state.recovery
1362
- && let WsFrame::PreFramed(ref bytes) = frame
1363
- {
1364
- recovery.push(&topic, bytes.clone());
1611
+ if let Some(ref recovery) = state.recovery {
1612
+ recovery.push(&topic, preframed.clone());
1365
1613
  }
1366
- let senders = {
1614
+ if state.conn_encryption.is_empty() {
1615
+ // Fast path: no encrypted connections
1616
+ let senders = {
1617
+ let guard = state.connections.read().await;
1618
+ collect_topic_senders(&guard, &state.topic_subscribers, &topic)
1619
+ };
1620
+ fanout_to_senders(senders, WsFrame::PreFramed(preframed));
1621
+ } else {
1367
1622
  let guard = state.connections.read().await;
1368
- collect_topic_senders(&guard, &state.topic_subscribers, &topic)
1369
- };
1370
- fanout_to_senders(senders, frame);
1623
+ let mut plain_senders = Vec::new();
1624
+ let mut enc_senders = Vec::new();
1625
+ let mut seen = AHashSet::new();
1626
+ if let Some(subs) = state.topic_subscribers.get(&topic) {
1627
+ for cid_ref in subs.iter() {
1628
+ let cid = cid_ref.key().clone();
1629
+ if seen.insert(cid.clone())
1630
+ && let Some(h) = guard.get(&cid)
1631
+ {
1632
+ if let Some(cipher_ref) = state.conn_encryption.get(&cid) {
1633
+ enc_senders.push((h.tx.clone(), cipher_ref.clone()));
1634
+ } else {
1635
+ plain_senders.push(h.tx.clone());
1636
+ }
1637
+ }
1638
+ }
1639
+ }
1640
+ // Glob pattern matching
1641
+ for entry in state.topic_subscribers.iter() {
1642
+ let pattern = entry.key();
1643
+ if (pattern.contains('*') || pattern.contains('?'))
1644
+ && glob_match(pattern, &topic)
1645
+ {
1646
+ for cid_ref in entry.value().iter() {
1647
+ let cid = cid_ref.key().clone();
1648
+ if seen.insert(cid.clone())
1649
+ && let Some(h) = guard.get(&cid)
1650
+ {
1651
+ if let Some(cipher_ref) = state.conn_encryption.get(&cid) {
1652
+ enc_senders.push((h.tx.clone(), cipher_ref.clone()));
1653
+ } else {
1654
+ plain_senders.push(h.tx.clone());
1655
+ }
1656
+ }
1657
+ }
1658
+ }
1659
+ }
1660
+ drop(guard);
1661
+ if !plain_senders.is_empty() {
1662
+ fanout_to_senders(plain_senders, WsFrame::PreFramed(preframed));
1663
+ }
1664
+ for (tx, cipher) in &enc_senders {
1665
+ if let Ok(enc) = encrypt_outbound(cipher, data.as_bytes()) {
1666
+ let mut buf = Vec::with_capacity(2 + enc.len());
1667
+ buf.extend_from_slice(b"E:");
1668
+ buf.extend_from_slice(&enc);
1669
+ let _ = tx.send(WsFrame::Msg(Message::Binary(buf.into())));
1670
+ }
1671
+ }
1672
+ }
1371
1673
  }
1372
1674
  ServerCommand::Disconnect { conn_id } => {
1373
1675
  let guard = state.connections.read().await;
@@ -1753,6 +2055,7 @@ impl RustWSEServer {
1753
2055
  ping_state.conn_formats.remove(cid);
1754
2056
  ping_state.conn_rates.remove(cid);
1755
2057
  ping_state.conn_last_activity.remove(cid);
2058
+ ping_state.conn_encryption.remove(cid);
1756
2059
  }
1757
2060
  ping_state.connection_count.store(
1758
2061
  conns.len(),
@@ -2110,6 +2413,9 @@ impl RustWSEServer {
2110
2413
  .map(|v| *v)
2111
2414
  .unwrap_or(false);
2112
2415
 
2416
+ // Check if this connection has E2E encryption
2417
+ let conn_cipher = self.shared.conn_encryption.get(conn_id);
2418
+
2113
2419
  let byte_count;
2114
2420
 
2115
2421
  if use_msgpack {
@@ -2121,6 +2427,19 @@ impl RustWSEServer {
2121
2427
  let mut final_bytes = Vec::with_capacity(buf.len() + 2);
2122
2428
  final_bytes.extend_from_slice(b"M:");
2123
2429
  final_bytes.extend_from_slice(&buf);
2430
+
2431
+ // Encrypt if E2E enabled for this connection
2432
+ let final_bytes = if let Some(ref cipher) = conn_cipher {
2433
+ let encrypted = encrypt_outbound(cipher, &final_bytes)
2434
+ .map_err(|e| PyRuntimeError::new_err(format!("Encrypt error: {e}")))?;
2435
+ let mut enc_buf = Vec::with_capacity(2 + encrypted.len());
2436
+ enc_buf.extend_from_slice(b"E:");
2437
+ enc_buf.extend_from_slice(&encrypted);
2438
+ enc_buf
2439
+ } else {
2440
+ final_bytes
2441
+ };
2442
+
2124
2443
  byte_count = final_bytes.len();
2125
2444
  cmd_tx
2126
2445
  .send(ServerCommand::SendPrebuilt {
@@ -2137,7 +2456,21 @@ impl RustWSEServer {
2137
2456
  let payload_str = format!("{}{}", category, json_str);
2138
2457
  let payload_bytes = payload_str.as_bytes();
2139
2458
 
2140
- if compression_threshold > 0 && payload_bytes.len() > compression_threshold {
2459
+ if let Some(ref cipher) = conn_cipher {
2460
+ // E2E encrypted: encrypt plaintext payload, send as E: prefix binary
2461
+ let encrypted = encrypt_outbound(cipher, payload_bytes)
2462
+ .map_err(|e| PyRuntimeError::new_err(format!("Encrypt error: {e}")))?;
2463
+ let mut final_bytes = Vec::with_capacity(2 + encrypted.len());
2464
+ final_bytes.extend_from_slice(b"E:");
2465
+ final_bytes.extend_from_slice(&encrypted);
2466
+ byte_count = final_bytes.len();
2467
+ cmd_tx
2468
+ .send(ServerCommand::SendPrebuilt {
2469
+ conn_id: conn_id.to_owned(),
2470
+ message: Message::Binary(final_bytes.into()),
2471
+ })
2472
+ .map_err(|_| PyRuntimeError::new_err("Channel closed"))?;
2473
+ } else if compression_threshold > 0 && payload_bytes.len() > compression_threshold {
2141
2474
  // Compressed: C: prefix + zlib (binary frame)
2142
2475
  let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
2143
2476
  encoder
@@ -4,6 +4,9 @@ from wse_server._wse_accel import (
4
4
  RustCompressionManager,
5
5
  RustEventSequencer,
6
6
  RustPriorityMessageQueue,
7
+ RustPriorityQueue,
8
+ RustSequencer,
9
+ RustTokenBucket,
7
10
  RustWSEServer,
8
11
  rust_aes_gcm_decrypt,
9
12
  rust_aes_gcm_encrypt,
@@ -16,19 +19,23 @@ from wse_server._wse_accel import (
16
19
  rust_jwt_encode,
17
20
  rust_match_event,
18
21
  rust_sha256,
22
+ rust_should_compress,
19
23
  rust_sign_message,
20
24
  rust_transform_event,
21
25
  )
22
26
  from wse_server.core.filters import EventFilter
23
27
  from wse_server.core.types import DeliveryGuarantee, EventPriority
24
28
 
25
- __version__ = "2.0.0"
29
+ __version__ = "2.0.2"
26
30
 
27
31
  __all__ = [
28
32
  "RustWSEServer",
29
33
  "RustCompressionManager",
30
34
  "RustEventSequencer",
31
35
  "RustPriorityMessageQueue",
36
+ "RustPriorityQueue",
37
+ "RustSequencer",
38
+ "RustTokenBucket",
32
39
  "rust_jwt_encode",
33
40
  "rust_jwt_decode",
34
41
  "rust_aes_gcm_encrypt",
@@ -39,6 +46,7 @@ __all__ = [
39
46
  "rust_sha256",
40
47
  "rust_compress",
41
48
  "rust_decompress",
49
+ "rust_should_compress",
42
50
  "rust_sign_message",
43
51
  "rust_match_event",
44
52
  "rust_transform_event",
@@ -0,0 +1,455 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ # =============================================================================
7
+ # Compression
8
+ # =============================================================================
9
+
10
+ def rust_compress(data: bytes, level: int = 6) -> bytes:
11
+ """Compress data using zlib at the given compression level."""
12
+ ...
13
+
14
+ def rust_decompress(data: bytes) -> bytes:
15
+ """Decompress zlib-compressed data."""
16
+ ...
17
+
18
+ def rust_should_compress(data: bytes, threshold: int = 1024) -> bool:
19
+ """Return True if data length exceeds the compression threshold."""
20
+ ...
21
+
22
+ # =============================================================================
23
+ # Event Transform & Filter
24
+ # =============================================================================
25
+
26
+ def rust_transform_event(
27
+ event: dict[str, Any], sequence: int, event_type_map: dict[str, Any]
28
+ ) -> dict[str, Any]:
29
+ """Transform a raw event dict into a WSE wire envelope."""
30
+ ...
31
+
32
+ def rust_match_event(event: dict[str, Any], criteria: dict[str, Any]) -> bool:
33
+ """Match an event against MongoDB-style filter criteria."""
34
+ ...
35
+
36
+ # =============================================================================
37
+ # HMAC / SHA-256
38
+ # =============================================================================
39
+
40
+ def rust_hmac_sha256(key: bytes, data: bytes) -> bytes:
41
+ """Compute HMAC-SHA256."""
42
+ ...
43
+
44
+ def rust_sha256(data: bytes) -> str:
45
+ """Compute SHA-256 hash, returned as a hex string."""
46
+ ...
47
+
48
+ def rust_sign_message(payload_json: str, secret: bytes) -> str:
49
+ """Sign a JSON payload string with HMAC-SHA256."""
50
+ ...
51
+
52
+ # =============================================================================
53
+ # AES-GCM-256 Encryption
54
+ # =============================================================================
55
+
56
+ def rust_aes_gcm_encrypt(key: bytes, plaintext: bytes) -> bytes:
57
+ """Encrypt with AES-GCM-256. Returns 12-byte IV + ciphertext + 16-byte tag."""
58
+ ...
59
+
60
+ def rust_aes_gcm_decrypt(key: bytes, data: bytes) -> bytes:
61
+ """Decrypt AES-GCM-256 ciphertext. Input: 12-byte IV + ciphertext + tag."""
62
+ ...
63
+
64
+ # =============================================================================
65
+ # ECDH P-256 Key Exchange
66
+ # =============================================================================
67
+
68
+ def rust_ecdh_generate_keypair() -> tuple[bytes, bytes]:
69
+ """Generate an ECDH P-256 keypair. Returns (private_key, public_key_sec1)."""
70
+ ...
71
+
72
+ def rust_ecdh_derive_shared_secret(private_key: bytes, peer_public_key: bytes) -> bytes:
73
+ """Derive a 32-byte AES key via ECDH + HKDF-SHA256."""
74
+ ...
75
+
76
+ # =============================================================================
77
+ # JWT
78
+ # =============================================================================
79
+
80
+ def rust_jwt_encode(claims: dict[str, Any], secret: bytes, algorithm: str = "HS256") -> str:
81
+ """Encode claims into a JWT (HS256)."""
82
+ ...
83
+
84
+ def rust_jwt_decode(
85
+ token: str,
86
+ secret: bytes,
87
+ algorithm: str = "HS256",
88
+ issuer: str | None = None,
89
+ audience: str | None = None,
90
+ ) -> dict[str, Any] | None:
91
+ """Decode and validate a JWT. Returns None on failure."""
92
+ ...
93
+
94
+ # =============================================================================
95
+ # RustCompressionManager
96
+ # =============================================================================
97
+
98
+ class RustCompressionManager:
99
+ """Zlib compression manager with optional msgpack support."""
100
+
101
+ def __init__(self, threshold: int = 1024, compression_level: int = 6) -> None: ...
102
+ def should_compress(self, data: bytes) -> bool:
103
+ """Return True if data exceeds the compression threshold."""
104
+ ...
105
+ def compress(self, data: bytes, level: int | None = None) -> bytes:
106
+ """Compress data with zlib."""
107
+ ...
108
+ def decompress(self, data: bytes) -> bytes:
109
+ """Decompress zlib data."""
110
+ ...
111
+ def pack_msgpack(self, data: dict[str, Any]) -> bytes:
112
+ """Serialize dict to msgpack bytes."""
113
+ ...
114
+ def unpack_msgpack(self, data: bytes) -> Any:
115
+ """Deserialize msgpack bytes."""
116
+ ...
117
+ def pack_event(self, event: dict[str, Any], use_msgpack: bool = True) -> bytes:
118
+ """Pack event as msgpack (with JSON fallback)."""
119
+ ...
120
+ def unpack_event(self, data: bytes, is_msgpack: bool = True) -> Any:
121
+ """Unpack event from msgpack (with JSON fallback)."""
122
+ ...
123
+ def get_stats(self) -> dict[str, int | float]:
124
+ """Return compression statistics.
125
+
126
+ Keys: total_compressed, total_decompressed, compression_failures,
127
+ decompression_failures, total_bytes_saved (int),
128
+ compression_success_rate, decompression_success_rate,
129
+ average_bytes_saved (float).
130
+ """
131
+ ...
132
+ def reset_stats(self) -> None:
133
+ """Reset all compression statistics to zero."""
134
+ ...
135
+
136
+ # =============================================================================
137
+ # RustTokenBucket
138
+ # =============================================================================
139
+
140
+ class RustTokenBucket:
141
+ """Token bucket rate limiter."""
142
+
143
+ def __init__(
144
+ self,
145
+ capacity: float,
146
+ refill_rate: float,
147
+ initial_tokens: float | None = None,
148
+ ) -> None: ...
149
+ def acquire(self, tokens: float = 1.0) -> bool:
150
+ """Try to acquire tokens. Returns True if successful."""
151
+ ...
152
+ @property
153
+ def tokens(self) -> float:
154
+ """Current token count after refill."""
155
+ ...
156
+ def reset(self) -> None:
157
+ """Reset bucket to full capacity."""
158
+ ...
159
+
160
+ # =============================================================================
161
+ # RustSequencer
162
+ # =============================================================================
163
+
164
+ class RustSequencer:
165
+ """Monotonic sequence counter with duplicate detection."""
166
+
167
+ def __init__(self, window_size: int) -> None: ...
168
+ def next_seq(self) -> int:
169
+ """Increment and return the next sequence number."""
170
+ ...
171
+ def get_current_seq(self) -> int:
172
+ """Get current sequence number without incrementing."""
173
+ ...
174
+ def is_duplicate(self, event_id: str) -> bool:
175
+ """Check if event ID has been seen."""
176
+ ...
177
+ def cleanup(self, max_age_secs: float) -> None:
178
+ """Remove entries older than max_age_secs."""
179
+ ...
180
+ def seen_count(self) -> int:
181
+ """Number of tracked event IDs."""
182
+ ...
183
+
184
+ # =============================================================================
185
+ # RustEventSequencer
186
+ # =============================================================================
187
+
188
+ class RustEventSequencer:
189
+ """Event sequencer with dedup and out-of-order buffering."""
190
+
191
+ def __init__(self, window_size: int = 10000, max_out_of_order: int = 100) -> None: ...
192
+ def next_seq(self) -> int:
193
+ """Increment and return the next sequence number."""
194
+ ...
195
+ def get_current_sequence(self) -> int:
196
+ """Get current sequence number."""
197
+ ...
198
+ def is_duplicate(self, event_id: str) -> bool:
199
+ """Check if event ID has been seen."""
200
+ ...
201
+ def process_sequenced_event(self, topic: str, sequence: int, event: Any) -> list[Any] | None:
202
+ """Process a sequenced event. Returns deliverable events or None if buffered."""
203
+ ...
204
+ def cleanup(self) -> None:
205
+ """Clean up buffered events older than 5 minutes and trim seen_ids."""
206
+ ...
207
+ def get_buffer_stats(self) -> dict[str, Any]:
208
+ """Get statistics about buffered events per topic."""
209
+ ...
210
+ def get_sequence_stats(self) -> dict[str, Any]:
211
+ """Get detailed sequence statistics including per-topic info."""
212
+ ...
213
+ def reset_sequence(self, topic: str | None = None) -> None:
214
+ """Reset sequence tracking for a specific topic or all topics."""
215
+ ...
216
+
217
+ # =============================================================================
218
+ # RustPriorityQueue
219
+ # =============================================================================
220
+
221
+ class RustPriorityQueue:
222
+ """Bounded priority queue (binary heap)."""
223
+
224
+ def __init__(self, max_size: int) -> None: ...
225
+ def push(self, priority: int, message: Any) -> bool:
226
+ """Push a message. Returns False if queue is full."""
227
+ ...
228
+ def pop(self) -> Any | None:
229
+ """Pop the highest-priority message."""
230
+ ...
231
+ def drain(self, max_count: int) -> list[Any]:
232
+ """Drain up to max_count messages in priority order."""
233
+ ...
234
+ def len(self) -> int: ...
235
+ def is_full(self) -> bool: ...
236
+ def clear(self) -> None: ...
237
+
238
+ # =============================================================================
239
+ # RustPriorityMessageQueue
240
+ # =============================================================================
241
+
242
+ class RustPriorityMessageQueue:
243
+ """Priority message queue with stats and batch dequeue."""
244
+
245
+ def __init__(self, max_size: int = 1000, batch_size: int = 10) -> None: ...
246
+ def enqueue(self, message: Any, priority: int = 5) -> bool:
247
+ """Enqueue a message. Priorities: BACKGROUND=1, LOW=3, NORMAL=5, HIGH=8, CRITICAL=10."""
248
+ ...
249
+ def dequeue_batch(self) -> list[tuple[int, Any]]:
250
+ """Dequeue a batch of (priority, message) tuples."""
251
+ ...
252
+ @property
253
+ def size(self) -> int:
254
+ """Current total message count."""
255
+ ...
256
+ def get_stats(self) -> dict[str, Any]:
257
+ """Queue statistics.
258
+
259
+ Keys: size (int), capacity (int), utilization_percent (float),
260
+ priority_distribution (dict), priority_queue_depths (dict),
261
+ dropped_by_priority (dict), total_dropped (int),
262
+ backpressure (bool), oldest_message_age (float | None),
263
+ processing_rate (float).
264
+ """
265
+ ...
266
+ def clear(self) -> None: ...
267
+
268
+ # =============================================================================
269
+ # RustWSEServer
270
+ # =============================================================================
271
+
272
+ class RustWSEServer:
273
+ """Standalone Rust WebSocket server controlled from Python.
274
+
275
+ All transport (TCP accept, framing, JWT auth, compression, encryption,
276
+ ping/pong) runs in Rust. Python handles application logic via drain mode
277
+ or callbacks.
278
+ """
279
+
280
+ def __init__(
281
+ self,
282
+ host: str,
283
+ port: int,
284
+ max_connections: int = 1000,
285
+ jwt_secret: bytes | None = None,
286
+ jwt_issuer: str | None = None,
287
+ jwt_audience: str | None = None,
288
+ max_inbound_queue_size: int = 131072,
289
+ recovery_enabled: bool = False,
290
+ recovery_buffer_size: int = 128,
291
+ recovery_ttl: int = 300,
292
+ recovery_max_messages: int = 500,
293
+ recovery_memory_budget: int = 268435456,
294
+ presence_enabled: bool = False,
295
+ presence_max_data_size: int = 4096,
296
+ presence_max_members: int = 0,
297
+ ) -> None: ...
298
+
299
+ # -- Lifecycle ------------------------------------------------------------
300
+
301
+ def start(self) -> None:
302
+ """Start the WebSocket server."""
303
+ ...
304
+ def stop(self) -> None:
305
+ """Stop the server and close all connections."""
306
+ ...
307
+ def is_running(self) -> bool:
308
+ """Return True if the server is running."""
309
+ ...
310
+
311
+ # -- Callbacks (alternative to drain mode) --------------------------------
312
+
313
+ def set_callbacks(
314
+ self,
315
+ on_connect: Callable[[str, str], Any],
316
+ on_message: Callable[[str, dict[str, Any]], Any],
317
+ on_disconnect: Callable[[str], Any],
318
+ ) -> None:
319
+ """Set callback functions for connect, message, and disconnect events."""
320
+ ...
321
+
322
+ # -- Drain Mode -----------------------------------------------------------
323
+
324
+ def enable_drain_mode(self) -> None:
325
+ """Enable drain mode. Events are queued for batch retrieval."""
326
+ ...
327
+ def drain_inbound(
328
+ self, max_count: int = 256, timeout_ms: int = 50
329
+ ) -> list[tuple[str, str | None, Any]]:
330
+ """Drain queued inbound events.
331
+
332
+ Returns list of (event_type, conn_id, data) tuples.
333
+ Event types: "connect", "auth_connect", "msg", "raw", "bin",
334
+ "disconnect", "presence_join", "presence_leave".
335
+ """
336
+ ...
337
+ def inbound_dropped_count(self) -> int:
338
+ """Number of inbound events dropped due to queue overflow."""
339
+ ...
340
+ def inbound_queue_depth(self) -> int:
341
+ """Current number of events in the inbound queue."""
342
+ ...
343
+
344
+ # -- Send -----------------------------------------------------------------
345
+
346
+ def send(self, conn_id: str, data: str) -> None:
347
+ """Send a text frame to a connection."""
348
+ ...
349
+ def send_bytes(self, conn_id: str, data: bytes) -> None:
350
+ """Send a binary frame to a connection."""
351
+ ...
352
+ def send_event(
353
+ self,
354
+ conn_id: str,
355
+ event: dict[str, Any],
356
+ compression_threshold: int = 1024,
357
+ ) -> int:
358
+ """Send an event through the full outbound pipeline. Returns bytes sent."""
359
+ ...
360
+
361
+ # -- Broadcast ------------------------------------------------------------
362
+
363
+ def broadcast_all(self, data: str) -> None:
364
+ """Send text to all connected clients (no topic filtering)."""
365
+ ...
366
+ def broadcast_all_bytes(self, data: bytes) -> None:
367
+ """Send binary to all connected clients."""
368
+ ...
369
+ def broadcast_local(self, topic: str, data: str) -> None:
370
+ """Fan-out to local topic subscribers only (no cluster)."""
371
+ ...
372
+ def broadcast(self, topic: str, data: str) -> None:
373
+ """Fan-out to topic subscribers locally and across cluster peers."""
374
+ ...
375
+
376
+ # -- Connection Management ------------------------------------------------
377
+
378
+ def get_connection_count(self) -> int:
379
+ """Number of active connections (lock-free AtomicUsize read)."""
380
+ ...
381
+ def get_connections(self) -> list[str]:
382
+ """List of active connection IDs."""
383
+ ...
384
+ def disconnect(self, conn_id: str) -> None:
385
+ """Force-disconnect a connection."""
386
+ ...
387
+
388
+ # -- Subscriptions --------------------------------------------------------
389
+
390
+ def subscribe_connection(
391
+ self,
392
+ conn_id: str,
393
+ topics: list[str],
394
+ presence_data: dict[str, Any] | None = None,
395
+ ) -> None:
396
+ """Subscribe a connection to topics. Optionally set initial presence data."""
397
+ ...
398
+ def subscribe_with_recovery(
399
+ self,
400
+ conn_id: str,
401
+ topics: list[str],
402
+ recover: bool = False,
403
+ epoch: str | None = None,
404
+ offset: int | None = None,
405
+ ) -> dict[str, Any]:
406
+ """Subscribe with message recovery. Returns per-topic recovery status."""
407
+ ...
408
+ def unsubscribe_connection(self, conn_id: str, topics: list[str] | None = None) -> None:
409
+ """Unsubscribe from topics. If topics is None, unsubscribe from all."""
410
+ ...
411
+ def get_topic_subscriber_count(self, topic: str) -> int:
412
+ """Number of connections subscribed to a topic."""
413
+ ...
414
+
415
+ # -- Presence -------------------------------------------------------------
416
+
417
+ def presence(self, topic: str) -> dict[str, dict[str, Any]]:
418
+ """Get presence data for a topic. Returns {user_id: {data, connections}}."""
419
+ ...
420
+ def presence_stats(self, topic: str) -> dict[str, int]:
421
+ """Get presence counts for a topic. Returns {num_users, num_connections}."""
422
+ ...
423
+ def update_presence(self, conn_id: str, data: dict[str, Any]) -> None:
424
+ """Update presence metadata for a connection across all its topics."""
425
+ ...
426
+
427
+ # -- Cluster --------------------------------------------------------------
428
+
429
+ def connect_cluster(
430
+ self,
431
+ peers: list[str],
432
+ tls_cert: str | None = None,
433
+ tls_key: str | None = None,
434
+ tls_ca: str | None = None,
435
+ cluster_port: int | None = None,
436
+ seeds: list[str] | None = None,
437
+ cluster_addr: str | None = None,
438
+ ) -> None:
439
+ """Connect to cluster peers via TCP mesh protocol."""
440
+ ...
441
+ def cluster_connected(self) -> bool:
442
+ """Return True if at least one cluster peer is connected."""
443
+ ...
444
+ def cluster_peers_count(self) -> int:
445
+ """Number of connected cluster peers."""
446
+ ...
447
+
448
+ # -- Health / Monitoring --------------------------------------------------
449
+
450
+ def health_snapshot(self) -> dict[str, Any]:
451
+ """Full server health metrics: connections, queue, cluster, recovery, presence, uptime."""
452
+ ...
453
+ def get_cluster_dlq_entries(self) -> list[dict[str, Any]]:
454
+ """Drain and return dead-letter-queue entries from failed cluster sends."""
455
+ ...
File without changes
File without changes
File without changes