From 682e293b36a13e4bfe41a3e262542184961c1547 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 19 May 2025 17:44:13 +0200 Subject: [PATCH 01/23] binary format messages on rust side --- Cargo.toml | 2 +- zenoh-plugin-remote-api/src/interface/mod.rs | 1 + .../src/interface/remote_message.rs | 717 ++++++++++++++++++ 3 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 zenoh-plugin-remote-api/src/interface/remote_message.rs diff --git a/Cargo.toml b/Cargo.toml index cc68424f..0dba42c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ serde = { version = "1.0.210", default-features = false, features = [ ] } # Default features are disabled due to usage in no_std crates serde_json = "1.0.128" zenoh = { git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = [ - "plugins", + "plugins", "internal" ], version = "1.3.4" } zenoh-ext = { git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", version = "1.3.4" } zenoh_backend_traits = { git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", version = "1.3.4" } diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index aadc9a6d..9dd9b9f0 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -13,6 +13,7 @@ // use std::sync::Arc; +pub(crate) mod remote_message; // mod interface::ser_de; pub(crate) mod ser_de; diff --git a/zenoh-plugin-remote-api/src/interface/remote_message.rs b/zenoh-plugin-remote-api/src/interface/remote_message.rs new file mode 100644 index 00000000..5830589c --- /dev/null +++ b/zenoh-plugin-remote-api/src/interface/remote_message.rs @@ -0,0 +1,717 @@ +// +// Copyright (c) 2025 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use core::str; +use std::{ops::Not, str::FromStr}; + +use uhlc::{Timestamp, NTP64}; +use uuid::Uuid; +use zenoh::{ + bytes::{Encoding, ZBytes}, + config::ZenohId, + key_expr::OwnedKeyExpr, + qos::{CongestionControl, Priority, Reliability}, + query::{ConsolidationMode, QueryTarget, ReplyKeyExpr}, + sample::Locality, +}; + +use zenoh_ext::{Deserialize, Serialize, ZDeserializeError, ZDeserializer, ZSerializer}; +use zenoh_result::bail; + +pub(crate) fn serialize_option(serializer: &mut ZSerializer, o: &Option) { + match o { + Some(v) => { + serializer.serialize(true); + serializer.serialize(v); + } + None => { + serializer.serialize(false); + } + } +} + +pub(crate) fn deserialize_option( + deserializer: &mut zenoh_ext::ZDeserializer, +) -> Result, ZDeserializeError> { + let has_value = deserializer.deserialize::()?; + match has_value { + true => Ok(Some(deserializer.deserialize()?)), + false => Ok(None), + } +} + +pub(crate) struct OpenSession { + pub(crate) id: Uuid, +} + +impl OpenSession { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(OpenSession { + id: Uuid::from_str(&deserializer.deserialize::()?)?, + }) + } +} + +pub(crate) struct CloseSession { + pub(crate) id: Uuid, +} + +impl CloseSession { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(CloseSession { + id: Uuid::from_str(&deserializer.deserialize::()?)?, + }) + } +} + +pub(crate) struct ResultWithId { + pub(crate) id: T, + pub(crate) error: Option, +} + +impl ResultWithId { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(&self.id); + serialize_option(&mut serializer, &self.error); + serializer.finish() + } +} + +fn congestion_control_from_u8(c: u8) -> Result { + match c { + 0 => Ok(CongestionControl::Drop), + 1 => Ok(CongestionControl::Block), + v => bail!("Unsupported congestion control value {}", v), + } +} + +fn congestion_control_to_u8(c: CongestionControl) -> u8 { + match c { + CongestionControl::Drop => 0, + CongestionControl::Block => 1, + } +} + +fn priority_from_u8(p: u8) -> Result { + p.try_into() +} + +fn priority_to_u8(p: Priority) -> u8 { + p as u8 +} + +fn reliability_from_u8(r: u8) -> Result { + match r { + 0 => Ok(Reliability::BestEffort), + 1 => Ok(Reliability::Reliable), + v => bail!("Unsupported reliability value {}", v), + } +} + +fn reliability_to_u8(r: Reliability) -> u8 { + match r { + Reliability::BestEffort => 0, + Reliability::Reliable => 1, + } +} + +fn locality_from_u8(l: u8) -> Result { + match l { + 0 => Ok(Locality::Any), + 1 => Ok(Locality::Remote), + 2 => Ok(Locality::SessionLocal), + v => bail!("Unsupported locality value {}", v), + } +} + +fn consolidation_from_u8(l: u8) -> Result { + match l { + 0 => Ok(ConsolidationMode::Auto), + 1 => Ok(ConsolidationMode::None), + 2 => Ok(ConsolidationMode::Monotonic), + 3 => Ok(ConsolidationMode::Latest), + v => bail!("Unsupported consolidation mode value {}", v), + } +} + +fn query_target_from_u8(t: u8) -> Result { + match t { + 0 => Ok(QueryTarget::All), + 1 => Ok(QueryTarget::AllComplete), + 2 => Ok(QueryTarget::BestMatching), + v => bail!("Unsupported query target value {}", v), + } +} + +fn reply_keyexpr_from_u8(a: u8) -> Result { + match a { + 0 => Ok(ReplyKeyExpr::Any), + 1 => Ok(ReplyKeyExpr::MatchingQuery), + v => bail!("Unsupported reply keyexpr value {}", v), + } +} + +fn reply_keyexpr_to_u8(a: ReplyKeyExpr) -> u8 { + match a { + ReplyKeyExpr::Any => 0, + ReplyKeyExpr::MatchingQuery => 1, + } +} + +fn encoding_from_id_schema(id_schema: (u16, String)) -> Encoding { + Encoding::new( + id_schema.0, + id_schema + .1 + .is_empty() + .not() + .then(|| id_schema.1.into_bytes().into()), + ) +} + +fn encoding_to_id_schema(encoding: &Encoding) -> (u16, &[u8]) { + ( + encoding.id(), + match encoding.schema() { + Some(s) => s.as_slice(), + None => &[], + }, + ) +} + +fn opt_encoding_from_id_schema(id_schema: Option<(u16, String)>) -> Option { + id_schema.map(|x| encoding_from_id_schema(x)) +} + +fn opt_timestamp_from_ntp_id( + ntp_id: Option<(u64, [u8; 16])>, +) -> Result, zenoh_result::Error> { + Ok(match ntp_id { + Some((t, id)) => Some(Timestamp::new(NTP64(t), id.try_into()?)), + None => None, + }) +} + +fn timestamp_to_ntp_id(t: &Timestamp) -> (u64, [u8; 16]) { + (t.get_time().0, t.get_id().to_le_bytes()) +} + +pub(crate) struct DeclarePublisher { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) encoding: Encoding, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, + pub(crate) reliability: Reliability, + pub(crate) allowed_destination: Locality, +} + +impl DeclarePublisher { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(DeclarePublisher { + id: deserializer.deserialize::()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + encoding: encoding_from_id_schema(deserializer.deserialize()?), + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + reliability: reliability_from_u8(deserializer.deserialize()?)?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct UndeclarePublisher { + pub(crate) id: u32, +} + +impl UndeclarePublisher { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(UndeclarePublisher { + id: deserializer.deserialize::()?, + }) + } +} + +pub(crate) struct DeclareSubscriber { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) allowed_origin: Locality, +} + +impl DeclareSubscriber { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(DeclareSubscriber { + id: deserializer.deserialize::()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + allowed_origin: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct UndeclareSubscriber { + pub(crate) id: u32, +} + +impl UndeclareSubscriber { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(UndeclareSubscriber { + id: deserializer.deserialize::()?, + }) + } +} + +pub(crate) struct DeclareQueryable { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) complete: bool, + pub(crate) allowed_origin: Locality, +} + +impl DeclareQueryable { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(DeclareQueryable { + id: deserializer.deserialize::()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + complete: deserializer.deserialize()?, + allowed_origin: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct UndeclareQueryable { + pub(crate) id: u32, +} + +impl UndeclareQueryable { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(UndeclareQueryable { + id: deserializer.deserialize::()?, + }) + } +} + +pub(crate) struct DeclareQuerier { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) target: QueryTarget, + pub(crate) accept_replies: ReplyKeyExpr, + pub(crate) timeout_ms: u64, + pub(crate) consolidation: ConsolidationMode, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, + pub(crate) reliability: Reliability, + pub(crate) allowed_destination: Locality, +} + +impl DeclareQuerier { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(DeclareQuerier { + id: deserializer.deserialize::()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + target: query_target_from_u8(deserializer.deserialize()?)?, + accept_replies: reply_keyexpr_from_u8(deserializer.deserialize()?)?, + timeout_ms: deserializer.deserialize()?, + consolidation: consolidation_from_u8(deserializer.deserialize()?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + reliability: reliability_from_u8(deserializer.deserialize()?)?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct UndeclareQuerier { + pub(crate) id: u32, +} + +impl UndeclareQuerier { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(UndeclareQuerier { + id: deserializer.deserialize::()?, + }) + } +} + +pub(crate) struct GetSessionInfo { + pub(crate) id: u32, +} + +impl GetSessionInfo { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(GetSessionInfo { + id: deserializer.deserialize::()?, + }) + } +} + +pub(crate) struct ResponseSessionInfo { + pub(crate) id: u32, + pub(crate) zid: ZenohId, + pub(crate) z_routers: Vec, + pub(crate) z_peers: Vec, +} + +impl ResponseSessionInfo { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(self.id); + serializer.serialize(self.zid.to_le_bytes()); + serializer.serialize_iter(self.z_routers.iter().map(|z| z.to_le_bytes())); + serializer.serialize_iter(self.z_peers.iter().map(|z| z.to_le_bytes())); + serializer.finish() + } +} + +pub(crate) struct GetTimestamp { + pub(crate) id: u32, +} + +impl GetTimestamp { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(GetTimestamp { + id: deserializer.deserialize::()?, + }) + } +} + +pub(crate) struct ResponseTimestamp { + pub(crate) id: u32, + pub(crate) timestamp: Timestamp, +} + +impl ResponseTimestamp { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(&self.id); + serializer.serialize(timestamp_to_ntp_id(&self.timestamp)); + serializer.finish() + } +} + +pub(crate) struct Put { + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) payload: Vec, + pub(crate) encoding: Encoding, + pub(crate) attachment: Option>, + pub(crate) timestamp: Option, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, + pub(crate) reliability: Reliability, + pub(crate) allowed_destination: Locality, +} + +impl Put { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(Put { + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + payload: deserializer.deserialize()?, + encoding: encoding_from_id_schema(deserializer.deserialize()?), + attachment: deserialize_option(&mut deserializer)?, + timestamp: opt_timestamp_from_ntp_id(deserialize_option(&mut deserializer)?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + reliability: reliability_from_u8(deserializer.deserialize()?)?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct PublisherPut { + pub(crate) publisher_id: u32, + pub(crate) payload: Vec, + pub(crate) encoding: Encoding, + pub(crate) attachment: Option>, + pub(crate) timestamp: Option, +} + +impl PublisherPut { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(PublisherPut { + publisher_id: deserializer.deserialize()?, + payload: deserializer.deserialize()?, + encoding: encoding_from_id_schema(deserializer.deserialize()?), + attachment: deserialize_option(&mut deserializer)?, + timestamp: opt_timestamp_from_ntp_id(deserialize_option(&mut deserializer)?)?, + }) + } +} + +pub(crate) struct Get { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) parameters: Option, + pub(crate) payload: Option>, + pub(crate) encoding: Option, + pub(crate) attachment: Option>, + pub(crate) target: QueryTarget, + pub(crate) accept_replies: ReplyKeyExpr, + pub(crate) timeout_ms: u64, + pub(crate) consolidation: ConsolidationMode, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, + pub(crate) reliability: Reliability, + pub(crate) allowed_destination: Locality, +} + +impl Get { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(Get { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + parameters: deserialize_option(&mut deserializer)?, + payload: deserialize_option(&mut deserializer)?, + encoding: opt_encoding_from_id_schema(deserialize_option(&mut deserializer)?), + attachment: deserialize_option(&mut deserializer)?, + target: query_target_from_u8(deserializer.deserialize()?)?, + accept_replies: reply_keyexpr_from_u8(deserializer.deserialize()?)?, + timeout_ms: deserializer.deserialize()?, + consolidation: consolidation_from_u8(deserializer.deserialize()?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + reliability: reliability_from_u8(deserializer.deserialize()?)?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct QuerierGet { + pub(crate) querier_id: u32, + pub(crate) id: u32, + pub(crate) parameters: Option, + pub(crate) payload: Option>, + pub(crate) encoding: Option, + pub(crate) attachment: Option>, +} + +impl QuerierGet { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(QuerierGet { + querier_id: deserializer.deserialize()?, + id: deserializer.deserialize()?, + parameters: deserialize_option(&mut deserializer)?, + payload: deserialize_option(&mut deserializer)?, + encoding: opt_encoding_from_id_schema(deserialize_option(&mut deserializer)?), + attachment: deserialize_option(&mut deserializer)?, + }) + } +} + +fn serialize_sample(serializer: &mut ZSerializer, sample: &zenoh::sample::Sample) { + serializer.serialize(sample.key_expr().as_str()); + serializer.serialize(sample.payload().to_bytes()); + serializer.serialize(encoding_to_id_schema(sample.encoding())); + serialize_option(serializer, &sample.attachment().map(|a| a.to_bytes())); + serialize_option( + serializer, + &sample.timestamp().map(|t| timestamp_to_ntp_id(t)), + ); + serializer.serialize(congestion_control_to_u8(sample.congestion_control())); + serializer.serialize(priority_to_u8(sample.priority())); + serializer.serialize(sample.express()); + serializer.serialize(reliability_to_u8(sample.reliability())); +} + +pub(crate) struct Sample { + pub(crate) subscriber_id: u32, + pub(crate) sample: zenoh::sample::Sample, +} + +impl Sample { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(&self.subscriber_id); + serialize_sample(&mut serializer, &self.sample); + + serializer.finish() + } +} + +pub(crate) struct Query { + pub(crate) queryable_id: u32, + pub(crate) query_id: u32, + pub(crate) query: zenoh::query::Query, +} + +fn serialize_query(serializer: &mut ZSerializer, query: &zenoh::query::Query) { + serializer.serialize(query.key_expr().as_str()); + serializer.serialize(query.parameters().as_str()); + serialize_option(serializer, &query.payload().map(|p| p.to_bytes())); + serialize_option( + serializer, + &query.encoding().map(|e| encoding_to_id_schema(e)), + ); + serialize_option(serializer, &query.attachment().map(|a| a.to_bytes())); + serializer.serialize(reply_keyexpr_to_u8( + query + .accepts_replies() + .unwrap_or(ReplyKeyExpr::MatchingQuery), + )); +} + +impl Query { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(&self.queryable_id); + serializer.serialize(&self.query_id); + serialize_query(&mut serializer, &self.query); + + serializer.finish() + } +} + +pub(crate) struct Reply { + pub(crate) query_id: u32, + pub(crate) reply: zenoh::query::Reply, +} + +fn serialize_reply(serializer: &mut ZSerializer, reply: &zenoh::query::Reply) { + match reply.result() { + Ok(r) => { + serializer.serialize(true); + serialize_sample(serializer, r); + } + Err(e) => { + serializer.serialize(false); + serializer.serialize(encoding_to_id_schema(e.encoding())); + serializer.serialize(e.payload().to_bytes()); + } + } +} + +impl Reply { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(&self.query_id); + serialize_reply(&mut serializer, &self.reply); + + serializer.finish() + } +} + +pub(crate) struct ResponseFinal { + query_id: u32, +} + +impl ResponseFinal { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(&self.query_id); + serializer.finish() + } + + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(ResponseFinal { + query_id: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct DeclareLivelinessToken { + pub(crate) token_id: u32, +} + +impl DeclareLivelinessToken { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(DeclareLivelinessToken { + token_id: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct UndeclareLivelinessToken { + pub(crate) token_id: u32, +} + +impl UndeclareLivelinessToken { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(&self.token_id); + serializer.finish() + } +} + +pub(crate) struct DeclareLivelinessSubscriber { + pub(crate) subscriber_id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) history: bool, +} + +impl DeclareLivelinessSubscriber { + pub(crate) fn from_wire( + data: ZBytes, + ) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(DeclareLivelinessSubscriber { + subscriber_id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + history: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct UndeclareLivelinessSubscriber { + pub(crate) subscriber_id: u32, +} + +impl UndeclareLivelinessSubscriber { + pub(crate) fn to_wire(&self) -> ZBytes { + let mut serializer = ZSerializer::new(); + serializer.serialize(&self.subscriber_id); + serializer.finish() + } +} + +pub(crate) struct LivelinessGet { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) timeout_ms: u64, +} + +impl LivelinessGet { + pub(crate) fn from_wire(data: ZBytes) -> Result { + let mut deserializer = ZDeserializer::new(&data); + Ok(LivelinessGet { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + timeout_ms: deserializer.deserialize()?, + }) + } +} From a04e78386568e9d3412ad085a74038879d126ec9 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 21 May 2025 19:58:57 +0200 Subject: [PATCH 02/23] finished binary format on rust side --- Cargo.lock | 27 + Cargo.toml | 1 + zenoh-plugin-remote-api/Cargo.toml | 1 + .../src/handle_control_message.rs | 529 ------ .../src/handle_data_message.rs | 259 --- zenoh-plugin-remote-api/src/interface/mod.rs | 1460 ++++++++++------- .../src/interface/remote_message.rs | 717 -------- .../src/interface/ser_de.rs | 258 --- zenoh-plugin-remote-api/src/lib.rs | 327 ++-- zenoh-plugin-remote-api/src/remote_state.rs | 792 +++++++++ 10 files changed, 1831 insertions(+), 2540 deletions(-) delete mode 100644 zenoh-plugin-remote-api/src/handle_control_message.rs delete mode 100644 zenoh-plugin-remote-api/src/handle_data_message.rs delete mode 100644 zenoh-plugin-remote-api/src/interface/remote_message.rs delete mode 100644 zenoh-plugin-remote-api/src/interface/ser_de.rs create mode 100644 zenoh-plugin-remote-api/src/remote_state.rs diff --git a/Cargo.lock b/Cargo.lock index 79844592..d91ef0ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,6 +710,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -883,6 +889,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1200,6 +1217,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" +dependencies = [ + "hashbrown 0.15.3", +] + [[package]] name = "lz4_flex" version = "0.11.3" @@ -3631,6 +3657,7 @@ dependencies = [ "git-version", "jsonschema", "lazy_static", + "lru", "rustc_version", "rustls-pemfile", "schemars", diff --git a/Cargo.toml b/Cargo.toml index 0dba42c6..510386ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ ts-rs = { version = "9.0", features = [ "no-serde-warnings", "import-esm", ] } +lru = "0.14.0" tracing = "0.1" schemars = { version = "0.8.21", features = ["either"] } serde = { version = "1.0.210", default-features = false, features = [ diff --git a/zenoh-plugin-remote-api/Cargo.toml b/zenoh-plugin-remote-api/Cargo.toml index 1c525ddc..777d308e 100644 --- a/zenoh-plugin-remote-api/Cargo.toml +++ b/zenoh-plugin-remote-api/Cargo.toml @@ -73,6 +73,7 @@ uuid = { workspace=true, default-features = false, features = [ "serde", ] } uhlc = { workspace=true, default-features = false } # Default features are disabled due to usage in no_std crates +lru = { workspace = true } [build-dependencies] rustc_version = "0.4.0" diff --git a/zenoh-plugin-remote-api/src/handle_control_message.rs b/zenoh-plugin-remote-api/src/handle_control_message.rs deleted file mode 100644 index 23d54728..00000000 --- a/zenoh-plugin-remote-api/src/handle_control_message.rs +++ /dev/null @@ -1,529 +0,0 @@ -// -// Copyright (c) 2024 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use std::{error::Error, net::SocketAddr, time::Duration}; - -use tracing::{error, warn}; -use uuid::Uuid; -use zenoh::{key_expr::KeyExpr, query::Selector}; - -use crate::{ - interface::{ - ControlMsg, DataMsg, LivelinessMsg, QueryWS, QueryableMsg, RemoteAPIMsg, ReplyWS, SampleWS, - SessionInfo, - }, - spawn_future, RemoteState, StateMap, -}; - -/// -/// Macro to replace the pattern of adding to builders if a field exists -/// i.e. add_if_some!(consolidation, get_builder); -/// expands to -/// if Some(consolidation) = consolidation{ -/// get_builder = get_builder.consolidation(consolidation); -/// } -macro_rules! add_if_some { - ($x:ident, $y:ident) => { - if let Some($x) = $x { - $y = $y.$x($x); - } - }; -} - -/// Function to handle control messages recieved from the client to Plugin -/// This function should never be long running. -/// The main purpose of this function is to handle changes to state, -/// and any long running operations i.e. Get / Queryable should spawn futures with thier long running resources. -pub(crate) async fn handle_control_message( - ctrl_msg: ControlMsg, - sock_addr: SocketAddr, - state_map: StateMap, -) -> Result<(), Box> { - // Access State Structure - let mut state_writer = state_map.write().await; - let state_map = match state_writer.get_mut(&sock_addr) { - Some(state_map) => state_map, - None => { - tracing::warn!("State Map Does not contain SocketAddr"); - return Ok(()); - } - }; - - // Handle Control Message - match ctrl_msg { - ControlMsg::OpenSession => { - let remote_api_message = - RemoteAPIMsg::Control(ControlMsg::Session(state_map.session_id)); - - if let Err(e) = state_map.websocket_tx.send(remote_api_message) { - error!("Forward Sample Channel error: {e}"); - }; - } - ControlMsg::SessionInfo => { - let session_info = state_map.session.info(); - - let zid = session_info.zid().await.to_string(); - let z_peers: Vec = session_info - .peers_zid() - .await - .map(|x| x.to_string()) - .collect(); - let z_routers: Vec = session_info - .routers_zid() - .await - .map(|x| x.to_string()) - .collect(); - - let session_info = SessionInfo { - zid, - z_routers, - z_peers, - }; - - let remote_api_message = RemoteAPIMsg::Data(DataMsg::SessionInfo(session_info)); - - if let Err(e) = state_map.websocket_tx.send(remote_api_message) { - error!("Forward Sample Channel error: {e}"); - }; - } - ControlMsg::CloseSession => { - if let Some(state_map) = state_writer.remove(&sock_addr) { - state_map.cleanup().await; - } else { - warn!("State Map Does not contain SocketAddr"); - } - } - ControlMsg::NewTimestamp(uuid) => { - let ts = state_map.session.new_timestamp(); - let ts_string = ts.to_string(); - let _ = state_map.timestamps.insert(uuid, ts); - - let since_the_epoch = ts - .get_time() - .to_system_time() - .duration_since(std::time::UNIX_EPOCH) - .expect("Time went backwards") - .as_millis() as u64; // JS numbers are F64, is the only way to get a Number that is similar to what is produced by Date.now() in Javascript - - if let Err(e) = state_map - .websocket_tx - .send(RemoteAPIMsg::Data(DataMsg::NewTimestamp { - id: uuid, - string_rep: ts_string, - millis_since_epoch: since_the_epoch, - })) - { - error!("{}", e); - }; - } - ControlMsg::Get { - key_expr, - parameters, - id, - consolidation, - congestion_control, - priority, - express, - target, - encoding, - payload, - attachment, - timeout, - } => { - let selector = Selector::owned(key_expr, parameters.unwrap_or_default()); - let mut get_builder = state_map.session.get(selector); - - add_if_some!(consolidation, get_builder); - add_if_some!(congestion_control, get_builder); - add_if_some!(priority, get_builder); - add_if_some!(express, get_builder); - add_if_some!(encoding, get_builder); - add_if_some!(target, get_builder); - - if let Some(payload_b64) = payload { - match payload_b64.b64_to_bytes() { - Ok(payload) => get_builder = get_builder.payload(payload), - Err(err) => warn!("Could not decode B64 encoded bytes {err}"), - } - } - if let Some(attachment_b64) = attachment { - match attachment_b64.b64_to_bytes() { - Ok(attachment) => get_builder = get_builder.attachment(attachment), - Err(err) => warn!("Could not decode B64 encoded bytes {err}"), - } - } - if let Some(timeout) = timeout { - get_builder = get_builder.timeout(Duration::from_millis(timeout)); - } - - let ws_tx = state_map.websocket_tx.clone(); - let finish_msg = RemoteAPIMsg::Control(ControlMsg::GetFinished { id }); - let receiver = get_builder.await?; - spawn_future(async move { - while let Ok(reply) = receiver.recv_async().await { - let reply_ws = ReplyWS::from((reply, id)); - let remote_api_msg = RemoteAPIMsg::Data(DataMsg::GetReply(reply_ws)); - if let Err(err) = ws_tx.send(remote_api_msg) { - tracing::error!("{}", err); - } - } - if let Err(err) = ws_tx.send(finish_msg) { - tracing::error!("{}", err); - } - }); - } - ControlMsg::Put { - key_expr, - payload, - encoding, - congestion_control, - priority, - express, - attachment, - timestamp, - } => { - let mut put_builder = match payload.b64_to_bytes() { - Ok(payload) => state_map.session.put(key_expr, payload), - Err(err) => { - warn!("ControlMsg::Put , could not decode B64 encoded bytes {err}"); - return Ok(()); - } - }; - - add_if_some!(encoding, put_builder); - add_if_some!(congestion_control, put_builder); - add_if_some!(priority, put_builder); - add_if_some!(express, put_builder); - - if let Some(ts) = timestamp.and_then(|k| state_map.timestamps.get(&k)) { - put_builder = put_builder.timestamp(*ts); - } - - if let Some(attachment_b64) = attachment { - match attachment_b64.b64_to_bytes() { - Ok(attachment) => put_builder = put_builder.attachment(attachment), - Err(err) => warn!("Could not decode B64 encoded bytes {err}"), - } - } - - put_builder.await?; - } - ControlMsg::Delete { - key_expr, - congestion_control, - priority, - express, - attachment, - timestamp, - } => { - let mut delete_builder = state_map.session.delete(key_expr); - add_if_some!(congestion_control, delete_builder); - add_if_some!(priority, delete_builder); - add_if_some!(express, delete_builder); - if let Some(ts) = timestamp.and_then(|k| state_map.timestamps.get(&k)) { - delete_builder = delete_builder.timestamp(*ts); - } - - if let Some(attachment_b64) = attachment { - match attachment_b64.b64_to_bytes() { - Ok(attachment) => delete_builder = delete_builder.attachment(attachment), - Err(err) => warn!("Could not decode B64 encoded bytes {err}"), - } - } - - delete_builder.await?; - } - // SUBSCRIBER - ControlMsg::DeclareSubscriber { - key_expr: owned_key_expr, - id: subscriber_uuid, - } => { - let key_expr = KeyExpr::new(owned_key_expr.clone())?; - let ch_tx = state_map.websocket_tx.clone(); - let subscriber_builder = state_map.session.declare_subscriber(key_expr); - let subscriber = subscriber_builder.await?; - let join_handle = spawn_future(async move { - while let Ok(sample) = subscriber.recv_async().await { - let sample_ws = SampleWS::from(sample); - let remote_api_message = - RemoteAPIMsg::Data(DataMsg::Sample(sample_ws, subscriber_uuid)); - if let Err(e) = ch_tx.send(remote_api_message) { - error!("Forward Sample Channel error: {e}"); - }; - } - }); - state_map - .subscribers - .insert(subscriber_uuid, (join_handle, owned_key_expr)); - - let remote_api_msg = RemoteAPIMsg::Control(ControlMsg::Subscriber(subscriber_uuid)); - state_map.websocket_tx.send(remote_api_msg)?; - } - ControlMsg::UndeclareSubscriber(uuid) => { - if let Some((join_handle, _)) = state_map.subscribers.remove(&uuid) { - join_handle.abort(); // This should drop the underlying subscriber of the future - // Send the same message back to the client to confirm that the subscriber has been undeclared. - // TODO: This can likely be removed if/when the protocol gets reworked. - let remote_api_msg = RemoteAPIMsg::Control(ControlMsg::UndeclareSubscriber(uuid)); - state_map.websocket_tx.send(remote_api_msg)?; - } else { - warn!("UndeclareSubscriber: No Subscriber with UUID {uuid}"); - } - } - // Publisher - ControlMsg::DeclarePublisher { - key_expr, - id: uuid, - encoding, - congestion_control, - priority, - express, - reliability, - } => { - let mut publisher_builder = state_map.session.declare_publisher(key_expr); - add_if_some!(encoding, publisher_builder); - add_if_some!(congestion_control, publisher_builder); - add_if_some!(priority, publisher_builder); - add_if_some!(express, publisher_builder); - add_if_some!(reliability, publisher_builder); - - let publisher = publisher_builder.await?; - state_map.publishers.insert(uuid, publisher); - } - ControlMsg::UndeclarePublisher(id) => { - if let Some(publisher) = state_map.publishers.remove(&id) { - publisher.undeclare().await?; - } else { - warn!("UndeclarePublisher: No Publisher with UUID {id}"); - } - } - // Queryable - ControlMsg::DeclareQueryable { - key_expr, - complete, - id: queryable_uuid, - } => { - let unanswered_queries = state_map.unanswered_queries.clone(); - let ch_tx = state_map.websocket_tx.clone(); - let queryable = state_map - .session - .declare_queryable(&key_expr) - .complete(complete) - .await?; - let join_handle = spawn_future(async move { - while let Ok(query) = queryable.recv_async().await { - let query_uuid = Uuid::new_v4(); - let queryable_msg = QueryableMsg::Query { - queryable_uuid, - query: QueryWS::from((&query, query_uuid)), - }; - - match unanswered_queries.write() { - Ok(mut rw_lock) => { - rw_lock.insert(query_uuid, query); - } - Err(err) => { - tracing::error!("Query RwLock has been poisoned {err:?}") - } - } - - let remote_msg = RemoteAPIMsg::Data(DataMsg::Queryable(queryable_msg)); - if let Err(err) = ch_tx.send(remote_msg) { - tracing::error!("Could not send Queryable Message on WS {}", err); - }; - } - }); - - state_map - .queryables - .insert(queryable_uuid, (join_handle, key_expr)); - } - ControlMsg::UndeclareQueryable(uuid) => { - if let Some((queryable, _)) = state_map.queryables.remove(&uuid) { - queryable.abort(); - // TODO: This can likely be removed if/when the protocol gets reworked. - let remote_api_msg = RemoteAPIMsg::Control(ControlMsg::UndeclareQueryable(uuid)); - state_map.websocket_tx.send(remote_api_msg)?; - }; - } - ControlMsg::Liveliness(liveliness_msg) => { - return handle_liveliness(liveliness_msg, state_map).await; - } - ControlMsg::DeclareQuerier { - id, - key_expr, - target, - timeout, - accept_replies, - congestion_control, - priority, - consolidation, - allowed_destination, - express, - } => { - let mut querier_builder = state_map.session.declare_querier(key_expr); - let timeout = timeout.map(Duration::from_millis); - - add_if_some!(target, querier_builder); - add_if_some!(timeout, querier_builder); - add_if_some!(accept_replies, querier_builder); - add_if_some!(accept_replies, querier_builder); - add_if_some!(congestion_control, querier_builder); - add_if_some!(priority, querier_builder); - add_if_some!(consolidation, querier_builder); - add_if_some!(allowed_destination, querier_builder); - add_if_some!(express, querier_builder); - - let querier = querier_builder.await?; - state_map.queriers.insert(id, querier); - } - ControlMsg::UndeclareQuerier(uuid) => { - if let Some(querier) = state_map.queriers.remove(&uuid) { - querier.undeclare().await?; - } else { - warn!("No Querier Found with UUID {}", uuid); - }; - } - ControlMsg::QuerierGet { - get_id, - querier_id, - parameters, - encoding, - payload, - attachment, - } => { - if let Some(querier) = state_map.queriers.get(&querier_id) { - let mut get_builder = querier.get(); - if let Some(payload_b64) = payload { - match payload_b64.b64_to_bytes() { - Ok(payload) => get_builder = get_builder.payload(payload), - Err(err) => warn!("Could not decode B64 encoded bytes {err}"), - } - } - if let Some(attachment_b64) = attachment { - match attachment_b64.b64_to_bytes() { - Ok(attachment) => get_builder = get_builder.attachment(attachment), - Err(err) => warn!("Could not decode B64 encoded bytes {err}"), - } - } - add_if_some!(encoding, get_builder); - add_if_some!(parameters, get_builder); - - let ws_tx = state_map.websocket_tx.clone(); - let finish_msg = RemoteAPIMsg::Control(ControlMsg::GetFinished { id: get_id }); - let receiver = get_builder.await?; - spawn_future(async move { - while let Ok(reply) = receiver.recv_async().await { - let reply_ws = ReplyWS::from((reply, get_id)); - let remote_api_msg = RemoteAPIMsg::Data(DataMsg::GetReply(reply_ws)); - if let Err(err) = ws_tx.send(remote_api_msg) { - tracing::error!("{}", err); - } - } - if let Err(err) = ws_tx.send(finish_msg) { - tracing::error!("{}", err); - } - }); - } else { - // TODO: Do we want to add an error here ? - warn!("No Querier With ID {querier_id} found") - } - } - msg @ (ControlMsg::GetFinished { id: _ } - | ControlMsg::Session(_) - | ControlMsg::Subscriber(_)) => { - // make server recieving these types unrepresentable - error!("Backend should not recieve this message Type: {msg:?}"); - } - }; - Ok(()) -} - -// Handle Liveliness Messages -async fn handle_liveliness( - liveliness_msg: LivelinessMsg, - state_map: &mut RemoteState, -) -> Result<(), Box> { - let liveliness = state_map.session.liveliness(); - match liveliness_msg { - LivelinessMsg::DeclareToken { key_expr, id } => { - let token = liveliness.declare_token(key_expr).await?; - state_map.liveliness_tokens.insert(id, token); - } - LivelinessMsg::UndeclareToken(uuid) => { - if let Some(token) = state_map.liveliness_tokens.remove(&uuid) { - token.undeclare().await?; - } - } - LivelinessMsg::DeclareSubscriber { - key_expr: owned_key_expr, - id, - history, - } => { - let key_expr = KeyExpr::new(owned_key_expr.clone())?; - let subscriber = liveliness - .declare_subscriber(key_expr) - .history(history) - .await?; - let ch_tx = state_map.websocket_tx.clone(); - - let handler = spawn_future(async move { - while let Ok(sample) = subscriber.recv_async().await { - let sample_ws = SampleWS::from(sample); - let remote_api_message = RemoteAPIMsg::Data(DataMsg::Sample(sample_ws, id)); - if let Err(e) = ch_tx.send(remote_api_message) { - error!("Forward Sample Channel error: {e}"); - }; - } - }); - state_map - .liveliness_subscribers - .insert(id, (handler, owned_key_expr)); - } - LivelinessMsg::UndeclareSubscriber(uuid) => { - if let Some((join_handle, _)) = state_map.liveliness_subscribers.remove(&uuid) { - join_handle.abort(); // This should drop the underlying liveliness_subscribers of the future - } else { - warn!("UndeclareSubscriber: No Subscriber with UUID {uuid}"); - } - } - LivelinessMsg::Get { - key_expr, - id, - timeout, - } => { - let mut builder = liveliness.get(key_expr); - if let Some(timeout) = timeout { - builder = builder.timeout(Duration::from_millis(timeout)); - } - let receiver = builder.await?; - let ws_tx = state_map.websocket_tx.clone(); - - spawn_future(async move { - while let Ok(reply) = receiver.recv_async().await { - let reply_ws = ReplyWS::from((reply, id)); - let remote_api_msg = RemoteAPIMsg::Data(DataMsg::GetReply(reply_ws)); - if let Err(err) = ws_tx.send(remote_api_msg) { - tracing::error!("{}", err); - } - } - let remote_api_msg = RemoteAPIMsg::Control(ControlMsg::GetFinished { id }); - if let Err(err) = ws_tx.send(remote_api_msg) { - tracing::error!("{}", err); - } - }); - } - } - Ok(()) -} diff --git a/zenoh-plugin-remote-api/src/handle_data_message.rs b/zenoh-plugin-remote-api/src/handle_data_message.rs deleted file mode 100644 index f33dd0b6..00000000 --- a/zenoh-plugin-remote-api/src/handle_data_message.rs +++ /dev/null @@ -1,259 +0,0 @@ -// -// Copyright (c) 2024 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// -use std::{error::Error, net::SocketAddr}; - -use tracing::{error, warn}; -use zenoh::{ - qos::{CongestionControl, Priority}, - query::Query, -}; - -use crate::{ - interface::{DataMsg, QueryReplyVariant, QueryableMsg}, - StateMap, -}; - -pub async fn handle_data_message( - data_msg: DataMsg, - sock_addr: SocketAddr, - state_map: StateMap, -) -> Result<(), Box> { - // Access State Structure - let state_writer = state_map.read().await; - let state_map = match state_writer.get(&sock_addr) { - Some(x) => x, - None => { - tracing::warn!("State Map Does not contain SocketAddr"); - return Ok(()); - } - }; - - // Data Message - match data_msg { - DataMsg::PublisherPut { - id, - payload, - attachment, - encoding, - timestamp, - } => { - if let Some(publisher) = state_map.publishers.get(&id) { - let mut put_builder = match payload.b64_to_bytes() { - Ok(payload) => publisher.put(payload), - Err(err) => { - warn!("DataMsg::PublisherPut : Could not decode B64 encoded bytes {err}"); - return Err(Box::new(err)); - } - }; - - if let Some(attachment_b64) = attachment { - match attachment_b64.b64_to_bytes() { - Ok(payload) => put_builder = put_builder.attachment(payload), - Err(err) => { - warn!( - "DataMsg::PublisherPut : Could not decode B64 encoded bytes {err}" - ); - return Err(Box::new(err)); - } - } - } - if let Some(encoding) = encoding { - put_builder = put_builder.encoding(encoding); - } - if let Some(ts) = timestamp.and_then(|k| state_map.timestamps.get(&k)) { - put_builder = put_builder.timestamp(*ts); - } - if let Err(err) = put_builder.await { - error!("PublisherPut {id}, {err}"); - } - } else { - warn!("Publisher {id}, does not exist in State"); - } - } - DataMsg::PublisherDelete { - id, - attachment, - timestamp, - } => { - if let Some(publisher) = state_map.publishers.get(&id) { - let mut publisher_builder = publisher.delete(); - match attachment.map(|x| x.b64_to_bytes()) { - Some(Ok(attachment)) => { - publisher_builder = publisher_builder.attachment(&attachment); - } - Some(Err(e)) => { - error!("{}", e); - } - None => {} - } - if let Some(ts) = timestamp.and_then(|k| state_map.timestamps.get(&k)) { - publisher_builder = publisher_builder.timestamp(*ts); - } - - if let Err(e) = publisher_builder.await { - error!("Could not publish {e}"); - }; - } - } - DataMsg::Queryable(queryable_msg) => match queryable_msg { - QueryableMsg::Reply { reply } => { - let query: Option = match state_map.unanswered_queries.write() { - Ok(mut wr) => wr.remove(&reply.query_uuid), - Err(err) => { - tracing::error!("unanswered Queries RwLock Poisoned {err}"); - return Ok(()); - } - }; - if let Some(q) = query { - match reply.result { - QueryReplyVariant::Reply { - key_expr, - payload, - encoding, - priority, - congestion_control, - express, - timestamp, - attachment, - } => match payload.b64_to_bytes() { - Ok(payload) => { - let priority = match priority.try_into() { - Ok(p) => p, - Err(e) => { - warn!("DataMsg::QueryReplyVariant::Reply : Could not convert value {priority} to Priority: {}", e); - Priority::default() - } - }; - let congestion_control = if congestion_control == 0 { - CongestionControl::Drop - } else if congestion_control == 1 { - CongestionControl::Block - } else { - warn!("DataMsg::QueryReplyVariant::Reply : Could not convert value {congestion_control} to CongestionControl:"); - CongestionControl::default() - }; - let mut builder = q - .reply(key_expr, payload) - .priority(priority) - .congestion_control(congestion_control) - .express(express); - if let Some(encoding) = encoding { - builder = builder.encoding(encoding); - } - if let Some(attachment_b64) = attachment { - match attachment_b64.b64_to_bytes() { - Ok(payload) => builder = builder.attachment(payload), - Err(err) => { - warn!( - "DataMsg::QueryReplyVariant::Reply : Could not decode B64 encoded bytes {err}" - ); - return Err(Box::new(err)); - } - } - } - if let Some(timestamp) = - timestamp.and_then(|k| state_map.timestamps.get(&k)) - { - builder = builder.timestamp(*timestamp); - } - builder.await? - } - Err(err) => { - warn!("QueryReplyVariant::Reply : Could not decode B64 encoded bytes {err}"); - return Err(Box::new(err)); - } - }, - QueryReplyVariant::ReplyErr { payload, encoding } => { - match payload.b64_to_bytes() { - Ok(payload) => { - let mut builder = q.reply_err(payload); - if let Some(encoding) = encoding { - builder = builder.encoding(encoding); - } - builder.await? - } - Err(err) => { - warn!("QueryReplyVariant::ReplyErr : Could not decode B64 encoded bytes {err}"); - return Err(Box::new(err)); - } - } - } - QueryReplyVariant::ReplyDelete { - key_expr, - priority, - congestion_control, - express, - timestamp, - attachment, - } => { - let priority = priority.try_into().unwrap_or({ - warn!("DataMsg::QueryReplyVariant::ReplyDelete : Could not convert value {priority} to Priority:"); - Priority::default() - }); - let congestion_control = if congestion_control == 0 { - CongestionControl::Drop - } else if congestion_control == 1 { - CongestionControl::Block - } else { - warn!("DataMsg::QueryReplyVariant::ReplyDelete : Could not convert value {congestion_control} to CongestionControl:"); - CongestionControl::default() - }; - let mut builder = q - .reply_del(key_expr) - .priority(priority) - .congestion_control(congestion_control) - .express(express); - if let Some(attachment_b64) = attachment { - match attachment_b64.b64_to_bytes() { - Ok(payload) => builder = builder.attachment(payload), - Err(err) => { - warn!( - "DataMsg::QueryReplyVariant::ReplyDelete : Could not decode B64 encoded bytes {err}" - ); - return Err(Box::new(err)); - } - } - } - if let Some(timestamp) = - timestamp.and_then(|k| state_map.timestamps.get(&k)) - { - builder = builder.timestamp(*timestamp); - } - builder.await? - } - } - } else { - tracing::error!("Query id not found in map {}", reply.query_uuid); - }; - } - QueryableMsg::Query { - queryable_uuid: _, - query: _, - } => { - warn!("Plugin should not receive Query from Client, This should go via Get API"); - } - }, - DataMsg::Sample(_, _) - | DataMsg::GetReply(_) - | DataMsg::NewTimestamp { - id: _, - string_rep: _, - millis_since_epoch: _, - } - | DataMsg::SessionInfo(_) => { - error!("Server Should not recieved a {data_msg:?} Variant from client"); - } - } - Ok(()) -} diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index 9dd9b9f0..d59661c6 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -12,668 +12,946 @@ // ZettaScale Zenoh Team, // -use std::sync::Arc; -pub(crate) mod remote_message; - -// mod interface::ser_de; -pub(crate) mod ser_de; -use base64::{prelude::BASE64_STANDARD, Engine}; -use ser_de::{ - deserialize_congestion_control, deserialize_consolidation_mode, deserialize_locality, - deserialize_priority, deserialize_query_target, deserialize_reliability, - deserialize_reply_key_expr, serialize_congestion_control, serialize_consolidation_mode, - serialize_locality, serialize_priority, serialize_query_target, serialize_reliability, - serialize_reply_key_expr, -}; -use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use std::ops::Not; + +use uhlc::{Timestamp, NTP64}; use uuid::Uuid; use zenoh::{ + bytes::{Encoding, ZBytes}, + config::ZenohId, key_expr::OwnedKeyExpr, qos::{CongestionControl, Priority, Reliability}, - query::{ConsolidationMode, Query, QueryTarget, Reply, ReplyError, ReplyKeyExpr}, - sample::{Locality, Sample, SampleKind}, + query::{ConsolidationMode, QueryTarget, ReplyKeyExpr}, + sample::{Locality, SampleKind}, }; -// ██████ ███████ ███ ███ ██████ ████████ ███████ █████ ██████ ██ ███ ███ ███████ ███████ ███████ █████ ██████ ███████ -// ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ -// ██████ █████ ██ ████ ██ ██ ██ ██ █████ ███████ ██████ ██ ██ ████ ██ █████ ███████ ███████ ███████ ██ ███ █████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ███████ ██ ██ ██████ ██ ███████ ██ ██ ██ ██ ██ ██ ███████ ███████ ███████ ██ ██ ██████ ███████ +use zenoh_ext::{Deserialize, Serialize, ZDeserializeError, ZDeserializer, ZSerializer}; +use zenoh_result::bail; -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub(crate) struct B64String(pub String); -impl From for B64String { - fn from(value: String) -> Self { - B64String(value) +pub(crate) type SequenceId = u32; + +pub(crate) fn serialize_option(serializer: &mut ZSerializer, o: &Option) { + match o { + Some(v) => { + serializer.serialize(true); + serializer.serialize(v); + } + None => { + serializer.serialize(false); + } } } -impl B64String { - pub fn b64_to_bytes(self) -> Result, base64::DecodeError> { - BASE64_STANDARD.decode(self.0) +pub(crate) fn deserialize_option( + deserializer: &mut zenoh_ext::ZDeserializer, +) -> Result, ZDeserializeError> { + let has_value = deserializer.deserialize::()?; + match has_value { + true => Ok(Some(deserializer.deserialize()?)), + false => Ok(None), } } -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub enum RemoteAPIMsg { - Data(DataMsg), - Control(ControlMsg), +pub(crate) struct Error { + pub(crate) content_id: InRemoteMessageId, + pub(crate) error: String, } -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub enum DataMsg { - // Client -> SVR - PublisherPut { - id: Uuid, - payload: B64String, - attachment: Option, - encoding: Option, - timestamp: Option, - }, - PublisherDelete { - id: Uuid, - attachment: Option, - timestamp: Option, - }, - // SVR -> Client - // Subscriber - Sample(SampleWS, Uuid), - // GetReply - GetReply(ReplyWS), - SessionInfo(SessionInfo), - NewTimestamp { - id: Uuid, - string_rep: String, - millis_since_epoch: u64, - }, +impl Error { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(self.content_id as u8); + serializer.serialize(&self.error); + } +} - // Bidirectional - Queryable(QueryableMsg), +pub(crate) struct Ok { + pub(crate) content_id: InRemoteMessageId, } -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub struct SessionInfo { - pub zid: String, - pub z_routers: Vec, - pub z_peers: Vec, +impl Ok { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(self.content_id as u8); + } } -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub enum QueryableMsg { - // SVR -> Client - // UUID of original queryable - Query { - queryable_uuid: Uuid, - query: QueryWS, - }, - // Client -> SVR - Reply { - reply: QueryReplyWS, - }, +fn congestion_control_from_u8(c: u8) -> Result { + match c { + 0 => Ok(CongestionControl::Drop), + 1 => Ok(CongestionControl::Block), + v => bail!("Unsupported congestion control value {}", v), + } } -// ██████ ██████ ███ ██ ████████ ██████ ██████ ██ ███ ███ ███████ ███████ ███████ █████ ██████ ███████ -// ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ████ ██ █████ ███████ ███████ ███████ ██ ███ █████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██████ ██████ ██ ████ ██ ██ ██ ██████ ███████ ██ ██ ███████ ███████ ███████ ██ ██ ██████ ███████ - -#[derive(Debug, Serialize, Deserialize, TS)] -#[ts(export)] -pub enum ControlMsg { - // Session - OpenSession, - CloseSession, - Session(Uuid), - NewTimestamp(Uuid), - - // - SessionInfo, - - // Session Action Messages - Get { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - parameters: Option, - id: Uuid, - // Parameters - #[serde( - deserialize_with = "deserialize_consolidation_mode", - serialize_with = "serialize_consolidation_mode", - default - )] - #[ts(type = "number | undefined")] - consolidation: Option, - #[ts(type = "number | undefined")] - timeout: Option, - #[serde( - deserialize_with = "deserialize_congestion_control", - serialize_with = "serialize_congestion_control", - default - )] - #[ts(type = "number | undefined")] - congestion_control: Option, - #[serde( - deserialize_with = "deserialize_priority", - serialize_with = "serialize_priority", - default - )] - #[ts(type = "number | undefined")] - priority: Option, - #[serde( - deserialize_with = "deserialize_query_target", - serialize_with = "serialize_query_target", - default - )] - #[ts(type = "number | undefined")] - target: Option, - #[ts(type = "boolean | undefined")] - express: Option, - #[ts(type = "string | undefined")] - encoding: Option, - #[ts(type = "string | undefined")] - payload: Option, - #[ts(type = "string | undefined")] - attachment: Option, - }, - GetFinished { - id: Uuid, - }, - Put { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - payload: B64String, - // - #[ts(type = "string | undefined")] - encoding: Option, - #[serde( - deserialize_with = "deserialize_congestion_control", - serialize_with = "serialize_congestion_control", - default - )] - #[ts(type = "number | undefined")] - congestion_control: Option, - #[serde( - deserialize_with = "deserialize_priority", - serialize_with = "serialize_priority", - default - )] - #[ts(type = "number | undefined")] - priority: Option, - #[ts(type = "boolean | undefined")] - express: Option, - #[ts(type = "string | undefined")] - attachment: Option, - #[ts(type = "string | undefined")] - timestamp: Option, - }, - Delete { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - // - #[serde( - deserialize_with = "deserialize_congestion_control", - serialize_with = "serialize_congestion_control", - default - )] - #[ts(type = "number | undefined")] - congestion_control: Option, - #[serde( - deserialize_with = "deserialize_priority", - serialize_with = "serialize_priority", - default - )] - #[ts(type = "number | undefined")] - priority: Option, - #[ts(type = "boolean | undefined")] - express: Option, - #[ts(type = "string | undefined")] - attachment: Option, - #[ts(type = "string | undefined")] - timestamp: Option, - }, - // Subscriber - DeclareSubscriber { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - id: Uuid, - }, - Subscriber(Uuid), - UndeclareSubscriber(Uuid), - - // Publisher - DeclarePublisher { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - #[ts(type = "string | undefined")] - encoding: Option, - #[serde( - deserialize_with = "deserialize_congestion_control", - serialize_with = "serialize_congestion_control", - default - )] - #[ts(type = "number | undefined")] - congestion_control: Option, - #[serde( - deserialize_with = "deserialize_priority", - serialize_with = "serialize_priority", - default - )] - #[ts(type = "number | undefined")] - priority: Option, - #[serde( - deserialize_with = "deserialize_reliability", - serialize_with = "serialize_reliability", - default - )] - #[ts(type = "number | undefined")] - reliability: Option, - #[ts(type = "boolean | undefined")] - express: Option, - id: Uuid, - }, - UndeclarePublisher(Uuid), - // Queryable - DeclareQueryable { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - id: Uuid, - complete: bool, - }, - UndeclareQueryable(Uuid), - // Quierer - DeclareQuerier { - id: Uuid, - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - #[serde( - deserialize_with = "deserialize_query_target", - serialize_with = "serialize_query_target", - default - )] - #[ts(type = "number | undefined")] - target: Option, - #[ts(type = "number | undefined")] - timeout: Option, - #[serde( - deserialize_with = "deserialize_reply_key_expr", - serialize_with = "serialize_reply_key_expr", - default - )] - #[ts(type = "number | undefined")] - accept_replies: Option, - #[serde( - deserialize_with = "deserialize_locality", - serialize_with = "serialize_locality", - default - )] - #[ts(type = "number | undefined")] - allowed_destination: Option, - #[serde( - deserialize_with = "deserialize_congestion_control", - serialize_with = "serialize_congestion_control", - default - )] - #[ts(type = "number | undefined")] - congestion_control: Option, - #[serde( - deserialize_with = "deserialize_priority", - serialize_with = "serialize_priority", - default - )] - #[ts(type = "number | undefined")] - priority: Option, - #[serde( - deserialize_with = "deserialize_consolidation_mode", - serialize_with = "serialize_consolidation_mode", - default - )] - #[ts(type = "number | undefined")] - consolidation: Option, - #[ts(type = "boolean | undefined")] - express: Option, - }, - UndeclareQuerier(Uuid), - // Querier - QuerierGet { - querier_id: Uuid, - get_id: Uuid, - #[ts(type = "string | undefined")] - parameters: Option, - #[ts(type = "string | undefined")] - encoding: Option, - #[ts(type = "string | undefined")] - payload: Option, - #[ts(type = "string | undefined")] - attachment: Option, - }, +fn congestion_control_to_u8(c: CongestionControl) -> u8 { + match c { + CongestionControl::Drop => 0, + CongestionControl::Block => 1, + } +} - // Liveliness - Liveliness(LivelinessMsg), +fn priority_from_u8(p: u8) -> Result { + p.try_into() } -#[derive(Debug, Serialize, Deserialize, TS)] -#[ts(export)] -pub enum LivelinessMsg { - DeclareToken { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - id: Uuid, - }, - UndeclareToken(Uuid), - DeclareSubscriber { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - id: Uuid, - history: bool, - }, - UndeclareSubscriber(Uuid), - Get { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - id: Uuid, - // timeout in Milliseconds - #[ts(type = "number | undefined")] - timeout: Option, - }, +fn priority_to_u8(p: Priority) -> u8 { + p as u8 } -// ██ ██ ██████ █████ ██████ ██████ ███████ ██████ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ █ ██ ██████ ███████ ██████ ██████ █████ ██████ ███████ -// ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ███ ███ ██ ██ ██ ██ ██ ██ ███████ ██ ██ ███████ - -// Wrapper to get OwnerKeyExpr to play with TS -#[allow(dead_code)] // To allow OwnedKeyExpr to be converted to String -#[derive(Debug, Deserialize, TS)] -#[serde(from = "String")] -pub struct OwnedKeyExprWrapper(Arc); - -impl From for OwnedKeyExprWrapper { - fn from(s: String) -> Self { - OwnedKeyExprWrapper(s.into()) - } -} - -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub struct QueryWS { - query_uuid: Uuid, - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - parameters: String, - encoding: Option, - #[ts(type = "string | undefined")] - attachment: Option, - #[ts(type = "string | undefined")] - payload: Option, -} - -impl From<(&Query, Uuid)> for QueryWS { - fn from((q, uuid): (&Query, Uuid)) -> Self { - let payload = q - .payload() - .map(|x| x.to_bytes().to_vec()) - .map(|vec_bytes| BASE64_STANDARD.encode(vec_bytes).into()); - let attachment: Option = q - .attachment() - .map(|x| x.to_bytes().to_vec()) - .map(|vec_bytes| BASE64_STANDARD.encode(vec_bytes).into()); - - QueryWS { - query_uuid: uuid, - key_expr: q.key_expr().to_owned().into(), - parameters: q.parameters().to_string(), - encoding: q.encoding().map(|x| x.to_string()), - attachment, - payload, - } +fn reliability_from_u8(r: u8) -> Result { + match r { + 0 => Ok(Reliability::BestEffort), + 1 => Ok(Reliability::Reliable), + v => bail!("Unsupported reliability value {}", v), } } -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub struct ReplyWS { - pub query_uuid: Uuid, - pub result: Result, +fn reliability_to_u8(r: Reliability) -> u8 { + match r { + Reliability::BestEffort => 0, + Reliability::Reliable => 1, + } } -impl From<(Reply, Uuid)> for ReplyWS { - fn from((reply, uuid): (Reply, Uuid)) -> Self { - match reply.result() { - Ok(sample) => { - let sample_ws = SampleWS::from(sample); - ReplyWS { - query_uuid: uuid, - result: Ok(sample_ws), - } - } - Err(err) => { - let error_ws = ReplyErrorWS::from(err); +fn locality_from_u8(l: u8) -> Result { + match l { + 0 => Ok(Locality::Any), + 1 => Ok(Locality::Remote), + 2 => Ok(Locality::SessionLocal), + v => bail!("Unsupported locality value {}", v), + } +} - ReplyWS { - query_uuid: uuid, - result: Err(error_ws), - } - } - } +fn consolidation_from_u8(l: u8) -> Result { + match l { + 0 => Ok(ConsolidationMode::Auto), + 1 => Ok(ConsolidationMode::None), + 2 => Ok(ConsolidationMode::Monotonic), + 3 => Ok(ConsolidationMode::Latest), + v => bail!("Unsupported consolidation mode value {}", v), } } -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub struct QueryReplyWS { - pub query_uuid: Uuid, - pub result: QueryReplyVariant, -} - -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub enum QueryReplyVariant { - Reply { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - payload: B64String, - encoding: Option, - priority: u8, - congestion_control: u8, - express: bool, - timestamp: Option, - attachment: Option, - }, - ReplyErr { - payload: B64String, - encoding: Option, - }, - ReplyDelete { - #[ts(as = "OwnedKeyExprWrapper")] - key_expr: OwnedKeyExpr, - priority: u8, - congestion_control: u8, - express: bool, - timestamp: Option, - attachment: Option, - }, +fn query_target_from_u8(t: u8) -> Result { + match t { + 0 => Ok(QueryTarget::All), + 1 => Ok(QueryTarget::AllComplete), + 2 => Ok(QueryTarget::BestMatching), + v => bail!("Unsupported query target value {}", v), + } } -#[derive(TS)] -#[ts(export)] -#[derive(Debug, Serialize, Deserialize)] -pub struct ReplyErrorWS { - pub(crate) payload: B64String, - pub(crate) encoding: String, +fn reply_keyexpr_from_u8(a: u8) -> Result { + match a { + 0 => Ok(ReplyKeyExpr::Any), + 1 => Ok(ReplyKeyExpr::MatchingQuery), + v => bail!("Unsupported reply keyexpr value {}", v), + } } -impl From for ReplyErrorWS { - fn from(r_e: ReplyError) -> Self { - let z_bytes: Vec = r_e.payload().to_bytes().to_vec(); +fn reply_keyexpr_to_u8(a: ReplyKeyExpr) -> u8 { + match a { + ReplyKeyExpr::Any => 0, + ReplyKeyExpr::MatchingQuery => 1, + } +} - ReplyErrorWS { - payload: BASE64_STANDARD.encode(z_bytes).into(), - encoding: r_e.encoding().to_string(), - } +fn encoding_from_id_schema(id_schema: (u16, String)) -> Encoding { + Encoding::new( + id_schema.0, + id_schema + .1 + .is_empty() + .not() + .then(|| id_schema.1.into_bytes().into()), + ) +} + +fn encoding_to_id_schema(encoding: &Encoding) -> (u16, &[u8]) { + ( + encoding.id(), + match encoding.schema() { + Some(s) => s.as_slice(), + None => &[], + }, + ) +} + +fn opt_encoding_from_id_schema(id_schema: Option<(u16, String)>) -> Option { + id_schema.map(|x| encoding_from_id_schema(x)) +} + +fn opt_timestamp_from_ntp_id( + ntp_id: Option<(u64, [u8; 16])>, +) -> Result, zenoh_result::Error> { + Ok(match ntp_id { + Some((t, id)) => Some(Timestamp::new(NTP64(t), id.try_into()?)), + None => None, + }) +} + +fn timestamp_to_ntp_id(t: &Timestamp) -> (u64, [u8; 16]) { + (t.get_time().0, t.get_id().to_le_bytes()) +} + +fn sample_kind_to_u8(k: SampleKind) -> u8 { + match k { + SampleKind::Put => 0, + SampleKind::Delete => 1, } } -impl From<&ReplyError> for ReplyErrorWS { - fn from(r_e: &ReplyError) -> Self { - let z_bytes: Vec = r_e.payload().to_bytes().to_vec(); +pub(crate) struct OpenAck { + pub(crate) uuid: Uuid, +} - ReplyErrorWS { - payload: base64::prelude::BASE64_STANDARD.encode(z_bytes).into(), - encoding: r_e.encoding().to_string(), - } +impl OpenAck { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(self.uuid.to_string()); } } -#[derive(Debug, Serialize, Deserialize, TS)] -#[ts(export)] -pub struct SampleWS { - #[ts(as = "OwnedKeyExprWrapper")] - pub(crate) key_expr: OwnedKeyExpr, - pub(crate) value: B64String, - pub(crate) kind: SampleKindWS, - pub(crate) encoding: String, - pub(crate) timestamp: Option, - pub(crate) congestion_control: u8, - pub(crate) priority: u8, +pub(crate) struct DeclarePublisher { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) encoding: Encoding, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, pub(crate) express: bool, - pub(crate) attachement: Option, + pub(crate) reliability: Reliability, + pub(crate) allowed_destination: Locality, } -#[derive(Debug, Serialize, Deserialize, TS)] -#[ts(export)] -pub enum SampleKindWS { - Put = 0, - Delete = 1, +impl DeclarePublisher { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(DeclarePublisher { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + encoding: encoding_from_id_schema(deserializer.deserialize()?), + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + reliability: reliability_from_u8(deserializer.deserialize()?)?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } } -impl From for SampleKindWS { - fn from(sk: SampleKind) -> Self { - match sk { - SampleKind::Put => SampleKindWS::Put, - SampleKind::Delete => SampleKindWS::Delete, - } +pub(crate) struct UndeclarePublisher { + pub(crate) id: u32, +} + +impl UndeclarePublisher { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(UndeclarePublisher { + id: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct DeclareSubscriber { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) allowed_origin: Locality, +} + +impl DeclareSubscriber { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(DeclareSubscriber { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + allowed_origin: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct UndeclareSubscriber { + pub(crate) id: u32, +} + +impl UndeclareSubscriber { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(UndeclareSubscriber { + id: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct DeclareQueryable { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) complete: bool, + pub(crate) allowed_origin: Locality, +} + +impl DeclareQueryable { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(DeclareQueryable { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + complete: deserializer.deserialize()?, + allowed_origin: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct UndeclareQueryable { + pub(crate) id: u32, +} + +impl UndeclareQueryable { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(UndeclareQueryable { + id: deserializer.deserialize()?, + }) } } -impl From<&Sample> for SampleWS { - fn from(s: &Sample) -> Self { - let z_bytes: Vec = s.payload().to_bytes().to_vec(); - - SampleWS { - key_expr: s.key_expr().to_owned().into(), - value: BASE64_STANDARD.encode(z_bytes).into(), - kind: s.kind().into(), - timestamp: s.timestamp().map(|x| x.to_string()), - priority: s.priority() as u8, - congestion_control: s.congestion_control() as u8, - encoding: s.encoding().to_string(), - express: s.express(), - attachement: s - .attachment() - .map(|x| x.to_bytes().to_vec()) - .map(|z_bytes| BASE64_STANDARD.encode(z_bytes).into()), +pub(crate) struct DeclareQuerier { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) target: QueryTarget, + pub(crate) accept_replies: ReplyKeyExpr, + pub(crate) timeout_ms: u64, + pub(crate) consolidation: ConsolidationMode, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, + pub(crate) allowed_destination: Locality, +} + +impl DeclareQuerier { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(DeclareQuerier { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + target: query_target_from_u8(deserializer.deserialize()?)?, + accept_replies: reply_keyexpr_from_u8(deserializer.deserialize()?)?, + timeout_ms: deserializer.deserialize()?, + consolidation: consolidation_from_u8(deserializer.deserialize()?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct UndeclareQuerier { + pub(crate) id: u32, +} + +impl UndeclareQuerier { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(UndeclareQuerier { + id: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct GetSessionInfo {} + +impl GetSessionInfo { + pub(crate) fn from_wire( + _deserializer: &mut ZDeserializer, + ) -> Result { + Ok(GetSessionInfo {}) + } +} + +pub(crate) struct ResponseSessionInfo { + pub(crate) zid: ZenohId, + pub(crate) z_routers: Vec, + pub(crate) z_peers: Vec, +} + +impl ResponseSessionInfo { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(self.zid.to_le_bytes()); + serializer.serialize_iter(self.z_routers.iter().map(|z| z.to_le_bytes())); + serializer.serialize_iter(self.z_peers.iter().map(|z| z.to_le_bytes())); + } +} + +pub(crate) struct GetTimestamp {} + +impl GetTimestamp { + pub(crate) fn from_wire( + _deserializer: &mut ZDeserializer, + ) -> Result { + Ok(GetTimestamp {}) + } +} + +pub(crate) struct ResponseTimestamp { + pub(crate) timestamp: Timestamp, +} + +impl ResponseTimestamp { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(timestamp_to_ntp_id(&self.timestamp)); + } +} + +pub(crate) struct Put { + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) payload: Vec, + pub(crate) encoding: Encoding, + pub(crate) attachment: Option>, + pub(crate) timestamp: Option, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, + pub(crate) reliability: Reliability, + pub(crate) allowed_destination: Locality, +} + +impl Put { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(Put { + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + payload: deserializer.deserialize()?, + encoding: encoding_from_id_schema(deserializer.deserialize()?), + attachment: deserialize_option(deserializer)?, + timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + reliability: reliability_from_u8(deserializer.deserialize()?)?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct Delete { + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) attachment: Option>, + pub(crate) timestamp: Option, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, + pub(crate) reliability: Reliability, + pub(crate) allowed_destination: Locality, +} + +impl Delete { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(Delete { + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + attachment: deserialize_option(deserializer)?, + timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + reliability: reliability_from_u8(deserializer.deserialize()?)?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct PublisherPut { + pub(crate) publisher_id: u32, + pub(crate) payload: Vec, + pub(crate) encoding: Option, + pub(crate) attachment: Option>, + pub(crate) timestamp: Option, +} + +impl PublisherPut { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(PublisherPut { + publisher_id: deserializer.deserialize()?, + payload: deserializer.deserialize()?, + encoding: opt_encoding_from_id_schema(deserialize_option(deserializer)?), + attachment: deserialize_option(deserializer)?, + timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, + }) + } +} + +pub(crate) struct PublisherDelete { + pub(crate) publisher_id: u32, + pub(crate) attachment: Option>, + pub(crate) timestamp: Option, +} + +impl PublisherDelete { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(PublisherDelete { + publisher_id: deserializer.deserialize()?, + attachment: deserialize_option(deserializer)?, + timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, + }) + } +} + +pub(crate) struct Get { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) parameters: Option, + pub(crate) payload: Option>, + pub(crate) encoding: Option, + pub(crate) attachment: Option>, + pub(crate) target: QueryTarget, + pub(crate) accept_replies: ReplyKeyExpr, + pub(crate) timeout_ms: u64, + pub(crate) consolidation: ConsolidationMode, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, + pub(crate) allowed_destination: Locality, +} + +impl Get { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(Get { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + parameters: deserialize_option(deserializer)?, + payload: deserialize_option(deserializer)?, + encoding: opt_encoding_from_id_schema(deserialize_option(deserializer)?), + attachment: deserialize_option(deserializer)?, + target: query_target_from_u8(deserializer.deserialize()?)?, + accept_replies: reply_keyexpr_from_u8(deserializer.deserialize()?)?, + timeout_ms: deserializer.deserialize()?, + consolidation: consolidation_from_u8(deserializer.deserialize()?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + }) + } +} + +pub(crate) struct QuerierGet { + pub(crate) querier_id: u32, + pub(crate) id: u32, + pub(crate) parameters: Option, + pub(crate) payload: Option>, + pub(crate) encoding: Option, + pub(crate) attachment: Option>, +} + +impl QuerierGet { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(QuerierGet { + querier_id: deserializer.deserialize()?, + id: deserializer.deserialize()?, + parameters: deserialize_option(deserializer)?, + payload: deserialize_option(deserializer)?, + encoding: opt_encoding_from_id_schema(deserialize_option(deserializer)?), + attachment: deserialize_option(deserializer)?, + }) + } +} + +fn serialize_sample(serializer: &mut ZSerializer, sample: &zenoh::sample::Sample) { + serializer.serialize(sample.key_expr().as_str()); + serializer.serialize(&sample.payload().to_bytes()); + serializer.serialize(sample_kind_to_u8(sample.kind())); + serializer.serialize(encoding_to_id_schema(&sample.encoding())); + serialize_option(serializer, &sample.attachment().map(|a| a.to_bytes())); + serialize_option( + serializer, + &sample.timestamp().map(|t| timestamp_to_ntp_id(t)), + ); + serializer.serialize(congestion_control_to_u8(sample.congestion_control())); + serializer.serialize(priority_to_u8(sample.priority())); + serializer.serialize(sample.express()); + serializer.serialize(reliability_to_u8(sample.reliability())); +} + +pub(crate) struct Sample { + pub(crate) subscriber_id: u32, + pub(crate) sample: zenoh::sample::Sample, +} + +impl Sample { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(&self.subscriber_id); + serialize_sample(serializer, &self.sample); + } +} + +pub(crate) struct Query { + pub(crate) queryable_id: u32, + pub(crate) query_id: u32, + pub(crate) query: zenoh::query::Query, +} + +fn serialize_query(serializer: &mut ZSerializer, query: &zenoh::query::Query) { + serializer.serialize(query.key_expr().as_str()); + serializer.serialize(query.parameters().as_str()); + serialize_option(serializer, &query.payload().map(|p| p.to_bytes())); + serialize_option( + serializer, + &query.encoding().map(|e| encoding_to_id_schema(e)), + ); + serialize_option(serializer, &query.attachment().map(|a| a.to_bytes())); + serializer.serialize(reply_keyexpr_to_u8( + query + .accepts_replies() + .unwrap_or(ReplyKeyExpr::MatchingQuery), + )); +} + +impl Query { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(&self.queryable_id); + serializer.serialize(&self.query_id); + serialize_query(serializer, &self.query); + } +} + +pub(crate) struct Reply { + pub(crate) query_id: u32, + pub(crate) reply: zenoh::query::Reply, +} + +fn serialize_reply(serializer: &mut ZSerializer, reply: &zenoh::query::Reply) { + match reply.result() { + Ok(s) => { + serializer.serialize(true); + serialize_sample(serializer, s); + } + Err(e) => { + serializer.serialize(false); + serializer.serialize(encoding_to_id_schema(&e.encoding())); + serializer.serialize(&e.payload().to_bytes()); } } } -impl From for SampleWS { - fn from(s: Sample) -> Self { - SampleWS::from(&s) +impl Reply { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(&self.query_id); + serialize_reply(serializer, &self.reply); } } -#[cfg(test)] -mod tests { +pub(crate) struct ReplyOk { + pub(crate) query_id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) payload: Vec, + pub(crate) encoding: Encoding, + pub(crate) attachment: Option>, + pub(crate) timestamp: Option, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, +} - use std::str::FromStr; +impl ReplyOk { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(ReplyOk { + query_id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + payload: deserializer.deserialize()?, + encoding: encoding_from_id_schema(deserializer.deserialize()?), + attachment: deserialize_option(deserializer)?, + timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct ReplyDel { + pub(crate) query_id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) attachment: Option>, + pub(crate) timestamp: Option, + pub(crate) congestion_control: CongestionControl, + pub(crate) priority: Priority, + pub(crate) express: bool, +} + +impl ReplyDel { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(ReplyDel { + query_id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + attachment: deserialize_option(deserializer)?, + timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, + congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, + priority: priority_from_u8(deserializer.deserialize()?)?, + express: deserializer.deserialize()?, + }) + } +} - use uuid::Uuid; - use zenoh::key_expr::KeyExpr; +pub(crate) struct ReplyErr { + pub(crate) query_id: u32, + pub(crate) payload: Vec, + pub(crate) encoding: Encoding, +} - use super::*; +impl ReplyErr { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(ReplyErr { + query_id: deserializer.deserialize()?, + payload: deserializer.deserialize()?, + encoding: encoding_from_id_schema(deserializer.deserialize()?), + }) + } +} - #[test] - fn test_b64_serializing() { - let bytes: Vec = std::iter::repeat(245).take(100).collect(); +pub(crate) struct QueryResponseFinal { + pub(crate) query_id: u32, +} - let b64_string = BASE64_STANDARD.encode(bytes.clone()); +impl QueryResponseFinal { + pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { + serializer.serialize(&self.query_id); + } - #[derive(Debug, Serialize, Deserialize)] - struct RawBytes { - bytes: Vec, + pub(crate) fn from_wire( + deserializer: &mut ZDeserializer, + ) -> Result { + Ok(QueryResponseFinal { + query_id: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct DeclareLivelinessToken { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, +} + +impl DeclareLivelinessToken { + pub(crate) fn from_wire( + deserializer: &mut ZDeserializer, + ) -> Result { + Ok(DeclareLivelinessToken { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + }) + } +} + +pub(crate) struct UndeclareLivelinessToken { + pub(crate) id: u32, +} + +impl UndeclareLivelinessToken { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(UndeclareLivelinessToken { + id: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct DeclareLivelinessSubscriber { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) history: bool, +} + +impl DeclareLivelinessSubscriber { + pub(crate) fn from_wire( + deserializer: &mut ZDeserializer, + ) -> Result { + Ok(DeclareLivelinessSubscriber { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + history: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct UndeclareLivelinessSubscriber { + pub(crate) id: u32, +} + +impl UndeclareLivelinessSubscriber { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(UndeclareLivelinessSubscriber { + id: deserializer.deserialize()?, + }) + } +} + +pub(crate) struct LivelinessGet { + pub(crate) id: u32, + pub(crate) keyexpr: OwnedKeyExpr, + pub(crate) timeout_ms: u64, +} + +impl LivelinessGet { + pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { + Ok(LivelinessGet { + id: deserializer.deserialize()?, + keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, + timeout_ms: deserializer.deserialize()?, + }) + } +} + +macro_rules! count { + () => (0usize); + ( $x:tt $($xs:tt)* ) => (1usize + count!($($xs)*)); +} + +macro_rules! remote_message_inner { + ($typ:ty, $enum_name:ident, $name:ident, $access:vis, $( #[$meta:meta] )* $($val:ident,)*) => { + #[repr($typ)] + #[derive(Clone, Copy, Debug)] + $access enum $enum_name { + $($val,)* } - #[derive(Debug, Serialize, Deserialize)] - struct B64Encoded { - b64_string: String, + + impl From<$enum_name> for $typ { + fn from(enm: $enum_name) -> Self { + enm as $typ + } } - let json_bytes = serde_json::to_string(&RawBytes { bytes }).unwrap(); - let json_b64 = serde_json::to_string(&B64Encoded { b64_string }).unwrap(); - assert!(json_b64.len() < json_bytes.len()) - } - - #[test] - fn serialize_messages() { - let json: String = - serde_json::to_string(&RemoteAPIMsg::Control(ControlMsg::OpenSession)).unwrap(); - assert_eq!(json, r#"{"Control":"OpenSession"}"#); - - let uuid = Uuid::from_str("a2663bb1-128c-4dd3-a42b-d1d3337e2e51").unwrap(); - - let json: String = - serde_json::to_string(&RemoteAPIMsg::Control(ControlMsg::Session(uuid))).unwrap(); - assert_eq!( - json, - r#"{"Control":{"Session":"a2663bb1-128c-4dd3-a42b-d1d3337e2e51"}}"# - ); - - let json: String = - serde_json::to_string(&RemoteAPIMsg::Control(ControlMsg::CloseSession)).unwrap(); - assert_eq!(json, r#"{"Control":"CloseSession"}"#); - - let key_expr: OwnedKeyExpr = KeyExpr::new("demo/test").unwrap().to_owned().into(); - - let _sample_ws = SampleWS { - key_expr: key_expr.clone(), - value: BASE64_STANDARD.encode(vec![1, 2, 3]).into(), - kind: SampleKindWS::Put, - encoding: "zenoh/bytes".into(), - timestamp: None, - priority: 1, - congestion_control: 1, - express: false, - attachement: None, - }; - - let sample_ws = SampleWS { - key_expr, - value: BASE64_STANDARD.encode(vec![1, 2, 3]).into(), - kind: SampleKindWS::Put, - encoding: "zenoh/bytes".into(), - timestamp: None, - priority: 1, - congestion_control: 1, - express: false, - attachement: None, - }; - let _json: String = serde_json::to_string(&DataMsg::Sample(sample_ws, uuid)).unwrap(); + impl TryFrom<$typ> for $enum_name { + type Error = zenoh_result::Error; + fn try_from(x: $typ) -> Result { + const VALS: [$enum_name; count!($($val)*)] = [$($enum_name::$val,)*]; + if VALS.len() > x as usize { + Ok(VALS[x as usize]) + } else { + bail!("Unsupported {} {} value", stringify!($enum_name), x); + } + } + } + $( #[$meta] )* + $access enum $name { + $($val($val),)* + } } } + +macro_rules! remote_message { + ( @from_wire + #[repr($typ:ty)] + $( #[$meta:meta] )* + $access:vis enum $name:ident { + $($val:ident,)* + }, + $enum_name:ident + ) => { + remote_message_inner!{$typ, $enum_name, $name, $access, $( #[$meta] )* $($val,)* } + + #[derive(Copy, Clone, Debug)] + $access struct Header { + $access content_id: $enum_name, + $access sequence_id: Option, + } + + $access enum FromWireError { + HeaderError(zenoh_result::Error), + BodyError((Header, zenoh_result::Error)) + } + + impl From for FromWireError { + fn from(e: zenoh_result::Error) -> Self { + Self::HeaderError(e) + } + } + + impl From for FromWireError { + fn from(e: ZDeserializeError) -> Self { + Self::HeaderError(e.into()) + } + } + + impl $name { + $access fn from_wire(data: Vec) -> Result<(Header, $name), FromWireError> { + let z_bytes: ZBytes = data.into(); + let mut deserializer = ZDeserializer::new(&z_bytes); + let t: $typ = deserializer.deserialize()?; + let requires_ack = (t & 0b10000000u8) != 0; + let enum_t: $enum_name = (t & 0b01111111u8).try_into()?; + match enum_t { + $($enum_name::$val => { + let header = Header { + content_id: enum_t, + sequence_id: requires_ack.then_some(deserializer.deserialize::()?) + }; + Ok((header, $name::$val($val::from_wire(&mut deserializer).map_err(|e| FromWireError::BodyError((header, e.into())))?))) + },)* + } + } + } + }; + ( @to_wire + #[repr($typ:ty)] + $( #[$meta:meta] )* + $access:vis enum $name:ident { + $($val:ident,)* + }, + $enum_name:ident + ) => { + remote_message_inner!{$typ, $enum_name, $name, $access, $( #[$meta] )* $($val,)* } + impl $name { + $access fn to_wire(&self, sequence_id: Option) -> Vec { + let mut serializer = ZSerializer::new(); + match self { + $($name::$val(x) => { + let t: $typ = $enum_name::$val.into(); + serializer.serialize(t); + if let Some(id) = sequence_id { + serializer.serialize(id); + } + x.to_wire(&mut serializer); + serializer.finish().to_bytes().to_vec() + },)* + } + } + } + }; +} + +remote_message! { + @from_wire + #[repr(u8)] + pub(crate) enum InRemoteMessage { + DeclarePublisher, + UndeclarePublisher, + DeclareSubscriber, + UndeclareSubscriber, + DeclareQueryable, + UndeclareQueryable, + DeclareQuerier, + UndeclareQuerier, + DeclareLivelinessToken, + UndeclareLivelinessToken, + DeclareLivelinessSubscriber, + UndeclareLivelinessSubscriber, + GetSessionInfo, + GetTimestamp, + Put, + Delete, + PublisherPut, + PublisherDelete, + Get, + QuerierGet, + LivelinessGet, + ReplyOk, + ReplyDel, + ReplyErr, + QueryResponseFinal, + }, + InRemoteMessageId +} + +remote_message! { + @to_wire + #[repr(u8)] + pub(crate) enum OutRemoteMessage { + OpenAck, + Ok, + Error, + ResponseTimestamp, + ResponseSessionInfo, + Sample, + Query, + Reply, + QueryResponseFinal, + }, + OutRemoteMessageId +} diff --git a/zenoh-plugin-remote-api/src/interface/remote_message.rs b/zenoh-plugin-remote-api/src/interface/remote_message.rs deleted file mode 100644 index 5830589c..00000000 --- a/zenoh-plugin-remote-api/src/interface/remote_message.rs +++ /dev/null @@ -1,717 +0,0 @@ -// -// Copyright (c) 2025 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use core::str; -use std::{ops::Not, str::FromStr}; - -use uhlc::{Timestamp, NTP64}; -use uuid::Uuid; -use zenoh::{ - bytes::{Encoding, ZBytes}, - config::ZenohId, - key_expr::OwnedKeyExpr, - qos::{CongestionControl, Priority, Reliability}, - query::{ConsolidationMode, QueryTarget, ReplyKeyExpr}, - sample::Locality, -}; - -use zenoh_ext::{Deserialize, Serialize, ZDeserializeError, ZDeserializer, ZSerializer}; -use zenoh_result::bail; - -pub(crate) fn serialize_option(serializer: &mut ZSerializer, o: &Option) { - match o { - Some(v) => { - serializer.serialize(true); - serializer.serialize(v); - } - None => { - serializer.serialize(false); - } - } -} - -pub(crate) fn deserialize_option( - deserializer: &mut zenoh_ext::ZDeserializer, -) -> Result, ZDeserializeError> { - let has_value = deserializer.deserialize::()?; - match has_value { - true => Ok(Some(deserializer.deserialize()?)), - false => Ok(None), - } -} - -pub(crate) struct OpenSession { - pub(crate) id: Uuid, -} - -impl OpenSession { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(OpenSession { - id: Uuid::from_str(&deserializer.deserialize::()?)?, - }) - } -} - -pub(crate) struct CloseSession { - pub(crate) id: Uuid, -} - -impl CloseSession { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(CloseSession { - id: Uuid::from_str(&deserializer.deserialize::()?)?, - }) - } -} - -pub(crate) struct ResultWithId { - pub(crate) id: T, - pub(crate) error: Option, -} - -impl ResultWithId { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(&self.id); - serialize_option(&mut serializer, &self.error); - serializer.finish() - } -} - -fn congestion_control_from_u8(c: u8) -> Result { - match c { - 0 => Ok(CongestionControl::Drop), - 1 => Ok(CongestionControl::Block), - v => bail!("Unsupported congestion control value {}", v), - } -} - -fn congestion_control_to_u8(c: CongestionControl) -> u8 { - match c { - CongestionControl::Drop => 0, - CongestionControl::Block => 1, - } -} - -fn priority_from_u8(p: u8) -> Result { - p.try_into() -} - -fn priority_to_u8(p: Priority) -> u8 { - p as u8 -} - -fn reliability_from_u8(r: u8) -> Result { - match r { - 0 => Ok(Reliability::BestEffort), - 1 => Ok(Reliability::Reliable), - v => bail!("Unsupported reliability value {}", v), - } -} - -fn reliability_to_u8(r: Reliability) -> u8 { - match r { - Reliability::BestEffort => 0, - Reliability::Reliable => 1, - } -} - -fn locality_from_u8(l: u8) -> Result { - match l { - 0 => Ok(Locality::Any), - 1 => Ok(Locality::Remote), - 2 => Ok(Locality::SessionLocal), - v => bail!("Unsupported locality value {}", v), - } -} - -fn consolidation_from_u8(l: u8) -> Result { - match l { - 0 => Ok(ConsolidationMode::Auto), - 1 => Ok(ConsolidationMode::None), - 2 => Ok(ConsolidationMode::Monotonic), - 3 => Ok(ConsolidationMode::Latest), - v => bail!("Unsupported consolidation mode value {}", v), - } -} - -fn query_target_from_u8(t: u8) -> Result { - match t { - 0 => Ok(QueryTarget::All), - 1 => Ok(QueryTarget::AllComplete), - 2 => Ok(QueryTarget::BestMatching), - v => bail!("Unsupported query target value {}", v), - } -} - -fn reply_keyexpr_from_u8(a: u8) -> Result { - match a { - 0 => Ok(ReplyKeyExpr::Any), - 1 => Ok(ReplyKeyExpr::MatchingQuery), - v => bail!("Unsupported reply keyexpr value {}", v), - } -} - -fn reply_keyexpr_to_u8(a: ReplyKeyExpr) -> u8 { - match a { - ReplyKeyExpr::Any => 0, - ReplyKeyExpr::MatchingQuery => 1, - } -} - -fn encoding_from_id_schema(id_schema: (u16, String)) -> Encoding { - Encoding::new( - id_schema.0, - id_schema - .1 - .is_empty() - .not() - .then(|| id_schema.1.into_bytes().into()), - ) -} - -fn encoding_to_id_schema(encoding: &Encoding) -> (u16, &[u8]) { - ( - encoding.id(), - match encoding.schema() { - Some(s) => s.as_slice(), - None => &[], - }, - ) -} - -fn opt_encoding_from_id_schema(id_schema: Option<(u16, String)>) -> Option { - id_schema.map(|x| encoding_from_id_schema(x)) -} - -fn opt_timestamp_from_ntp_id( - ntp_id: Option<(u64, [u8; 16])>, -) -> Result, zenoh_result::Error> { - Ok(match ntp_id { - Some((t, id)) => Some(Timestamp::new(NTP64(t), id.try_into()?)), - None => None, - }) -} - -fn timestamp_to_ntp_id(t: &Timestamp) -> (u64, [u8; 16]) { - (t.get_time().0, t.get_id().to_le_bytes()) -} - -pub(crate) struct DeclarePublisher { - pub(crate) id: u32, - pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) encoding: Encoding, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) reliability: Reliability, - pub(crate) allowed_destination: Locality, -} - -impl DeclarePublisher { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(DeclarePublisher { - id: deserializer.deserialize::()?, - keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - encoding: encoding_from_id_schema(deserializer.deserialize()?), - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - reliability: reliability_from_u8(deserializer.deserialize()?)?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, - }) - } -} - -pub(crate) struct UndeclarePublisher { - pub(crate) id: u32, -} - -impl UndeclarePublisher { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(UndeclarePublisher { - id: deserializer.deserialize::()?, - }) - } -} - -pub(crate) struct DeclareSubscriber { - pub(crate) id: u32, - pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) allowed_origin: Locality, -} - -impl DeclareSubscriber { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(DeclareSubscriber { - id: deserializer.deserialize::()?, - keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - allowed_origin: locality_from_u8(deserializer.deserialize()?)?, - }) - } -} - -pub(crate) struct UndeclareSubscriber { - pub(crate) id: u32, -} - -impl UndeclareSubscriber { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(UndeclareSubscriber { - id: deserializer.deserialize::()?, - }) - } -} - -pub(crate) struct DeclareQueryable { - pub(crate) id: u32, - pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) complete: bool, - pub(crate) allowed_origin: Locality, -} - -impl DeclareQueryable { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(DeclareQueryable { - id: deserializer.deserialize::()?, - keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - complete: deserializer.deserialize()?, - allowed_origin: locality_from_u8(deserializer.deserialize()?)?, - }) - } -} - -pub(crate) struct UndeclareQueryable { - pub(crate) id: u32, -} - -impl UndeclareQueryable { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(UndeclareQueryable { - id: deserializer.deserialize::()?, - }) - } -} - -pub(crate) struct DeclareQuerier { - pub(crate) id: u32, - pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) target: QueryTarget, - pub(crate) accept_replies: ReplyKeyExpr, - pub(crate) timeout_ms: u64, - pub(crate) consolidation: ConsolidationMode, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) reliability: Reliability, - pub(crate) allowed_destination: Locality, -} - -impl DeclareQuerier { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(DeclareQuerier { - id: deserializer.deserialize::()?, - keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - target: query_target_from_u8(deserializer.deserialize()?)?, - accept_replies: reply_keyexpr_from_u8(deserializer.deserialize()?)?, - timeout_ms: deserializer.deserialize()?, - consolidation: consolidation_from_u8(deserializer.deserialize()?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - reliability: reliability_from_u8(deserializer.deserialize()?)?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, - }) - } -} - -pub(crate) struct UndeclareQuerier { - pub(crate) id: u32, -} - -impl UndeclareQuerier { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(UndeclareQuerier { - id: deserializer.deserialize::()?, - }) - } -} - -pub(crate) struct GetSessionInfo { - pub(crate) id: u32, -} - -impl GetSessionInfo { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(GetSessionInfo { - id: deserializer.deserialize::()?, - }) - } -} - -pub(crate) struct ResponseSessionInfo { - pub(crate) id: u32, - pub(crate) zid: ZenohId, - pub(crate) z_routers: Vec, - pub(crate) z_peers: Vec, -} - -impl ResponseSessionInfo { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(self.id); - serializer.serialize(self.zid.to_le_bytes()); - serializer.serialize_iter(self.z_routers.iter().map(|z| z.to_le_bytes())); - serializer.serialize_iter(self.z_peers.iter().map(|z| z.to_le_bytes())); - serializer.finish() - } -} - -pub(crate) struct GetTimestamp { - pub(crate) id: u32, -} - -impl GetTimestamp { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(GetTimestamp { - id: deserializer.deserialize::()?, - }) - } -} - -pub(crate) struct ResponseTimestamp { - pub(crate) id: u32, - pub(crate) timestamp: Timestamp, -} - -impl ResponseTimestamp { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(&self.id); - serializer.serialize(timestamp_to_ntp_id(&self.timestamp)); - serializer.finish() - } -} - -pub(crate) struct Put { - pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) payload: Vec, - pub(crate) encoding: Encoding, - pub(crate) attachment: Option>, - pub(crate) timestamp: Option, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) reliability: Reliability, - pub(crate) allowed_destination: Locality, -} - -impl Put { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(Put { - keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - payload: deserializer.deserialize()?, - encoding: encoding_from_id_schema(deserializer.deserialize()?), - attachment: deserialize_option(&mut deserializer)?, - timestamp: opt_timestamp_from_ntp_id(deserialize_option(&mut deserializer)?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - reliability: reliability_from_u8(deserializer.deserialize()?)?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, - }) - } -} - -pub(crate) struct PublisherPut { - pub(crate) publisher_id: u32, - pub(crate) payload: Vec, - pub(crate) encoding: Encoding, - pub(crate) attachment: Option>, - pub(crate) timestamp: Option, -} - -impl PublisherPut { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(PublisherPut { - publisher_id: deserializer.deserialize()?, - payload: deserializer.deserialize()?, - encoding: encoding_from_id_schema(deserializer.deserialize()?), - attachment: deserialize_option(&mut deserializer)?, - timestamp: opt_timestamp_from_ntp_id(deserialize_option(&mut deserializer)?)?, - }) - } -} - -pub(crate) struct Get { - pub(crate) id: u32, - pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) parameters: Option, - pub(crate) payload: Option>, - pub(crate) encoding: Option, - pub(crate) attachment: Option>, - pub(crate) target: QueryTarget, - pub(crate) accept_replies: ReplyKeyExpr, - pub(crate) timeout_ms: u64, - pub(crate) consolidation: ConsolidationMode, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) reliability: Reliability, - pub(crate) allowed_destination: Locality, -} - -impl Get { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(Get { - id: deserializer.deserialize()?, - keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - parameters: deserialize_option(&mut deserializer)?, - payload: deserialize_option(&mut deserializer)?, - encoding: opt_encoding_from_id_schema(deserialize_option(&mut deserializer)?), - attachment: deserialize_option(&mut deserializer)?, - target: query_target_from_u8(deserializer.deserialize()?)?, - accept_replies: reply_keyexpr_from_u8(deserializer.deserialize()?)?, - timeout_ms: deserializer.deserialize()?, - consolidation: consolidation_from_u8(deserializer.deserialize()?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - reliability: reliability_from_u8(deserializer.deserialize()?)?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, - }) - } -} - -pub(crate) struct QuerierGet { - pub(crate) querier_id: u32, - pub(crate) id: u32, - pub(crate) parameters: Option, - pub(crate) payload: Option>, - pub(crate) encoding: Option, - pub(crate) attachment: Option>, -} - -impl QuerierGet { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(QuerierGet { - querier_id: deserializer.deserialize()?, - id: deserializer.deserialize()?, - parameters: deserialize_option(&mut deserializer)?, - payload: deserialize_option(&mut deserializer)?, - encoding: opt_encoding_from_id_schema(deserialize_option(&mut deserializer)?), - attachment: deserialize_option(&mut deserializer)?, - }) - } -} - -fn serialize_sample(serializer: &mut ZSerializer, sample: &zenoh::sample::Sample) { - serializer.serialize(sample.key_expr().as_str()); - serializer.serialize(sample.payload().to_bytes()); - serializer.serialize(encoding_to_id_schema(sample.encoding())); - serialize_option(serializer, &sample.attachment().map(|a| a.to_bytes())); - serialize_option( - serializer, - &sample.timestamp().map(|t| timestamp_to_ntp_id(t)), - ); - serializer.serialize(congestion_control_to_u8(sample.congestion_control())); - serializer.serialize(priority_to_u8(sample.priority())); - serializer.serialize(sample.express()); - serializer.serialize(reliability_to_u8(sample.reliability())); -} - -pub(crate) struct Sample { - pub(crate) subscriber_id: u32, - pub(crate) sample: zenoh::sample::Sample, -} - -impl Sample { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(&self.subscriber_id); - serialize_sample(&mut serializer, &self.sample); - - serializer.finish() - } -} - -pub(crate) struct Query { - pub(crate) queryable_id: u32, - pub(crate) query_id: u32, - pub(crate) query: zenoh::query::Query, -} - -fn serialize_query(serializer: &mut ZSerializer, query: &zenoh::query::Query) { - serializer.serialize(query.key_expr().as_str()); - serializer.serialize(query.parameters().as_str()); - serialize_option(serializer, &query.payload().map(|p| p.to_bytes())); - serialize_option( - serializer, - &query.encoding().map(|e| encoding_to_id_schema(e)), - ); - serialize_option(serializer, &query.attachment().map(|a| a.to_bytes())); - serializer.serialize(reply_keyexpr_to_u8( - query - .accepts_replies() - .unwrap_or(ReplyKeyExpr::MatchingQuery), - )); -} - -impl Query { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(&self.queryable_id); - serializer.serialize(&self.query_id); - serialize_query(&mut serializer, &self.query); - - serializer.finish() - } -} - -pub(crate) struct Reply { - pub(crate) query_id: u32, - pub(crate) reply: zenoh::query::Reply, -} - -fn serialize_reply(serializer: &mut ZSerializer, reply: &zenoh::query::Reply) { - match reply.result() { - Ok(r) => { - serializer.serialize(true); - serialize_sample(serializer, r); - } - Err(e) => { - serializer.serialize(false); - serializer.serialize(encoding_to_id_schema(e.encoding())); - serializer.serialize(e.payload().to_bytes()); - } - } -} - -impl Reply { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(&self.query_id); - serialize_reply(&mut serializer, &self.reply); - - serializer.finish() - } -} - -pub(crate) struct ResponseFinal { - query_id: u32, -} - -impl ResponseFinal { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(&self.query_id); - serializer.finish() - } - - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(ResponseFinal { - query_id: deserializer.deserialize()?, - }) - } -} - -pub(crate) struct DeclareLivelinessToken { - pub(crate) token_id: u32, -} - -impl DeclareLivelinessToken { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(DeclareLivelinessToken { - token_id: deserializer.deserialize()?, - }) - } -} - -pub(crate) struct UndeclareLivelinessToken { - pub(crate) token_id: u32, -} - -impl UndeclareLivelinessToken { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(&self.token_id); - serializer.finish() - } -} - -pub(crate) struct DeclareLivelinessSubscriber { - pub(crate) subscriber_id: u32, - pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) history: bool, -} - -impl DeclareLivelinessSubscriber { - pub(crate) fn from_wire( - data: ZBytes, - ) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(DeclareLivelinessSubscriber { - subscriber_id: deserializer.deserialize()?, - keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - history: deserializer.deserialize()?, - }) - } -} - -pub(crate) struct UndeclareLivelinessSubscriber { - pub(crate) subscriber_id: u32, -} - -impl UndeclareLivelinessSubscriber { - pub(crate) fn to_wire(&self) -> ZBytes { - let mut serializer = ZSerializer::new(); - serializer.serialize(&self.subscriber_id); - serializer.finish() - } -} - -pub(crate) struct LivelinessGet { - pub(crate) id: u32, - pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) timeout_ms: u64, -} - -impl LivelinessGet { - pub(crate) fn from_wire(data: ZBytes) -> Result { - let mut deserializer = ZDeserializer::new(&data); - Ok(LivelinessGet { - id: deserializer.deserialize()?, - keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - timeout_ms: deserializer.deserialize()?, - }) - } -} diff --git a/zenoh-plugin-remote-api/src/interface/ser_de.rs b/zenoh-plugin-remote-api/src/interface/ser_de.rs deleted file mode 100644 index 964d2e70..00000000 --- a/zenoh-plugin-remote-api/src/interface/ser_de.rs +++ /dev/null @@ -1,258 +0,0 @@ -use serde::{Deserialize, Deserializer, Serializer}; -use zenoh::{ - qos::{CongestionControl, Priority, Reliability}, - query::{ConsolidationMode, QueryTarget, ReplyKeyExpr}, - sample::Locality, -}; - -pub fn deserialize_consolidation_mode<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Option::::deserialize(d) { - Ok(Some(value)) => Ok(Some(match value { - 0u8 => ConsolidationMode::Auto, - 1u8 => ConsolidationMode::None, - 2u8 => ConsolidationMode::Monotonic, - 3u8 => ConsolidationMode::Latest, - _ => { - return Err(serde::de::Error::custom(format!( - "Value not valid for ConsolidationMode Enum {:?}", - value - ))) - } - })), - Ok(None) => Ok(None), - Err(err) => Err(serde::de::Error::custom(format!( - "Value not valid for ConsolidationMode Enum {:?}", - err - ))), - } -} - -pub fn serialize_consolidation_mode( - consolidation_mode: &Option, - s: S, -) -> Result -where - S: Serializer, -{ - match consolidation_mode { - Some(c_mode) => s.serialize_u8(*c_mode as u8), - None => s.serialize_none(), - } -} - -pub fn deserialize_reply_key_expr<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Option::::deserialize(d) { - Ok(Some(value)) => Ok(Some(match value { - 0u8 => ReplyKeyExpr::Any, - 1u8 => ReplyKeyExpr::MatchingQuery, - _ => { - return Err(serde::de::Error::custom(format!( - "Value not valid for ReplyKeyExpr Enum {:?}", - value - ))) - } - })), - Ok(None) => Ok(None), - Err(err) => Err(serde::de::Error::custom(format!( - "Value not valid for ReplyKeyExpr Enum {:?}", - err - ))), - } -} - -pub fn serialize_reply_key_expr( - consolidation_mode: &Option, - s: S, -) -> Result -where - S: Serializer, -{ - match consolidation_mode { - Some(c_mode) => s.serialize_u8(*c_mode as u8), - None => s.serialize_none(), - } -} - -pub fn deserialize_congestion_control<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Option::::deserialize(d) { - Ok(Some(value)) => Ok(Some(match value { - 0u8 => CongestionControl::Drop, - 1u8 => CongestionControl::Block, - val => { - return Err(serde::de::Error::custom(format!( - "Value not valid for CongestionControl Enum {:?}", - val - ))) - } - })), - Ok(None) => Ok(None), - val => Err(serde::de::Error::custom(format!( - "Value not valid for CongestionControl Enum {:?}", - val - ))), - } -} - -pub fn serialize_congestion_control( - congestion_control: &Option, - s: S, -) -> Result -where - S: Serializer, -{ - match congestion_control { - Some(c_ctrl) => s.serialize_u8(*c_ctrl as u8), - None => s.serialize_none(), - } -} - -pub fn deserialize_priority<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Option::::deserialize(d) { - Ok(Some(value)) => Ok(Some(match value { - 1u8 => Priority::RealTime, - 2u8 => Priority::InteractiveHigh, - 3u8 => Priority::InteractiveLow, - 4u8 => Priority::DataHigh, - 5u8 => Priority::Data, - 6u8 => Priority::DataLow, - 7u8 => Priority::Background, - val => { - return Err(serde::de::Error::custom(format!( - "Value not valid for Priority Enum {:?}", - val - ))) - } - })), - Ok(None) => Ok(None), - val => Err(serde::de::Error::custom(format!( - "Value not valid for Priority Enum {:?}", - val - ))), - } -} - -pub fn serialize_priority(priority: &Option, s: S) -> Result -where - S: Serializer, -{ - match priority { - Some(prio) => s.serialize_u8(*prio as u8), - None => s.serialize_none(), - } -} - -pub fn deserialize_reliability<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Option::::deserialize(d) { - Ok(Some(value)) => Ok(Some(match value { - 0u8 => Reliability::BestEffort, - 1u8 => Reliability::Reliable, - val => { - return Err(serde::de::Error::custom(format!( - "Value not valid for Reliability Enum {:?}", - val - ))) - } - })), - Ok(None) => Ok(None), - val => Err(serde::de::Error::custom(format!( - "Value not valid for Reliability Enum {:?}", - val - ))), - } -} - -pub fn serialize_reliability(reliability: &Option, s: S) -> Result -where - S: Serializer, -{ - match reliability { - Some(rel) => s.serialize_u8(*rel as u8), - None => s.serialize_none(), - } -} - -pub fn deserialize_locality<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Option::::deserialize(d) { - Ok(Some(value)) => Ok(Some(match value { - 0u8 => Locality::SessionLocal, - 1u8 => Locality::Remote, - 2u8 => Locality::Any, - val => { - return Err(serde::de::Error::custom(format!( - "Value not valid for Locality Enum {:?}", - val - ))) - } - })), - Ok(None) => Ok(None), - val => Err(serde::de::Error::custom(format!( - "Value not valid for Locality Enum {:?}", - val - ))), - } -} - -pub fn serialize_locality(locality: &Option, s: S) -> Result -where - S: Serializer, -{ - match locality { - Some(rel) => s.serialize_u8(*rel as u8), - None => s.serialize_none(), - } -} - -pub fn deserialize_query_target<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Option::::deserialize(d) { - Ok(Some(value)) => Ok(Some(match value { - 0u8 => QueryTarget::BestMatching, - 1u8 => QueryTarget::All, - 2u8 => QueryTarget::AllComplete, - val => { - return Err(serde::de::Error::custom(format!( - "Value not valid for QueryTarget Enum {:?}", - val - ))) - } - })), - Ok(None) => Ok(None), - val => Err(serde::de::Error::custom(format!( - "Value not valid for QueryTarget Enum {:?}", - val - ))), - } -} - -pub fn serialize_query_target( - query_target: &Option, - s: S, -) -> Result -where - S: Serializer, -{ - match query_target { - Some(rel) => s.serialize_u8(*rel as u8), - None => s.serialize_none(), - } -} diff --git a/zenoh-plugin-remote-api/src/lib.rs b/zenoh-plugin-remote-api/src/lib.rs index e1539563..8935aecb 100644 --- a/zenoh-plugin-remote-api/src/lib.rs +++ b/zenoh-plugin-remote-api/src/lib.rs @@ -25,12 +25,12 @@ use std::{ io::{self, BufReader, ErrorKind}, net::SocketAddr, path::Path, - sync::Arc, + sync::{Arc, Mutex}, }; -use flume::Sender; use futures::{future, pin_mut, StreamExt, TryStreamExt}; -use interface::RemoteAPIMsg; +use interface::{InRemoteMessage, OpenAck, OutRemoteMessage, SequenceId}; +use remote_state::RemoteState; use rustls_pemfile::{certs, private_key}; use serde::Serialize; use tokio::{ @@ -48,8 +48,7 @@ use tokio_rustls::{ TlsAcceptor, }; use tokio_tungstenite::tungstenite::protocol::Message; -use tracing::{debug, error}; -use uhlc::Timestamp; +use tracing; use uuid::Uuid; use zenoh::{ bytes::{Encoding, ZBytes}, @@ -61,10 +60,7 @@ use zenoh::{ format::{kedefine, keformat}, keyexpr, OwnedKeyExpr, }, - liveliness::LivelinessToken, - pubsub::Publisher, - query::{Querier, Query}, - Session, + query::Query, }; use zenoh_plugin_trait::{plugin_long_version, plugin_version, Plugin, PluginControl}; use zenoh_result::{bail, zerror, ZResult}; @@ -72,12 +68,9 @@ use zenoh_result::{bail, zerror, ZResult}; mod config; pub use config::Config; -mod handle_control_message; -mod handle_data_message; mod interface; -use crate::{ - handle_control_message::handle_control_message, handle_data_message::handle_data_message, -}; + +mod remote_state; kedefine!( // Admin space key expressions of plugin's version @@ -213,8 +206,7 @@ pub async fn run( config: Config, opt_certs: Option<(Vec>, PrivateKeyDer<'static>)>, ) { - let hm: HashMap = HashMap::new(); - let state_map = Arc::new(RwLock::new(hm)); + let state_map = Arc::new(RwLock::new(HashMap::new())); // Return WebServer And State let remote_api_runtime = RemoteAPIRuntime { @@ -255,46 +247,61 @@ impl RemoteAPIRuntime { } } -#[derive(Debug, Serialize)] -struct AdminSpaceClient { +#[derive(Debug, Serialize, Clone)] +pub(crate) struct AdminSpaceClient { uuid: String, remote_address: SocketAddr, - publishers: Vec, - subscribers: Vec, - queryables: Vec, + publishers: HashMap, + subscribers: HashMap, + queryables: HashMap, + queriers: HashMap, + liveliness_tokens: HashMap, } -impl From<(&SocketAddr, &RemoteState)> for AdminSpaceClient { - fn from(value: (&SocketAddr, &RemoteState)) -> Self { - let remote_state = value.1; - let sock_addr = value.0; - - let pub_keyexprs = remote_state - .publishers - .values() - .map(|x| x.key_expr().to_string()) - .collect::>(); - - let query_keyexprs = remote_state - .queryables - .values() - .map(|(_, key_expr)| key_expr.to_string()) - .collect::>(); - - let sub_keyexprs = remote_state - .subscribers - .values() - .map(|(_, key_expr)| key_expr.to_string()) - .collect::>(); - +impl AdminSpaceClient { + pub(crate) fn new(uuid: String, remote_address: SocketAddr) -> Self { AdminSpaceClient { - uuid: remote_state.session_id.to_string(), - remote_address: *sock_addr, - publishers: pub_keyexprs, - subscribers: sub_keyexprs, - queryables: query_keyexprs, + uuid, + remote_address, + publishers: HashMap::new(), + subscribers: HashMap::new(), + queryables: HashMap::new(), + queriers: HashMap::new(), + liveliness_tokens: HashMap::new(), } } + + pub(crate) fn register_publisher(&mut self, id: u32, key_expr: &str) { + self.publishers.insert(id, key_expr.to_string()); + } + + pub(crate) fn register_subscriber(&mut self, id: u32, key_expr: &str) { + self.publishers.insert(id, key_expr.to_string()); + } + + pub(crate) fn register_queryable(&mut self, id: u32, key_expr: &str) { + self.queryables.insert(id, key_expr.to_string()); + } + + pub(crate) fn register_querier(&mut self, id: u32, key_expr: &str) { + self.queriers.insert(id, key_expr.to_string()); + } + + pub(crate) fn unregister_publisher(&mut self, id: u32) { + self.publishers.remove(&id); + } + + pub(crate) fn unregister_subscriber(&mut self, id: u32) { + self.subscribers.remove(&id); + } + + pub(crate) fn unregister_queryable(&mut self, id: u32) { + self.queryables.remove(&id); + } + + pub(crate) fn unregister_querier(&mut self, id: u32) { + self.queriers.remove(&id); + } } async fn run_admin_space_queryable(zenoh_runtime: Runtime, state_map: StateMap, config: Config) { @@ -338,10 +345,11 @@ async fn run_admin_space_queryable(zenoh_runtime: Runtime, state_map: StateMap, if query_ke.is_wild() { if query_ke.contains("clients") { let read_guard = state_map.read().await; - let mut admin_space_clients = Vec::new(); - for (sock, remote_state) in read_guard.iter() { - admin_space_clients.push(AdminSpaceClient::from((sock, remote_state))); - } + let admin_space_clients = read_guard + .values() + .map(|v| v.lock().unwrap().clone()) + .collect::>(); + drop(read_guard); send_reply(admin_space_clients, query, query_ke).await; } else { for (ke, admin_ref) in admin_space.iter() { @@ -368,17 +376,10 @@ async fn run_admin_space_queryable(zenoh_runtime: Runtime, state_map: StateMap, } if let Some(id) = opt_id { let read_guard = state_map.read().await; - for (sock, remote_state) in read_guard.iter() { - if remote_state.session_id.to_string() == id { - send_reply( - AdminSpaceClient::from((sock, remote_state)), - query, - own_ke, - ) - .await; - - break; - } + if let Some(state) = read_guard.get(id) { + let state = state.lock().unwrap().clone(); + drop(read_guard); + send_reply(state, query, own_ke).await; } } } @@ -402,11 +403,11 @@ where .encoding(Encoding::APPLICATION_JSON) .await { - error!("AdminSpace: Reply to Query failed, {}", err); + tracing::error!("AdminSpace: Reply to Query failed, {}", err); }; } Err(_) => { - error!("AdminSpace: Could not seralize client data"); + tracing::error!("AdminSpace: Could not seralize client data"); } }; } @@ -472,75 +473,7 @@ impl RunningPluginTrait for RunningPlugin { } } -type StateMap = Arc>>; - -struct RemoteState { - websocket_tx: Sender, - session_id: Uuid, - session: Session, - // Timestamp - timestamps: HashMap, - // PubSub - subscribers: HashMap, OwnedKeyExpr)>, - publishers: HashMap>, - // Queryable - queryables: HashMap, OwnedKeyExpr)>, - unanswered_queries: Arc>>, - // Liveliness - liveliness_tokens: HashMap, - liveliness_subscribers: HashMap, OwnedKeyExpr)>, - // Querier - queriers: HashMap>, -} - -impl RemoteState { - fn new(websocket_tx: Sender, session_id: Uuid, session: Session) -> Self { - Self { - websocket_tx, - session_id, - session, - timestamps: HashMap::new(), - subscribers: HashMap::new(), - publishers: HashMap::new(), - queryables: HashMap::new(), - unanswered_queries: Arc::new(std::sync::RwLock::new(HashMap::new())), - liveliness_tokens: HashMap::new(), - liveliness_subscribers: HashMap::new(), - queriers: HashMap::new(), - } - } - - async fn cleanup(self) { - for (_, publisher) in self.publishers { - if let Err(e) = publisher.undeclare().await { - error!("{e}") - } - } - for (_, (subscriber, _)) in self.subscribers { - subscriber.abort(); - } - - for (_, (queryable, _)) in self.queryables { - queryable.abort(); - } - - drop(self.unanswered_queries); - - for (_, queryable) in self.liveliness_tokens { - if let Err(e) = queryable.undeclare().await { - error!("{e}") - } - } - - for (_, (liveliness_subscriber, _)) in self.liveliness_subscribers { - liveliness_subscriber.abort(); - } - - if let Err(err) = self.session.close().await { - error!("{err}") - }; - } -} +type StateMap = Arc>>>>; pub trait Streamable: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Send + Unpin @@ -576,15 +509,12 @@ async fn run_websocket_server( }; while let Ok((tcp_stream, sock_addr)) = server.accept().await { - let state_map = state_map.clone(); let zenoh_runtime = zenoh_runtime.clone(); let opt_tls_acceptor = opt_tls_acceptor.clone(); - + let state_map2 = state_map.clone(); let new_websocket = async move { let sock_adress = Arc::new(sock_addr); - let (ws_ch_tx, ws_ch_rx) = flume::unbounded::(); - - let mut write_guard = state_map.write().await; + let (ws_ch_tx, ws_ch_rx) = flume::unbounded::<(OutRemoteMessage, Option)>(); let session = match zenoh::session::init(zenoh_runtime.clone()).await { Ok(session) => session, @@ -596,17 +526,11 @@ async fn run_websocket_server( let id = Uuid::new_v4(); tracing::debug!("Client {sock_addr:?} -> {id}"); - let state: RemoteState = RemoteState::new(ws_ch_tx.clone(), id, session); - - // if remote state exists in map already. Ignore it and reinitialize - let _ = write_guard.insert(sock_addr, state); - drop(write_guard); - let streamable: Box = match &opt_tls_acceptor { Some(acceptor) => match acceptor.accept(tcp_stream).await { Ok(tls_stream) => Box::new(tls_stream), Err(err) => { - error!("Could not secure TcpStream -> TlsStream {:?}", err); + tracing::error!("Could not secure TcpStream -> TlsStream {:?}", err); return; } }, @@ -616,7 +540,7 @@ async fn run_websocket_server( let ws_stream = match tokio_tungstenite::accept_async(streamable).await { Ok(ws_stream) => ws_stream, Err(e) => { - error!("Error during the websocket handshake occurred: {}", e); + tracing::error!("Error during the websocket handshake occurred: {}", e); return; } }; @@ -625,39 +549,39 @@ async fn run_websocket_server( let ch_rx_stream = ws_ch_rx .into_stream() - .map(|remote_api_msg| { - let val = serde_json::to_string(&remote_api_msg).unwrap(); // This unwrap should be alright - Ok(Message::Text(val)) - }) + .map(|(out_msg, sequence_id)| Ok(Message::Binary(out_msg.to_wire(sequence_id)))) .forward(ws_tx); - let sock_adress_cl = sock_adress.clone(); + // send confirmation that session was successfully opened + let admin_client = + Arc::new(Mutex::new(AdminSpaceClient::new(id.to_string(), sock_addr))); + state_map2 + .write() + .await + .insert(id.to_string(), admin_client.clone()); - let state_map_cl_outer = state_map.clone(); + let mut remote_state = RemoteState::new(ws_ch_tx.clone(), admin_client, session); + let _ = ws_ch_tx.send((OutRemoteMessage::OpenAck(OpenAck { uuid: id }), None)); // Incoming message from Websocket let incoming_ws = tokio::task::spawn(async move { let mut non_close_messages = ws_rx.try_filter(|msg| future::ready(!msg.is_close())); - let state_map_cl = state_map_cl_outer.clone(); - let sock_adress_ref = sock_adress_cl.clone(); + while let Ok(Some(msg)) = non_close_messages.try_next().await { - if let Some(response) = - handle_message(msg, *sock_adress_ref, state_map_cl.clone()).await - { + if let Some(response) = handle_message(msg, &mut remote_state).await { if let Err(err) = ws_ch_tx.send(response) { - error!("WS Send Error: {err:?}"); + tracing::error!("WS Send Error: {err:?}"); }; }; } + remote_state.clear().await; }); pin_mut!(ch_rx_stream, incoming_ws); future::select(ch_rx_stream, incoming_ws).await; // cleanup state - if let Some(state) = state_map.write().await.remove(sock_adress.as_ref()) { - state.cleanup().await; - }; + state_map2.write().await.remove(&id.to_string()); tracing::info!("Client Disconnected {}", sock_adress.as_ref()); }; @@ -668,36 +592,67 @@ async fn run_websocket_server( async fn handle_message( msg: Message, - sock_addr: SocketAddr, - state_map: StateMap, -) -> Option { + state: &mut RemoteState, +) -> Option<(OutRemoteMessage, Option)> { match msg { - Message::Text(text) => match serde_json::from_str::(&text) { - Ok(msg) => match msg { - RemoteAPIMsg::Control(ctrl_msg) => { - match handle_control_message(ctrl_msg, sock_addr, state_map).await { - Ok(_) => return None, - Err(err) => { - tracing::error!(err); - } + Message::Binary(val) => match InRemoteMessage::from_wire(val) { + Ok((header, msg)) => { + match state.handle_message(msg).await { + Ok(Some(msg)) => Some((msg, header.sequence_id)), + Ok(None) => header.sequence_id.map(|_| { + ( + OutRemoteMessage::Ok(interface::Ok { + content_id: header.content_id, + }), + header.sequence_id, + ) + }), + Err(error) => { + tracing::error!( + "RemoteAPI: Failed to execute request {:?}: {}", + header.content_id, + error + ); + header.sequence_id.map(|_| { + // send error response if ack was requested + ( + OutRemoteMessage::Error(interface::Error { + content_id: header.content_id, + error: error.to_string(), + }), + header.sequence_id, + ) + }) } } - RemoteAPIMsg::Data(data_msg) => { - if let Err(err) = handle_data_message(data_msg, sock_addr, state_map).await { - tracing::error!(err); - } + } + Err(err) => match err { + interface::FromWireError::HeaderError(error) => { + tracing::error!("RemoteAPI: Failed to parse message header: {}", error); + None + } + interface::FromWireError::BodyError((header, error)) => { + tracing::error!( + "RemoteAPI: Failed to parse message body for {:?}: {}", + header, + error + ); + header.sequence_id.map(|_| { + // send error response if ack was requested + ( + OutRemoteMessage::Error(interface::Error { + content_id: header.content_id, + error: error.to_string(), + }), + header.sequence_id, + ) + }) } }, - Err(err) => { - tracing::error!( - "RemoteAPI: WS Message Cannot be Deserialized to RemoteAPIMsg {}, message: {}", - err, - text - ); - } }, _ => { - debug!("RemoteAPI: WS Message Not Text"); + tracing::error!("RemoteAPI: message format is not `Binary`"); + None } }; None diff --git a/zenoh-plugin-remote-api/src/remote_state.rs b/zenoh-plugin-remote-api/src/remote_state.rs new file mode 100644 index 00000000..207a1f32 --- /dev/null +++ b/zenoh-plugin-remote-api/src/remote_state.rs @@ -0,0 +1,792 @@ +// +// Copyright (c) 2024 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +//! ⚠️ WARNING ⚠️ +//! +//! This crate is intended for Zenoh's internal use. +//! +//! [Click here for Zenoh's documentation](../zenoh/index.html) + +use std::{ + collections::HashMap, + sync::{atomic::AtomicU32, Arc, Mutex}, + time::Duration, +}; + +use flume::Sender; +use lru::LruCache; +use zenoh::{ + handlers::CallbackDrop, + liveliness::LivelinessToken, + pubsub::{Publisher, Subscriber}, + query::{Querier, Query, Queryable, Reply, Selector}, + Session, +}; +use zenoh_result::bail; + +use crate::{ + interface::{ + self, DeclareLivelinessSubscriber, DeclareLivelinessToken, DeclarePublisher, + DeclareQuerier, DeclareQueryable, DeclareSubscriber, Delete, Get, LivelinessGet, + PublisherDelete, PublisherPut, Put, QuerierGet, QueryResponseFinal, ReplyDel, ReplyErr, + ReplyOk, ResponseSessionInfo, ResponseTimestamp, UndeclareLivelinessSubscriber, + UndeclareLivelinessToken, UndeclarePublisher, UndeclareQuerier, UndeclareQueryable, + UndeclareSubscriber, + }, + AdminSpaceClient, InRemoteMessage, OutRemoteMessage, SequenceId, +}; + +// Since we do not have api to get query timeout +// we do not have possibility to understand when a stale query should be dropped +// so to make things simple we are going to use lru cache which would handle stale queries +// autmatically. + +const MAX_NUM_PENDING_QUERIES: usize = 1000; +pub(crate) struct RemoteState { + tx: Sender<(OutRemoteMessage, Option)>, + admin_client: Arc>, + session: Session, + subscribers: HashMap>, + publishers: HashMap>, + queryables: HashMap>, + pending_queries: Arc>>, + query_counter: Arc, + liveliness_tokens: HashMap, + liveliness_subscribers: HashMap>, + queriers: HashMap>, +} + +impl RemoteState { + pub(crate) fn new( + tx: Sender<(OutRemoteMessage, Option)>, + admin_client: Arc>, + session: Session, + ) -> Self { + Self { + tx, + admin_client, + session, + subscribers: HashMap::new(), + publishers: HashMap::new(), + queryables: HashMap::new(), + pending_queries: Arc::new(Mutex::new(LruCache::new( + MAX_NUM_PENDING_QUERIES.try_into().unwrap(), + ))), + query_counter: Arc::new(AtomicU32::new(0)), + liveliness_tokens: HashMap::new(), + liveliness_subscribers: HashMap::new(), + queriers: HashMap::new(), + } + } + + pub(crate) async fn clear(&mut self) { + let mut publishers = HashMap::new(); + std::mem::swap(&mut publishers, &mut self.publishers); + for (_, publisher) in publishers { + if let Err(e) = publisher.undeclare().await { + tracing::error!("{e}") + } + } + + let mut subscribers = HashMap::new(); + std::mem::swap(&mut subscribers, &mut self.subscribers); + + for (_, subscriber) in subscribers { + if let Err(e) = subscriber.undeclare().await { + tracing::error!("{e}") + } + } + + let mut queryables = HashMap::new(); + std::mem::swap(&mut queryables, &mut self.queryables); + for (_, queryable) in queryables { + if let Err(e) = queryable.undeclare().await { + tracing::error!("{e}") + } + } + + self.pending_queries.lock().as_mut().unwrap().clear(); + + let mut liveliness_tokens = HashMap::new(); + std::mem::swap(&mut liveliness_tokens, &mut self.liveliness_tokens); + for (_, token) in liveliness_tokens { + if let Err(e) = token.undeclare().await { + tracing::error!("{e}") + } + } + + let mut liveliness_subscribers = HashMap::new(); + std::mem::swap( + &mut liveliness_subscribers, + &mut self.liveliness_subscribers, + ); + for (_, subscriber) in liveliness_subscribers { + if let Err(e) = subscriber.undeclare().await { + tracing::error!("{e}") + } + } + + if let Err(err) = self.session.close().await { + tracing::error!("{err}") + }; + } + + async fn declare_publisher( + &mut self, + declare_publisher: DeclarePublisher, + ) -> Result, zenoh_result::Error> { + if self.publishers.contains_key(&declare_publisher.id) { + bail!( + "Publisher with id: '{}' already exists", + declare_publisher.id + ); + } + let publisher = self + .session + .declare_publisher(declare_publisher.keyexpr) + .encoding(declare_publisher.encoding) + .priority(declare_publisher.priority) + .congestion_control(declare_publisher.congestion_control) + .express(declare_publisher.express) + .allowed_destination(declare_publisher.allowed_destination) + .reliability(declare_publisher.reliability) + .await?; + self.admin_client + .lock() + .unwrap() + .register_publisher(declare_publisher.id, publisher.key_expr().as_str()); + self.publishers.insert(declare_publisher.id, publisher); + Ok(None) + } + + async fn undeclare_publisher( + &mut self, + undeclare_publisher: UndeclarePublisher, + ) -> Result, zenoh_result::Error> { + match self.publishers.remove(&undeclare_publisher.id) { + Some(p) => { + p.undeclare().await?; + self.admin_client + .lock() + .unwrap() + .unregister_publisher(undeclare_publisher.id); + Ok(None) + } + None => bail!( + "Publisher with id {} does not exist", + undeclare_publisher.id + ), + } + } + + async fn declare_subscriber( + &mut self, + declare_subscriber: DeclareSubscriber, + ) -> Result, zenoh_result::Error> { + if self.subscribers.contains_key(&declare_subscriber.id) { + bail!( + "Subscriber with id: '{}' already exists", + declare_subscriber.id + ); + } + let tx = self.tx.clone(); + let subscriber = self + .session + .declare_subscriber(declare_subscriber.keyexpr) + .allowed_origin(declare_subscriber.allowed_origin) + .callback(move |s| { + let msg = interface::Sample { + subscriber_id: declare_subscriber.id, + sample: s.into(), + }; + let _ = tx.send((OutRemoteMessage::Sample(msg), None)); + }) + .await?; + self.admin_client + .lock() + .unwrap() + .register_subscriber(declare_subscriber.id, subscriber.key_expr().as_str()); + self.subscribers.insert(declare_subscriber.id, subscriber); + Ok(None) + } + + async fn undeclare_subscriber( + &mut self, + undeclare_subscriber: UndeclareSubscriber, + ) -> Result, zenoh_result::Error> { + match self.subscribers.remove(&undeclare_subscriber.id) { + Some(s) => { + s.undeclare().await?; + self.admin_client + .lock() + .unwrap() + .unregister_subscriber(undeclare_subscriber.id); + Ok(None) + } + None => bail!( + "Subscriber with id {} does not exist", + undeclare_subscriber.id + ), + } + } + + async fn declare_queryable( + &mut self, + declare_queryable: DeclareQueryable, + ) -> Result, zenoh_result::Error> { + if self.queryables.contains_key(&declare_queryable.id) { + bail!( + "Queryable with id: '{}' already exists", + declare_queryable.id + ); + } + let tx = self.tx.clone(); + let query_counter = self.query_counter.clone(); + let pending_queries = self.pending_queries.clone(); + + let queryable = self + .session + .declare_queryable(declare_queryable.keyexpr) + .complete(declare_queryable.complete) + .allowed_origin(declare_queryable.allowed_origin) + .callback(move |q| { + let query_id = query_counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + let msg = interface::Query { + queryable_id: declare_queryable.id, + query_id, + query: q.clone(), + }; + pending_queries.lock().unwrap().put(query_id, q); + let _ = tx.send((OutRemoteMessage::Query(msg), None)); + }) + .await?; + self.admin_client + .lock() + .unwrap() + .register_queryable(declare_queryable.id, queryable.key_expr().as_str()); + self.queryables.insert(declare_queryable.id, queryable); + Ok(None) + } + + async fn undeclare_queryable( + &mut self, + undeclare_queryable: UndeclareQueryable, + ) -> Result, zenoh_result::Error> { + match self.queryables.remove(&undeclare_queryable.id) { + Some(q) => { + q.undeclare().await?; + self.admin_client + .lock() + .unwrap() + .unregister_queryable(undeclare_queryable.id); + Ok(None) + } + None => bail!( + "Queryable with id {} does not exist", + undeclare_queryable.id + ), + } + } + + async fn declare_querier( + &mut self, + declare_querier: DeclareQuerier, + ) -> Result, zenoh_result::Error> { + if self.queriers.contains_key(&declare_querier.id) { + bail!("Querier with id: '{}' already exists", declare_querier.id); + } + let querier = self + .session + .declare_querier(declare_querier.keyexpr) + .priority(declare_querier.priority) + .congestion_control(declare_querier.congestion_control) + .express(declare_querier.express) + .allowed_destination(declare_querier.allowed_destination) + .accept_replies(declare_querier.accept_replies) + .target(declare_querier.target) + .timeout(Duration::from_millis(declare_querier.timeout_ms)) + .consolidation(declare_querier.consolidation) + .await?; + self.admin_client + .lock() + .unwrap() + .register_querier(declare_querier.id, querier.key_expr().as_str()); + self.queriers.insert(declare_querier.id, querier); + Ok(None) + } + + async fn undeclare_querier( + &mut self, + undeclare_querier: UndeclareQuerier, + ) -> Result, zenoh_result::Error> { + match self.queriers.remove(&undeclare_querier.id) { + Some(q) => { + q.undeclare().await?; + self.admin_client + .lock() + .unwrap() + .unregister_querier(undeclare_querier.id); + Ok(None) + } + None => bail!("Querier with id {} does not exist", undeclare_querier.id), + } + } + + async fn get_session_info(&self) -> OutRemoteMessage { + let info = self.session.info(); + OutRemoteMessage::ResponseSessionInfo(ResponseSessionInfo { + zid: info.zid().await, + z_routers: info.routers_zid().await.collect(), + z_peers: info.peers_zid().await.collect(), + }) + } + + fn get_timestamp(&self) -> OutRemoteMessage { + OutRemoteMessage::ResponseTimestamp(ResponseTimestamp { + timestamp: self.session.new_timestamp(), + }) + } + + async fn put(&self, put: Put) -> Result<(), zenoh_result::Error> { + self.session + .put(put.keyexpr, put.payload) + .encoding(put.encoding) + .attachment(put.attachment) + .priority(put.priority) + .congestion_control(put.congestion_control) + .express(put.express) + .allowed_destination(put.allowed_destination) + .reliability(put.reliability) + .timestamp(put.timestamp) + .await?; + Ok(()) + } + + async fn delete(&self, delete: Delete) -> Result<(), zenoh_result::Error> { + self.session + .delete(delete.keyexpr) + .attachment(delete.attachment) + .priority(delete.priority) + .congestion_control(delete.congestion_control) + .express(delete.express) + .allowed_destination(delete.allowed_destination) + .reliability(delete.reliability) + .timestamp(delete.timestamp) + .await?; + Ok(()) + } + + async fn publisher_put(&self, publisher_put: PublisherPut) -> Result<(), zenoh_result::Error> { + match self.publishers.get(&publisher_put.publisher_id) { + Some(p) => { + let mut pb = p + .put(publisher_put.payload) + .attachment(publisher_put.attachment) + .timestamp(publisher_put.timestamp); + if let Some(encoding) = publisher_put.encoding { + pb = pb.encoding(encoding); + } + pb.await?; + Ok(()) + } + None => { + bail!( + "Publisher with id {} does not exist", + publisher_put.publisher_id + ); + } + } + } + + async fn publisher_delete( + &self, + publisher_delete: PublisherDelete, + ) -> Result<(), zenoh_result::Error> { + match self.publishers.get(&publisher_delete.publisher_id) { + Some(p) => { + p.delete() + .attachment(publisher_delete.attachment) + .timestamp(publisher_delete.timestamp) + .await?; + Ok(()) + } + None => { + bail!( + "Publisher with id {} does not exist", + publisher_delete.publisher_id + ); + } + } + } + fn create_get_callback(&self, query_id: u32) -> CallbackDrop { + let tx1 = self.tx.clone(); + let tx2 = self.tx.clone(); + CallbackDrop { + callback: move |r: zenoh::query::Reply| { + let msg = interface::Reply { + query_id, + reply: r.into(), + }; + let _ = tx1.send((OutRemoteMessage::Reply(msg), None)); + }, + drop: move || { + let msg = interface::QueryResponseFinal { query_id }; + let _ = tx2.send((OutRemoteMessage::QueryResponseFinal(msg), None)); + }, + } + } + + async fn get(&self, get: Get) -> Result<(), zenoh_result::Error> { + let selector: Selector = match get.parameters { + Some(p) => (get.keyexpr, p).into(), + None => get.keyexpr.into(), + }; + let mut gb = self.session.get(selector); + if let Some(payload) = get.payload { + gb = gb.payload(payload); + } + if let Some(attachment) = get.attachment { + gb = gb.attachment(attachment); + } + if let Some(encoding) = get.encoding { + gb = gb.encoding(encoding); + } + + gb.accept_replies(get.accept_replies) + .priority(get.priority) + .congestion_control(get.congestion_control) + .express(get.express) + .allowed_destination(get.allowed_destination) + .consolidation(get.consolidation) + .target(get.target) + .timeout(Duration::from_millis(get.timeout_ms)) + .with(self.create_get_callback(get.id)) + .await?; + Ok(()) + } + + async fn querier_get(&self, querier_get: QuerierGet) -> Result<(), zenoh_result::Error> { + match self.queriers.get(&querier_get.querier_id) { + Some(querier) => { + let mut gb = querier.get(); + if let Some(payload) = querier_get.payload { + gb = gb.payload(payload); + } + if let Some(attachment) = querier_get.attachment { + gb = gb.attachment(attachment); + } + if let Some(encoding) = querier_get.encoding { + gb = gb.encoding(encoding); + } + if let Some(params) = querier_get.parameters { + gb = gb.parameters(params); + } + + gb.with(self.create_get_callback(querier_get.id)).await?; + Ok(()) + } + None => bail!("Querier with id {} does not exist", querier_get.id), + } + } + + async fn reply_ok(&self, reply_ok: ReplyOk) -> Result<(), zenoh_result::Error> { + let q = self + .pending_queries + .lock() + .unwrap() + .get(&reply_ok.query_id) + .cloned(); + + match q { + Some(q) => { + q.reply(reply_ok.keyexpr, reply_ok.payload) + .attachment(reply_ok.attachment) + .encoding(reply_ok.encoding) + .priority(reply_ok.priority) + .congestion_control(reply_ok.congestion_control) + .express(reply_ok.express) + .timestamp(reply_ok.timestamp) + .await?; + } + None => { + bail!("Query with id {} does not exist", reply_ok.query_id); + } + } + + Ok(()) + } + + async fn reply_del(&self, reply_del: ReplyDel) -> Result<(), zenoh_result::Error> { + let q = self + .pending_queries + .lock() + .unwrap() + .get(&reply_del.query_id) + .cloned(); + + match q { + Some(q) => { + q.reply_del(reply_del.keyexpr) + .attachment(reply_del.attachment) + .priority(reply_del.priority) + .congestion_control(reply_del.congestion_control) + .express(reply_del.express) + .timestamp(reply_del.timestamp) + .await?; + } + None => { + bail!("Query with id {} does not exist", reply_del.query_id); + } + } + + Ok(()) + } + + async fn reply_err(&self, reply_err: ReplyErr) -> Result<(), zenoh_result::Error> { + let q = self + .pending_queries + .lock() + .unwrap() + .get(&reply_err.query_id) + .cloned(); + + match q { + Some(q) => { + q.reply_err(reply_err.payload) + .encoding(reply_err.encoding) + .await?; + } + None => { + bail!("Query with id {} does not exist", reply_err.query_id); + } + } + + Ok(()) + } + + async fn declare_liveliness_token( + &mut self, + declare_liveliness_token: DeclareLivelinessToken, + ) -> Result, zenoh_result::Error> { + if self + .liveliness_tokens + .contains_key(&declare_liveliness_token.id) + { + bail!( + "Liveliness token with id: '{}' already exists", + declare_liveliness_token.id + ); + } + let token = self + .session + .liveliness() + .declare_token(declare_liveliness_token.keyexpr) + .await?; + self.liveliness_tokens + .insert(declare_liveliness_token.id, token); + Ok(None) + } + + async fn undeclare_liveliness_token( + &mut self, + undeclare_liveliness_token: UndeclareLivelinessToken, + ) -> Result, zenoh_result::Error> { + match self + .liveliness_tokens + .remove(&undeclare_liveliness_token.id) + { + Some(t) => { + t.undeclare().await?; + Ok(None) + } + None => bail!( + "Liveliness token with id {} does not exist", + undeclare_liveliness_token.id + ), + } + } + + async fn declare_liveliness_subscriber( + &mut self, + declare_liveliness_subscriber: DeclareLivelinessSubscriber, + ) -> Result, zenoh_result::Error> { + if self + .liveliness_subscribers + .contains_key(&declare_liveliness_subscriber.id) + { + bail!( + "Liveliness subscriber with id: '{}' already exists", + declare_liveliness_subscriber.id + ); + } + let tx = self.tx.clone(); + let subscriber = self + .session + .liveliness() + .declare_subscriber(declare_liveliness_subscriber.keyexpr) + .history(declare_liveliness_subscriber.history) + .callback(move |s| { + let msg = interface::Sample { + subscriber_id: declare_liveliness_subscriber.id, + sample: s.into(), + }; + let _ = tx.send((OutRemoteMessage::Sample(msg), None)); + }) + .await?; + self.liveliness_subscribers + .insert(declare_liveliness_subscriber.id, subscriber); + Ok(None) + } + + async fn undeclare_liveliness_subscriber( + &mut self, + undeclare_liveliness_subscriber: UndeclareLivelinessSubscriber, + ) -> Result, zenoh_result::Error> { + match self + .liveliness_subscribers + .remove(&undeclare_liveliness_subscriber.id) + { + Some(t) => { + t.undeclare().await?; + Ok(None) + } + None => bail!( + "Liveliness subscriber with id {} does not exist", + undeclare_liveliness_subscriber.id + ), + } + } + + async fn liveliness_get( + &self, + liveliness_get: LivelinessGet, + ) -> Result<(), zenoh_result::Error> { + self.session + .liveliness() + .get(liveliness_get.keyexpr) + .timeout(Duration::from_millis(liveliness_get.timeout_ms)) + .with(self.create_get_callback(liveliness_get.id)) + .await?; + Ok(()) + } + + fn response_final( + &mut self, + response_final: QueryResponseFinal, + ) -> Result<(), zenoh_result::Error> { + match self + .pending_queries + .lock() + .unwrap() + .pop(&response_final.query_id) + { + Some(_) => Ok(()), + None => bail!("Query with id {} does not exist", response_final.query_id), + } + } + + pub(crate) async fn handle_message( + &mut self, + msg: InRemoteMessage, + ) -> Result, zenoh_result::Error> { + match msg { + InRemoteMessage::DeclarePublisher(declare_publisher) => { + self.declare_publisher(declare_publisher).await + } + InRemoteMessage::UndeclarePublisher(undeclare_publisher) => { + self.undeclare_publisher(undeclare_publisher).await + } + InRemoteMessage::DeclareSubscriber(declare_subscriber) => { + self.declare_subscriber(declare_subscriber).await + } + InRemoteMessage::UndeclareSubscriber(undeclare_subscriber) => { + self.undeclare_subscriber(undeclare_subscriber).await + } + InRemoteMessage::DeclareQueryable(declare_queryable) => { + self.declare_queryable(declare_queryable).await + } + InRemoteMessage::UndeclareQueryable(undeclare_queryable) => { + self.undeclare_queryable(undeclare_queryable).await + } + InRemoteMessage::DeclareQuerier(declare_querier) => { + self.declare_querier(declare_querier).await + } + InRemoteMessage::UndeclareQuerier(undeclare_querier) => { + self.undeclare_querier(undeclare_querier).await + } + InRemoteMessage::GetSessionInfo(_) => Ok(Some(self.get_session_info().await)), + InRemoteMessage::GetTimestamp(_) => Ok(Some(self.get_timestamp())), + InRemoteMessage::Put(put) => { + self.put(put).await?; + Ok(None) + } + InRemoteMessage::Delete(delete) => { + self.delete(delete).await?; + Ok(None) + } + InRemoteMessage::PublisherPut(publisher_put) => { + self.publisher_put(publisher_put).await?; + Ok(None) + } + InRemoteMessage::PublisherDelete(publisher_delete) => { + self.publisher_delete(publisher_delete).await?; + Ok(None) + } + InRemoteMessage::Get(get) => { + self.get(get).await?; + Ok(None) + } + InRemoteMessage::QuerierGet(querier_get) => { + self.querier_get(querier_get).await?; + Ok(None) + } + InRemoteMessage::ReplyOk(reply_ok) => { + self.reply_ok(reply_ok).await?; + Ok(None) + } + InRemoteMessage::ReplyDel(reply_del) => { + self.reply_del(reply_del).await?; + Ok(None) + } + InRemoteMessage::ReplyErr(reply_err) => { + self.reply_err(reply_err).await?; + Ok(None) + } + InRemoteMessage::DeclareLivelinessToken(declare_liveliness_token) => { + self.declare_liveliness_token(declare_liveliness_token) + .await + } + InRemoteMessage::UndeclareLivelinessToken(undeclare_liveliness_token) => { + self.undeclare_liveliness_token(undeclare_liveliness_token) + .await + } + InRemoteMessage::DeclareLivelinessSubscriber(declare_liveliness_subscriber) => { + self.declare_liveliness_subscriber(declare_liveliness_subscriber) + .await + } + InRemoteMessage::LivelinessGet(liveliness_get) => { + self.liveliness_get(liveliness_get).await?; + Ok(None) + } + InRemoteMessage::QueryResponseFinal(response_final) => { + self.response_final(response_final)?; + Ok(None) + } + InRemoteMessage::UndeclareLivelinessSubscriber(undeclare_liveliness_subscriber) => { + self.undeclare_liveliness_subscriber(undeclare_liveliness_subscriber) + .await + } + } + } +} From 502a93deecccccb1b25c6e6322d25f16eeb3adc1 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 22 May 2025 11:02:42 +0200 Subject: [PATCH 03/23] pack qos and query settings --- zenoh-plugin-remote-api/src/interface/mod.rs | 297 +++++++++++-------- zenoh-plugin-remote-api/src/remote_state.rs | 70 ++--- 2 files changed, 209 insertions(+), 158 deletions(-) diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index d59661c6..c21e74a6 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -74,41 +74,153 @@ impl Ok { } } -fn congestion_control_from_u8(c: u8) -> Result { - match c { - 0 => Ok(CongestionControl::Drop), - 1 => Ok(CongestionControl::Block), - v => bail!("Unsupported congestion control value {}", v), +pub(crate) struct Qos { + inner: u8, +} + +impl Qos { + pub(crate) fn new( + priority: Priority, + congestion_control: CongestionControl, + express: bool, + reliability: Reliability, + locality: Locality, + ) -> Self { + let p = priority as u8; + let c = match congestion_control { + CongestionControl::Drop => 0u8, + CongestionControl::Block => 1u8, + }; + let e = match express { + true => 1u8, + false => 0u8, + }; + let r = match reliability { + Reliability::BestEffort => 0u8, + Reliability::Reliable => 1u8, + }; + let l = match locality { + Locality::SessionLocal => 0u8, + Locality::Remote => 1u8, + Locality::Any => 2u8, + }; + // llrecppp + Self { + inner: p | (c << 3) | (e << 4) | (r << 5) | (l << 6), + } } -} -fn congestion_control_to_u8(c: CongestionControl) -> u8 { - match c { - CongestionControl::Drop => 0, - CongestionControl::Block => 1, + pub(crate) fn priority(&self) -> Priority { + let p = self.inner & 0b111u8; + p.try_into().unwrap_or_default() + } + + pub(crate) fn congestion_control(&self) -> CongestionControl { + let c = (self.inner >> 3) & 1u8; + match c == 0 { + true => CongestionControl::Drop, + false => CongestionControl::Block, + } + } + + pub(crate) fn express(&self) -> bool { + let e = (self.inner >> 4) & 1u8; + e > 0 + } + + pub(crate) fn reliability(&self) -> Reliability { + let r = (self.inner >> 5) & 1u8; + match r == 0 { + true => Reliability::BestEffort, + false => Reliability::Reliable, + } } -} -fn priority_from_u8(p: u8) -> Result { - p.try_into() + pub(crate) fn locality(&self) -> Locality { + let l = (self.inner >> 6) & 0b11u8; + match l { + 0u8 => Locality::SessionLocal, + 1u8 => Locality::Remote, + 2u8 => Locality::Any, + _ => Locality::default(), + } + } } -fn priority_to_u8(p: Priority) -> u8 { - p as u8 +impl Serialize for Qos { + fn serialize(&self, serializer: &mut ZSerializer) { + serializer.serialize(self.inner); + } } -fn reliability_from_u8(r: u8) -> Result { - match r { - 0 => Ok(Reliability::BestEffort), - 1 => Ok(Reliability::Reliable), - v => bail!("Unsupported reliability value {}", v), +impl Deserialize for Qos { + fn deserialize(deserializer: &mut ZDeserializer) -> Result { + Ok(Self { + inner: deserializer.deserialize()?, + }) } } -fn reliability_to_u8(r: Reliability) -> u8 { - match r { - Reliability::BestEffort => 0, - Reliability::Reliable => 1, +pub(crate) struct QuerySettings { + inner: u8, +} + +impl QuerySettings { + #[allow(dead_code)] + pub(crate) fn new( + target: QueryTarget, + consolidation: ConsolidationMode, + reply_keyexpr: ReplyKeyExpr, + ) -> Self { + let t = match target { + QueryTarget::BestMatching => 0u8, + QueryTarget::All => 1u8, + QueryTarget::AllComplete => 2u8, + }; + let c = match consolidation { + ConsolidationMode::Auto => 0u8, + ConsolidationMode::None => 1u8, + ConsolidationMode::Monotonic => 2u8, + ConsolidationMode::Latest => 3u8, + }; + let r = match reply_keyexpr { + ReplyKeyExpr::Any => 0u8, + ReplyKeyExpr::MatchingQuery => 1u8, + }; + + // rcctt + Self { + inner: t | (c << 2) | (r << 4), + } + } + + pub(crate) fn target(&self) -> QueryTarget { + let t = self.inner & 0b11u8; + match t { + 0 => QueryTarget::All, + 1 => QueryTarget::AllComplete, + 2 => QueryTarget::BestMatching, + _ => QueryTarget::default(), + } + } + + pub(crate) fn consolidation(&self) -> ConsolidationMode { + let c = (self.inner >> 2) & 0b11u8; + match c { + 0 => ConsolidationMode::Auto, + 1 => ConsolidationMode::None, + 2 => ConsolidationMode::Monotonic, + 3 => ConsolidationMode::Latest, + _ => ConsolidationMode::default(), + } + } + + pub(crate) fn reply_keyexpr(&self) -> ReplyKeyExpr { + let r = (self.inner >> 4) & 1u8; + match r == 0 { + true => ReplyKeyExpr::Any, + false => ReplyKeyExpr::MatchingQuery, + } } } @@ -121,37 +233,24 @@ fn locality_from_u8(l: u8) -> Result { } } -fn consolidation_from_u8(l: u8) -> Result { - match l { - 0 => Ok(ConsolidationMode::Auto), - 1 => Ok(ConsolidationMode::None), - 2 => Ok(ConsolidationMode::Monotonic), - 3 => Ok(ConsolidationMode::Latest), - v => bail!("Unsupported consolidation mode value {}", v), - } -} - -fn query_target_from_u8(t: u8) -> Result { - match t { - 0 => Ok(QueryTarget::All), - 1 => Ok(QueryTarget::AllComplete), - 2 => Ok(QueryTarget::BestMatching), - v => bail!("Unsupported query target value {}", v), +fn reply_keyexpr_to_u8(a: ReplyKeyExpr) -> u8 { + match a { + ReplyKeyExpr::Any => 0, + ReplyKeyExpr::MatchingQuery => 1, } } -fn reply_keyexpr_from_u8(a: u8) -> Result { - match a { - 0 => Ok(ReplyKeyExpr::Any), - 1 => Ok(ReplyKeyExpr::MatchingQuery), - v => bail!("Unsupported reply keyexpr value {}", v), +impl Serialize for QuerySettings { + fn serialize(&self, serializer: &mut ZSerializer) { + serializer.serialize(self.inner); } } -fn reply_keyexpr_to_u8(a: ReplyKeyExpr) -> u8 { - match a { - ReplyKeyExpr::Any => 0, - ReplyKeyExpr::MatchingQuery => 1, +impl Deserialize for QuerySettings { + fn deserialize(deserializer: &mut ZDeserializer) -> Result { + Ok(Self { + inner: deserializer.deserialize()?, + }) } } @@ -214,11 +313,7 @@ pub(crate) struct DeclarePublisher { pub(crate) id: u32, pub(crate) keyexpr: OwnedKeyExpr, pub(crate) encoding: Encoding, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) reliability: Reliability, - pub(crate) allowed_destination: Locality, + pub(crate) qos: Qos, } impl DeclarePublisher { @@ -227,11 +322,7 @@ impl DeclarePublisher { id: deserializer.deserialize()?, keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, encoding: encoding_from_id_schema(deserializer.deserialize()?), - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - reliability: reliability_from_u8(deserializer.deserialize()?)?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + qos: deserializer.deserialize()?, }) } } @@ -309,14 +400,9 @@ impl UndeclareQueryable { pub(crate) struct DeclareQuerier { pub(crate) id: u32, pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) target: QueryTarget, - pub(crate) accept_replies: ReplyKeyExpr, + pub(crate) qos: Qos, + pub(crate) query_settings: QuerySettings, pub(crate) timeout_ms: u64, - pub(crate) consolidation: ConsolidationMode, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) allowed_destination: Locality, } impl DeclareQuerier { @@ -324,14 +410,9 @@ impl DeclareQuerier { Ok(DeclareQuerier { id: deserializer.deserialize()?, keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - target: query_target_from_u8(deserializer.deserialize()?)?, - accept_replies: reply_keyexpr_from_u8(deserializer.deserialize()?)?, + qos: deserializer.deserialize()?, + query_settings: deserializer.deserialize()?, timeout_ms: deserializer.deserialize()?, - consolidation: consolidation_from_u8(deserializer.deserialize()?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, }) } } @@ -398,11 +479,7 @@ pub(crate) struct Put { pub(crate) encoding: Encoding, pub(crate) attachment: Option>, pub(crate) timestamp: Option, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) reliability: Reliability, - pub(crate) allowed_destination: Locality, + pub(crate) qos: Qos, } impl Put { @@ -413,11 +490,7 @@ impl Put { encoding: encoding_from_id_schema(deserializer.deserialize()?), attachment: deserialize_option(deserializer)?, timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - reliability: reliability_from_u8(deserializer.deserialize()?)?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + qos: deserializer.deserialize()?, }) } } @@ -426,11 +499,7 @@ pub(crate) struct Delete { pub(crate) keyexpr: OwnedKeyExpr, pub(crate) attachment: Option>, pub(crate) timestamp: Option, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) reliability: Reliability, - pub(crate) allowed_destination: Locality, + pub(crate) qos: Qos, } impl Delete { @@ -439,11 +508,7 @@ impl Delete { keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, attachment: deserialize_option(deserializer)?, timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - reliability: reliability_from_u8(deserializer.deserialize()?)?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, + qos: deserializer.deserialize()?, }) } } @@ -491,14 +556,9 @@ pub(crate) struct Get { pub(crate) payload: Option>, pub(crate) encoding: Option, pub(crate) attachment: Option>, - pub(crate) target: QueryTarget, - pub(crate) accept_replies: ReplyKeyExpr, + pub(crate) qos: Qos, + pub(crate) query_settings: QuerySettings, pub(crate) timeout_ms: u64, - pub(crate) consolidation: ConsolidationMode, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, - pub(crate) allowed_destination: Locality, } impl Get { @@ -510,14 +570,9 @@ impl Get { payload: deserialize_option(deserializer)?, encoding: opt_encoding_from_id_schema(deserialize_option(deserializer)?), attachment: deserialize_option(deserializer)?, - target: query_target_from_u8(deserializer.deserialize()?)?, - accept_replies: reply_keyexpr_from_u8(deserializer.deserialize()?)?, + qos: deserializer.deserialize()?, + query_settings: deserializer.deserialize()?, timeout_ms: deserializer.deserialize()?, - consolidation: consolidation_from_u8(deserializer.deserialize()?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, - allowed_destination: locality_from_u8(deserializer.deserialize()?)?, }) } } @@ -554,10 +609,14 @@ fn serialize_sample(serializer: &mut ZSerializer, sample: &zenoh::sample::Sample serializer, &sample.timestamp().map(|t| timestamp_to_ntp_id(t)), ); - serializer.serialize(congestion_control_to_u8(sample.congestion_control())); - serializer.serialize(priority_to_u8(sample.priority())); - serializer.serialize(sample.express()); - serializer.serialize(reliability_to_u8(sample.reliability())); + let qos = Qos::new( + sample.priority(), + sample.congestion_control(), + sample.express(), + sample.reliability(), + Locality::default(), + ); + serializer.serialize(qos); } pub(crate) struct Sample { @@ -635,9 +694,7 @@ pub(crate) struct ReplyOk { pub(crate) encoding: Encoding, pub(crate) attachment: Option>, pub(crate) timestamp: Option, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, + pub(crate) qos: Qos, } impl ReplyOk { @@ -649,9 +706,7 @@ impl ReplyOk { encoding: encoding_from_id_schema(deserializer.deserialize()?), attachment: deserialize_option(deserializer)?, timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, + qos: deserializer.deserialize()?, }) } } @@ -661,9 +716,7 @@ pub(crate) struct ReplyDel { pub(crate) keyexpr: OwnedKeyExpr, pub(crate) attachment: Option>, pub(crate) timestamp: Option, - pub(crate) congestion_control: CongestionControl, - pub(crate) priority: Priority, - pub(crate) express: bool, + pub(crate) qos: Qos, } impl ReplyDel { @@ -673,9 +726,7 @@ impl ReplyDel { keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, attachment: deserialize_option(deserializer)?, timestamp: opt_timestamp_from_ntp_id(deserialize_option(deserializer)?)?, - congestion_control: congestion_control_from_u8(deserializer.deserialize()?)?, - priority: priority_from_u8(deserializer.deserialize()?)?, - express: deserializer.deserialize()?, + qos: deserializer.deserialize()?, }) } } diff --git a/zenoh-plugin-remote-api/src/remote_state.rs b/zenoh-plugin-remote-api/src/remote_state.rs index 207a1f32..6d95f280 100644 --- a/zenoh-plugin-remote-api/src/remote_state.rs +++ b/zenoh-plugin-remote-api/src/remote_state.rs @@ -156,11 +156,11 @@ impl RemoteState { .session .declare_publisher(declare_publisher.keyexpr) .encoding(declare_publisher.encoding) - .priority(declare_publisher.priority) - .congestion_control(declare_publisher.congestion_control) - .express(declare_publisher.express) - .allowed_destination(declare_publisher.allowed_destination) - .reliability(declare_publisher.reliability) + .priority(declare_publisher.qos.priority()) + .congestion_control(declare_publisher.qos.congestion_control()) + .express(declare_publisher.qos.express()) + .allowed_destination(declare_publisher.qos.locality()) + .reliability(declare_publisher.qos.reliability()) .await?; self.admin_client .lock() @@ -309,14 +309,14 @@ impl RemoteState { let querier = self .session .declare_querier(declare_querier.keyexpr) - .priority(declare_querier.priority) - .congestion_control(declare_querier.congestion_control) - .express(declare_querier.express) - .allowed_destination(declare_querier.allowed_destination) - .accept_replies(declare_querier.accept_replies) - .target(declare_querier.target) + .priority(declare_querier.qos.priority()) + .congestion_control(declare_querier.qos.congestion_control()) + .express(declare_querier.qos.express()) + .allowed_destination(declare_querier.qos.locality()) + .accept_replies(declare_querier.query_settings.reply_keyexpr()) + .target(declare_querier.query_settings.target()) .timeout(Duration::from_millis(declare_querier.timeout_ms)) - .consolidation(declare_querier.consolidation) + .consolidation(declare_querier.query_settings.consolidation()) .await?; self.admin_client .lock() @@ -363,11 +363,11 @@ impl RemoteState { .put(put.keyexpr, put.payload) .encoding(put.encoding) .attachment(put.attachment) - .priority(put.priority) - .congestion_control(put.congestion_control) - .express(put.express) - .allowed_destination(put.allowed_destination) - .reliability(put.reliability) + .priority(put.qos.priority()) + .congestion_control(put.qos.congestion_control()) + .express(put.qos.express()) + .allowed_destination(put.qos.locality()) + .reliability(put.qos.reliability()) .timestamp(put.timestamp) .await?; Ok(()) @@ -377,11 +377,11 @@ impl RemoteState { self.session .delete(delete.keyexpr) .attachment(delete.attachment) - .priority(delete.priority) - .congestion_control(delete.congestion_control) - .express(delete.express) - .allowed_destination(delete.allowed_destination) - .reliability(delete.reliability) + .priority(delete.qos.priority()) + .congestion_control(delete.qos.congestion_control()) + .express(delete.qos.express()) + .allowed_destination(delete.qos.locality()) + .reliability(delete.qos.reliability()) .timestamp(delete.timestamp) .await?; Ok(()) @@ -463,13 +463,13 @@ impl RemoteState { gb = gb.encoding(encoding); } - gb.accept_replies(get.accept_replies) - .priority(get.priority) - .congestion_control(get.congestion_control) - .express(get.express) - .allowed_destination(get.allowed_destination) - .consolidation(get.consolidation) - .target(get.target) + gb.accept_replies(get.query_settings.reply_keyexpr()) + .priority(get.qos.priority()) + .congestion_control(get.qos.congestion_control()) + .express(get.qos.express()) + .allowed_destination(get.qos.locality()) + .consolidation(get.query_settings.consolidation()) + .target(get.query_settings.target()) .timeout(Duration::from_millis(get.timeout_ms)) .with(self.create_get_callback(get.id)) .await?; @@ -513,9 +513,9 @@ impl RemoteState { q.reply(reply_ok.keyexpr, reply_ok.payload) .attachment(reply_ok.attachment) .encoding(reply_ok.encoding) - .priority(reply_ok.priority) - .congestion_control(reply_ok.congestion_control) - .express(reply_ok.express) + .priority(reply_ok.qos.priority()) + .congestion_control(reply_ok.qos.congestion_control()) + .express(reply_ok.qos.express()) .timestamp(reply_ok.timestamp) .await?; } @@ -539,9 +539,9 @@ impl RemoteState { Some(q) => { q.reply_del(reply_del.keyexpr) .attachment(reply_del.attachment) - .priority(reply_del.priority) - .congestion_control(reply_del.congestion_control) - .express(reply_del.express) + .priority(reply_del.qos.priority()) + .congestion_control(reply_del.qos.congestion_control()) + .express(reply_del.qos.express()) .timestamp(reply_del.timestamp) .await?; } From f28388a7bb28d4a7f44832cfc46481bd484560c1 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 22 May 2025 20:09:42 +0200 Subject: [PATCH 04/23] wip ts --- zenoh-plugin-remote-api/src/interface/mod.rs | 14 +- zenoh-plugin-remote-api/src/remote_state.rs | 16 +- zenoh-ts/src/encoding.ts | 739 ++++++++++--------- zenoh-ts/src/enums.ts | 237 ++++++ zenoh-ts/src/ext/serialization.ts | 11 + zenoh-ts/src/message.ts | 638 ++++++++++++++++ zenoh-ts/src/remote_api/interface/types.ts | 19 + zenoh-ts/src/sample.ts | 357 +-------- zenoh-ts/src/timestamp.ts | 41 +- zenoh-ts/src/z_bytes.ts | 9 + zenoh-ts/src/zid.ts | 41 + 11 files changed, 1438 insertions(+), 684 deletions(-) create mode 100644 zenoh-ts/src/enums.ts create mode 100644 zenoh-ts/src/message.ts create mode 100644 zenoh-ts/src/remote_api/interface/types.ts create mode 100644 zenoh-ts/src/zid.ts diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index c21e74a6..6499ba94 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -402,7 +402,7 @@ pub(crate) struct DeclareQuerier { pub(crate) keyexpr: OwnedKeyExpr, pub(crate) qos: Qos, pub(crate) query_settings: QuerySettings, - pub(crate) timeout_ms: u64, + pub(crate) timeout_ms: u32, } impl DeclareQuerier { @@ -552,13 +552,13 @@ impl PublisherDelete { pub(crate) struct Get { pub(crate) id: u32, pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) parameters: Option, + pub(crate) parameters: String, pub(crate) payload: Option>, pub(crate) encoding: Option, pub(crate) attachment: Option>, pub(crate) qos: Qos, pub(crate) query_settings: QuerySettings, - pub(crate) timeout_ms: u64, + pub(crate) timeout_ms: u32, } impl Get { @@ -566,7 +566,7 @@ impl Get { Ok(Get { id: deserializer.deserialize()?, keyexpr: OwnedKeyExpr::try_from(deserializer.deserialize::()?)?, - parameters: deserialize_option(deserializer)?, + parameters: deserializer.deserialize()?, payload: deserialize_option(deserializer)?, encoding: opt_encoding_from_id_schema(deserialize_option(deserializer)?), attachment: deserialize_option(deserializer)?, @@ -580,7 +580,7 @@ impl Get { pub(crate) struct QuerierGet { pub(crate) querier_id: u32, pub(crate) id: u32, - pub(crate) parameters: Option, + pub(crate) parameters: String, pub(crate) payload: Option>, pub(crate) encoding: Option, pub(crate) attachment: Option>, @@ -591,7 +591,7 @@ impl QuerierGet { Ok(QuerierGet { querier_id: deserializer.deserialize()?, id: deserializer.deserialize()?, - parameters: deserialize_option(deserializer)?, + parameters: deserializer.deserialize()?, payload: deserialize_option(deserializer)?, encoding: opt_encoding_from_id_schema(deserialize_option(deserializer)?), attachment: deserialize_option(deserializer)?, @@ -826,7 +826,7 @@ impl UndeclareLivelinessSubscriber { pub(crate) struct LivelinessGet { pub(crate) id: u32, pub(crate) keyexpr: OwnedKeyExpr, - pub(crate) timeout_ms: u64, + pub(crate) timeout_ms: u32, } impl LivelinessGet { diff --git a/zenoh-plugin-remote-api/src/remote_state.rs b/zenoh-plugin-remote-api/src/remote_state.rs index 6d95f280..70351324 100644 --- a/zenoh-plugin-remote-api/src/remote_state.rs +++ b/zenoh-plugin-remote-api/src/remote_state.rs @@ -315,7 +315,7 @@ impl RemoteState { .allowed_destination(declare_querier.qos.locality()) .accept_replies(declare_querier.query_settings.reply_keyexpr()) .target(declare_querier.query_settings.target()) - .timeout(Duration::from_millis(declare_querier.timeout_ms)) + .timeout(Duration::from_millis(declare_querier.timeout_ms as u64)) .consolidation(declare_querier.query_settings.consolidation()) .await?; self.admin_client @@ -448,9 +448,9 @@ impl RemoteState { } async fn get(&self, get: Get) -> Result<(), zenoh_result::Error> { - let selector: Selector = match get.parameters { - Some(p) => (get.keyexpr, p).into(), - None => get.keyexpr.into(), + let selector: Selector = match get.parameters.len() > 0 { + true => (get.keyexpr, get.parameters).into(), + false => get.keyexpr.into(), }; let mut gb = self.session.get(selector); if let Some(payload) = get.payload { @@ -470,7 +470,7 @@ impl RemoteState { .allowed_destination(get.qos.locality()) .consolidation(get.query_settings.consolidation()) .target(get.query_settings.target()) - .timeout(Duration::from_millis(get.timeout_ms)) + .timeout(Duration::from_millis(get.timeout_ms as u64)) .with(self.create_get_callback(get.id)) .await?; Ok(()) @@ -489,8 +489,8 @@ impl RemoteState { if let Some(encoding) = querier_get.encoding { gb = gb.encoding(encoding); } - if let Some(params) = querier_get.parameters { - gb = gb.parameters(params); + if querier_get.parameters.len() > 0 { + gb = gb.parameters(querier_get.parameters); } gb.with(self.create_get_callback(querier_get.id)).await?; @@ -675,7 +675,7 @@ impl RemoteState { self.session .liveliness() .get(liveliness_get.keyexpr) - .timeout(Duration::from_millis(liveliness_get.timeout_ms)) + .timeout(Duration::from_millis(liveliness_get.timeout_ms as u64)) .with(self.create_get_callback(liveliness_get.id)) .await?; Ok(()) diff --git a/zenoh-ts/src/encoding.ts b/zenoh-ts/src/encoding.ts index f677ae38..914b6261 100644 --- a/zenoh-ts/src/encoding.ts +++ b/zenoh-ts/src/encoding.ts @@ -12,60 +12,85 @@ // ZettaScale Zenoh Team, // +import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; + enum EncodingPredefined { - ZENOH_BYTES = "zenoh/bytes", - ZENOH_STRING = "zenoh/string", - ZENOH_SERIALIZED = "zenoh/serialized", - APPLICATION_OCTET_STREAM = "application/octet-stream", - TEXT_PLAIN = "text/plain", - APPLICATION_JSON = "application/json", - TEXT_JSON = "text/json", - APPLICATION_CDR = "application/cdr", - APPLICATION_CBOR = "application/cbor", - APPLICATION_YAML = "application/yaml", - TEXT_YAML = "text/yaml", - TEXT_JSON5 = "text/json5", - APPLICATION_PROTOBUF = "application/protobuf", - APPLICATION_PYTHON_SERIALIZED_OBJECT = "application/python-serialized-object", - APPLICATION_JAVA_SERIALIZED_OBJECT = "application/java-serialized-object", - APPLICATION_OPENMETRICS_TEXT = "application/openmetrics-text", - IMAGE_PNG = "image/png", - IMAGE_JPEG = "image/jpeg", - IMAGE_GIF = "image/gif", - IMAGE_BMP = "image/bmp", - IMAGE_WEBP = "image/webp", - APPLICATION_XML = "application/xml", - APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded", - TEXT_HTML = "text/html", - TEXT_XML = "text/xml", - TEXT_CSS = "text/css", - TEXT_JAVASCRIPT = "text/javascript", - TEXT_MARKDOWN = "text/markdown", - TEXT_CSV = "text/csv", - APPLICATION_SQL = "application/sql", - APPLICATION_COAP_PAYLOAD = "application/coap-payload", - APPLICATION_JSON_PATCH_JSON = "application/json-patch+json", - APPLICATION_JSON_SEQ = "application/json-seq", - APPLICATION_JSONPATH = "application/jsonpath", - APPLICATION_JWT = "application/jwt", - APPLICATION_MP4 = "application/mp4", - APPLICATION_SOAP_XML = "application/soap+xml", - APPLICATION_YANG = "application/yang", - AUDIO_AAC = "audio/aac", - AUDIO_FLAC = "audio/flac", - AUDIO_MP4 = "audio/mp4", - AUDIO_OGG = "audio/ogg", - AUDIO_VORBIS = "audio/vorbis", - VIDEO_H261 = "video/h261", - VIDEO_H263 = "video/h263", - VIDEO_H264 = "video/h264", - VIDEO_H265 = "video/h265", - VIDEO_H266 = "video/h266", - VIDEO_MP4 = "video/mp4", - VIDEO_OGG = "video/ogg", - VIDEO_RAW = "video/raw", - VIDEO_VP8 = "video/vp8", - VIDEO_VP9 = "video/vp9", + ZENOH_BYTES = 0, + ZENOH_STRING, + ZENOH_SERIALIZED, + APPLICATION_OCTET_STREAM, + TEXT_PLAIN, + APPLICATION_JSON, + TEXT_JSON, + APPLICATION_CDR, + APPLICATION_CBOR, + APPLICATION_YAML, + TEXT_YAML, + TEXT_JSON5, + APPLICATION_PROTOBUF, + APPLICATION_PYTHON_SERIALIZED_OBJECT, + APPLICATION_JAVA_SERIALIZED_OBJECT, + APPLICATION_OPENMETRICS_TEXT, + IMAGE_PNG, + IMAGE_JPEG, + IMAGE_GIF, + IMAGE_BMP, + IMAGE_WEBP, + APPLICATION_XML, + APPLICATION_X_WWW_FORM_URLENCODED, + TEXT_HTML, + TEXT_XML, + TEXT_CSS, + TEXT_JAVASCRIPT, + TEXT_MARKDOWN, + TEXT_CSV, + APPLICATION_SQL, + APPLICATION_COAP_PAYLOAD, + APPLICATION_JSON_PATCH_JSON, + APPLICATION_JSON_SEQ, + APPLICATION_JSONPATH, + APPLICATION_JWT, + APPLICATION_MP4, + APPLICATION_SOAP_XML, + APPLICATION_YANG, + AUDIO_AAC, + AUDIO_FLAC, + AUDIO_MP4, + AUDIO_OGG, + AUDIO_VORBIS, + VIDEO_H261, + VIDEO_H263, + VIDEO_H264, + VIDEO_H265, + VIDEO_H266, + VIDEO_MP4, + VIDEO_OGG, + VIDEO_RAW, + VIDEO_VP8, + VIDEO_VP9, + // TODO: add id for specifying custom encoding +} + +function createIdToEncodingMap(): Map { + let out = new Map(); + for (let e in EncodingPredefined) { + let n = Number(e); + if (!isNaN(n)) { + out.set(n as EncodingPredefined, (EncodingPredefined[n] as string).toLocaleLowerCase().replaceAll('_', '/')); + } + } + return out; +} + +function createEncodingToIdMap(): Map { + let out = new Map(); + for (let e in EncodingPredefined) { + let n = Number(e); + if (!isNaN(n)) { + out.set((EncodingPredefined[n] as string).toLocaleLowerCase().replaceAll('_', '/'), n as EncodingPredefined); + } + } + return out; } export type IntoEncoding = Encoding | String | string; @@ -74,285 +99,341 @@ export type IntoEncoding = Encoding | String | string; * Zenoh Encoding Class */ export class Encoding { - private constructor(private strRep: string) {} + private static readonly ID_TO_ENCODING = createIdToEncodingMap(); + private static readonly ENCODING_TO_ID = createEncodingToIdMap(); + private static readonly SEP = ";"; - withSchema(input: string): Encoding { - const idx = this.strRep.indexOf(";"); - if (idx === -1) { - return new Encoding(this.strRep + ";" + input); - } else { - return new Encoding(this.strRep.substring(0, idx+1) + input); + + private constructor(private id?: EncodingPredefined, private schema?: string) {} + + withSchema(input: string): Encoding { + return new Encoding(this.id, input); + } + + static default(): Encoding { + return new Encoding(EncodingPredefined.ZENOH_BYTES); + } + + toString(): string { + let out: string = ""; + if (this.id != undefined) { + out += Encoding.ID_TO_ENCODING.get(this.id) as string; + } + if (this.schema != undefined) { + if (this.id != undefined) { + out += ";"; + } + out += this.schema; + } + return out; } - } - static default(): Encoding { - return new Encoding(EncodingPredefined.ZENOH_BYTES); - } + static fromString(input: string): Encoding { + if (input.length == 0) { + return new Encoding(EncodingPredefined.ZENOH_BYTES, undefined) + } + const idx = input.indexOf(Encoding.SEP); + if (idx == -1) { + return new Encoding(undefined, input); + } else { + const id = Encoding.ENCODING_TO_ID.get(input.substring(0, idx + 1)); + if (id != undefined) { + let schema = input.substring(idx + 1) + return new Encoding(id, schema.length == 0 ? undefined : schema); + } else { + return new Encoding(undefined, input); + } + } + } + + // Enum Variants + /** + * Constant alias for string "zenoh/bytes" + */ + static readonly ZENOH_BYTES = new Encoding(EncodingPredefined.ZENOH_BYTES); + /** + * Constant alias for string "zenoh/string" + */ + static readonly ZENOH_STRING: Encoding = new Encoding(EncodingPredefined.ZENOH_STRING); + /** + * Constant alias for string "zenoh/serialized" + */ + static readonly ZENOH_SERIALIZED: Encoding = new Encoding( + EncodingPredefined.ZENOH_SERIALIZED + ); + /** + * Constant alias for string "application/octet-stream" + */ + static readonly APPLICATION_OCTET_STREAM: Encoding = new Encoding( + EncodingPredefined.APPLICATION_OCTET_STREAM + ); + /** + * Constant alias for string "text/plain" + */ + static readonly TEXT_PLAIN: Encoding = new Encoding(EncodingPredefined.TEXT_PLAIN); + /** + * Constant alias for string "application/json" + */ + static readonly APPLICATION_JSON: Encoding = new Encoding( + EncodingPredefined.APPLICATION_JSON + ); + /** + * Constant alias for string "text/json" + */ + static readonly TEXT_JSON: Encoding = new Encoding(EncodingPredefined.TEXT_JSON); + /** + * Constant alias for string "application/cdr" + */ + static readonly APPLICATION_CDR: Encoding = new Encoding( + EncodingPredefined.APPLICATION_CDR + ); + /** + * Constant alias for string "application/cbor" + */ + static readonly APPLICATION_CBOR: Encoding = new Encoding( + EncodingPredefined.APPLICATION_CBOR + ); + /** + * Constant alias for string "application/yaml" + */ + static readonly APPLICATION_YAML: Encoding = new Encoding( + EncodingPredefined.APPLICATION_YAML + ); + /** + * Constant alias for string "text/yaml" + */ + static readonly TEXT_YAML: Encoding = new Encoding(EncodingPredefined.TEXT_YAML); + /** + * Constant alias for string "text/json5" + */ + static readonly TEXT_JSON5: Encoding = new Encoding(EncodingPredefined.TEXT_JSON5); + /** + * Constant alias for string "application/protobuf" + */ + static readonly APPLICATION_PROTOBUF: Encoding = new Encoding( + EncodingPredefined.APPLICATION_PROTOBUF + ); + /** + * Constant alias for string "application/python-serialized-object" + */ + static readonly APPLICATION_PYTHON_SERIALIZED_OBJECT: Encoding = new Encoding( + EncodingPredefined.APPLICATION_PYTHON_SERIALIZED_OBJECT + ); + /** + * Constant alias for string "application/java-serialized-object" + */ + static readonly APPLICATION_JAVA_SERIALIZED_OBJECT: Encoding = new Encoding( + EncodingPredefined.APPLICATION_JAVA_SERIALIZED_OBJECT + ); + /** + * Constant alias for string "application/openmetrics-text" + */ + static readonly APPLICATION_OPENMETRICS_TEXT: Encoding = new Encoding( + EncodingPredefined.APPLICATION_OPENMETRICS_TEXT + ); + /** + * Constant alias for string "image/png" + */ + static readonly IMAGE_PNG: Encoding = new Encoding(EncodingPredefined.IMAGE_PNG); + /** + * Constant alias for string "image/jpeg" + */ + static readonly IMAGE_JPEG: Encoding = new Encoding(EncodingPredefined.IMAGE_JPEG); + /** + * Constant alias for string "image/gif" + */ + static readonly IMAGE_GIF: Encoding = new Encoding(EncodingPredefined.IMAGE_GIF); + /** + * Constant alias for string "image/bmp" + */ + static readonly IMAGE_BMP: Encoding = new Encoding(EncodingPredefined.IMAGE_BMP); + /** + * Constant alias for string "image/webp" + */ + static readonly IMAGE_WEBP: Encoding = new Encoding(EncodingPredefined.IMAGE_WEBP); + /** + * Constant alias for string "application/xml" + */ + static readonly APPLICATION_XML: Encoding = new Encoding( + EncodingPredefined.APPLICATION_XML + ); + /** + * Constant alias for string "application/x-www-form-urlencoded" + */ + static readonly APPLICATION_X_WWW_FORM_URLENCODED: Encoding = new Encoding( + EncodingPredefined.APPLICATION_X_WWW_FORM_URLENCODED + ); + /** + * Constant alias for string "text/html" + */ + static readonly TEXT_HTML: Encoding = new Encoding(EncodingPredefined.TEXT_HTML); + /** + * Constant alias for string "text/xml" + */ + static readonly TEXT_XML: Encoding = new Encoding(EncodingPredefined.TEXT_XML); + /** + * Constant alias for string "text/css" + */ + static readonly TEXT_CSS: Encoding = new Encoding(EncodingPredefined.TEXT_CSS); + /** + * Constant alias for string "text/javascript" + */ + static readonly TEXT_JAVASCRIPT: Encoding = new Encoding( + EncodingPredefined.TEXT_JAVASCRIPT + ); + /** + * Constant alias for string "text/markdown" + */ + static readonly TEXT_MARKDOWN: Encoding = new Encoding( + EncodingPredefined.TEXT_MARKDOWN + ); + /** + * Constant alias for string "text/csv" + */ + static readonly TEXT_CSV: Encoding = new Encoding(EncodingPredefined.TEXT_CSV); + /** + * Constant alias for string "application/sql" + */ + static readonly APPLICATION_SQL: Encoding = new Encoding( + EncodingPredefined.APPLICATION_SQL + ); + /** + * Constant alias for string "application/coap-payload" + */ + static readonly APPLICATION_COAP_PAYLOAD: Encoding = new Encoding( + EncodingPredefined.APPLICATION_COAP_PAYLOAD + ); + /** + * Constant alias for string "application/json-patch+json" + */ + static readonly APPLICATION_JSON_PATCH_JSON: Encoding = new Encoding( + EncodingPredefined.APPLICATION_JSON_PATCH_JSON + ); + /** + * Constant alias for string "application/json-seq" + */ + static readonly APPLICATION_JSON_SEQ: Encoding = new Encoding( + EncodingPredefined.APPLICATION_JSON_SEQ + ); + /** + * Constant alias for string "application/jsonpath" + */ + static readonly APPLICATION_JSONPATH: Encoding = new Encoding( + EncodingPredefined.APPLICATION_JSONPATH + ); + /** + * Constant alias for string "application/jwt" + */ + static readonly APPLICATION_JWT: Encoding = new Encoding( + EncodingPredefined.APPLICATION_JWT + ); + /** + * Constant alias for string "application/mp4" + */ + static readonly APPLICATION_MP4: Encoding = new Encoding( + EncodingPredefined.APPLICATION_MP4 + ); + /** + * Constant alias for string "application/soap+xml" + */ + static readonly APPLICATION_SOAP_XML: Encoding = new Encoding( + EncodingPredefined.APPLICATION_SOAP_XML + ); + /** + * Constant alias for string "application/yang" + */ + static readonly APPLICATION_YANG: Encoding = new Encoding( + EncodingPredefined.APPLICATION_YANG + ); + /** + * Constant alias for string "audio/aac" + */ + static readonly AUDIO_AAC: Encoding = new Encoding(EncodingPredefined.AUDIO_AAC); + /** + * Constant alias for string "audio/flac" + */ + static readonly AUDIO_FLAC: Encoding = new Encoding(EncodingPredefined.AUDIO_FLAC); + /** + * Constant alias for string "audio/mp4" + */ + static readonly AUDIO_MP4: Encoding = new Encoding(EncodingPredefined.AUDIO_MP4); + /** + * Constant alias for string "audio/ogg" + */ + static readonly AUDIO_OGG: Encoding = new Encoding(EncodingPredefined.AUDIO_OGG); + /** + * Constant alias for string "audio/vorbis" + */ + static readonly AUDIO_VORBIS: Encoding = new Encoding(EncodingPredefined.AUDIO_VORBIS); + /** + * Constant alias for string "video/h261" + */ + static readonly VIDEO_H261: Encoding = new Encoding(EncodingPredefined.VIDEO_H261); + /** + * Constant alias for string "video/h263" + */ + static readonly VIDEO_H263: Encoding = new Encoding(EncodingPredefined.VIDEO_H263); + /** + * Constant alias for string "video/h264" + */ + static readonly VIDEO_H264: Encoding = new Encoding(EncodingPredefined.VIDEO_H264); + /** + * Constant alias for string "video/h265" + */ + static readonly VIDEO_H265: Encoding = new Encoding(EncodingPredefined.VIDEO_H265); + /** + * Constant alias for string "video/h266" + */ + static readonly VIDEO_H266: Encoding = new Encoding(EncodingPredefined.VIDEO_H266); + /** + * Constant alias for string "video/mp4" + */ + static readonly VIDEO_MP4: Encoding = new Encoding(EncodingPredefined.VIDEO_MP4); + /** + * Constant alias for string "video/ogg" + */ + static readonly VIDEO_OGG: Encoding = new Encoding(EncodingPredefined.VIDEO_OGG); + /** + * Constant alias for string "video/raw" + */ + static readonly VIDEO_RAW: Encoding = new Encoding(EncodingPredefined.VIDEO_RAW); + /** + * Constant alias for string "video/vp8" + */ + static readonly VIDEO_VP8: Encoding = new Encoding(EncodingPredefined.VIDEO_VP8); + /** + * Constant alias for string "video/vp9" + */ + static readonly VIDEO_VP9: Encoding = new Encoding(EncodingPredefined.VIDEO_VP9); - toString(): string { - return this.strRep; - } - static fromString(input: string): Encoding { - return new Encoding(input); - } + public serializeWithZSerializer(serializer: ZBytesSerializer) { + // TODO: add id for specifying custom encoding + serializer.serializeNumberUint16(this.id ?? EncodingPredefined.ZENOH_BYTES); + if (this.schema == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeString(this.schema); + } + } - // Enum Variants - /** - * Constant alias for string "zenoh/bytes" - */ - static readonly ZENOH_BYTES = new Encoding(EncodingPredefined.ZENOH_BYTES); - /** - * Constant alias for string "zenoh/string" - */ - static readonly ZENOH_STRING: Encoding = new Encoding(EncodingPredefined.ZENOH_STRING); - /** - * Constant alias for string "zenoh/serialized" - */ - static readonly ZENOH_SERIALIZED: Encoding = new Encoding( - EncodingPredefined.ZENOH_SERIALIZED - ); - /** - * Constant alias for string "application/octet-stream" - */ - static readonly APPLICATION_OCTET_STREAM: Encoding = new Encoding( - EncodingPredefined.APPLICATION_OCTET_STREAM - ); - /** - * Constant alias for string "text/plain" - */ - static readonly TEXT_PLAIN: Encoding = new Encoding(EncodingPredefined.TEXT_PLAIN); - /** - * Constant alias for string "application/json" - */ - static readonly APPLICATION_JSON: Encoding = new Encoding( - EncodingPredefined.APPLICATION_JSON - ); - /** - * Constant alias for string "text/json" - */ - static readonly TEXT_JSON: Encoding = new Encoding(EncodingPredefined.TEXT_JSON); - /** - * Constant alias for string "application/cdr" - */ - static readonly APPLICATION_CDR: Encoding = new Encoding( - EncodingPredefined.APPLICATION_CDR - ); - /** - * Constant alias for string "application/cbor" - */ - static readonly APPLICATION_CBOR: Encoding = new Encoding( - EncodingPredefined.APPLICATION_CBOR - ); - /** - * Constant alias for string "application/yaml" - */ - static readonly APPLICATION_YAML: Encoding = new Encoding( - EncodingPredefined.APPLICATION_YAML - ); - /** - * Constant alias for string "text/yaml" - */ - static readonly TEXT_YAML: Encoding = new Encoding(EncodingPredefined.TEXT_YAML); - /** - * Constant alias for string "text/json5" - */ - static readonly TEXT_JSON5: Encoding = new Encoding(EncodingPredefined.TEXT_JSON5); - /** - * Constant alias for string "application/protobuf" - */ - static readonly APPLICATION_PROTOBUF: Encoding = new Encoding( - EncodingPredefined.APPLICATION_PROTOBUF - ); - /** - * Constant alias for string "application/python-serialized-object" - */ - static readonly APPLICATION_PYTHON_SERIALIZED_OBJECT: Encoding = new Encoding( - EncodingPredefined.APPLICATION_PYTHON_SERIALIZED_OBJECT - ); - /** - * Constant alias for string "application/java-serialized-object" - */ - static readonly APPLICATION_JAVA_SERIALIZED_OBJECT: Encoding = new Encoding( - EncodingPredefined.APPLICATION_JAVA_SERIALIZED_OBJECT - ); - /** - * Constant alias for string "application/openmetrics-text" - */ - static readonly APPLICATION_OPENMETRICS_TEXT: Encoding = new Encoding( - EncodingPredefined.APPLICATION_OPENMETRICS_TEXT - ); - /** - * Constant alias for string "image/png" - */ - static readonly IMAGE_PNG: Encoding = new Encoding(EncodingPredefined.IMAGE_PNG); - /** - * Constant alias for string "image/jpeg" - */ - static readonly IMAGE_JPEG: Encoding = new Encoding(EncodingPredefined.IMAGE_JPEG); - /** - * Constant alias for string "image/gif" - */ - static readonly IMAGE_GIF: Encoding = new Encoding(EncodingPredefined.IMAGE_GIF); - /** - * Constant alias for string "image/bmp" - */ - static readonly IMAGE_BMP: Encoding = new Encoding(EncodingPredefined.IMAGE_BMP); - /** - * Constant alias for string "image/webp" - */ - static readonly IMAGE_WEBP: Encoding = new Encoding(EncodingPredefined.IMAGE_WEBP); - /** - * Constant alias for string "application/xml" - */ - static readonly APPLICATION_XML: Encoding = new Encoding( - EncodingPredefined.APPLICATION_XML - ); - /** - * Constant alias for string "application/x-www-form-urlencoded" - */ - static readonly APPLICATION_X_WWW_FORM_URLENCODED: Encoding = new Encoding( - EncodingPredefined.APPLICATION_X_WWW_FORM_URLENCODED - ); - /** - * Constant alias for string "text/html" - */ - static readonly TEXT_HTML: Encoding = new Encoding(EncodingPredefined.TEXT_HTML); - /** - * Constant alias for string "text/xml" - */ - static readonly TEXT_XML: Encoding = new Encoding(EncodingPredefined.TEXT_XML); - /** - * Constant alias for string "text/css" - */ - static readonly TEXT_CSS: Encoding = new Encoding(EncodingPredefined.TEXT_CSS); - /** - * Constant alias for string "text/javascript" - */ - static readonly TEXT_JAVASCRIPT: Encoding = new Encoding( - EncodingPredefined.TEXT_JAVASCRIPT - ); - /** - * Constant alias for string "text/markdown" - */ - static readonly TEXT_MARKDOWN: Encoding = new Encoding( - EncodingPredefined.TEXT_MARKDOWN - ); - /** - * Constant alias for string "text/csv" - */ - static readonly TEXT_CSV: Encoding = new Encoding(EncodingPredefined.TEXT_CSV); - /** - * Constant alias for string "application/sql" - */ - static readonly APPLICATION_SQL: Encoding = new Encoding( - EncodingPredefined.APPLICATION_SQL - ); - /** - * Constant alias for string "application/coap-payload" - */ - static readonly APPLICATION_COAP_PAYLOAD: Encoding = new Encoding( - EncodingPredefined.APPLICATION_COAP_PAYLOAD - ); - /** - * Constant alias for string "application/json-patch+json" - */ - static readonly APPLICATION_JSON_PATCH_JSON: Encoding = new Encoding( - EncodingPredefined.APPLICATION_JSON_PATCH_JSON - ); - /** - * Constant alias for string "application/json-seq" - */ - static readonly APPLICATION_JSON_SEQ: Encoding = new Encoding( - EncodingPredefined.APPLICATION_JSON_SEQ - ); - /** - * Constant alias for string "application/jsonpath" - */ - static readonly APPLICATION_JSONPATH: Encoding = new Encoding( - EncodingPredefined.APPLICATION_JSONPATH - ); - /** - * Constant alias for string "application/jwt" - */ - static readonly APPLICATION_JWT: Encoding = new Encoding( - EncodingPredefined.APPLICATION_JWT - ); - /** - * Constant alias for string "application/mp4" - */ - static readonly APPLICATION_MP4: Encoding = new Encoding( - EncodingPredefined.APPLICATION_MP4 - ); - /** - * Constant alias for string "application/soap+xml" - */ - static readonly APPLICATION_SOAP_XML: Encoding = new Encoding( - EncodingPredefined.APPLICATION_SOAP_XML - ); - /** - * Constant alias for string "application/yang" - */ - static readonly APPLICATION_YANG: Encoding = new Encoding( - EncodingPredefined.APPLICATION_YANG - ); - /** - * Constant alias for string "audio/aac" - */ - static readonly AUDIO_AAC: Encoding = new Encoding(EncodingPredefined.AUDIO_AAC); - /** - * Constant alias for string "audio/flac" - */ - static readonly AUDIO_FLAC: Encoding = new Encoding(EncodingPredefined.AUDIO_FLAC); - /** - * Constant alias for string "audio/mp4" - */ - static readonly AUDIO_MP4: Encoding = new Encoding(EncodingPredefined.AUDIO_MP4); - /** - * Constant alias for string "audio/ogg" - */ - static readonly AUDIO_OGG: Encoding = new Encoding(EncodingPredefined.AUDIO_OGG); - /** - * Constant alias for string "audio/vorbis" - */ - static readonly AUDIO_VORBIS: Encoding = new Encoding(EncodingPredefined.AUDIO_VORBIS); - /** - * Constant alias for string "video/h261" - */ - static readonly VIDEO_H261: Encoding = new Encoding(EncodingPredefined.VIDEO_H261); - /** - * Constant alias for string "video/h263" - */ - static readonly VIDEO_H263: Encoding = new Encoding(EncodingPredefined.VIDEO_H263); - /** - * Constant alias for string "video/h264" - */ - static readonly VIDEO_H264: Encoding = new Encoding(EncodingPredefined.VIDEO_H264); - /** - * Constant alias for string "video/h265" - */ - static readonly VIDEO_H265: Encoding = new Encoding(EncodingPredefined.VIDEO_H265); - /** - * Constant alias for string "video/h266" - */ - static readonly VIDEO_H266: Encoding = new Encoding(EncodingPredefined.VIDEO_H266); - /** - * Constant alias for string "video/mp4" - */ - static readonly VIDEO_MP4: Encoding = new Encoding(EncodingPredefined.VIDEO_MP4); - /** - * Constant alias for string "video/ogg" - */ - static readonly VIDEO_OGG: Encoding = new Encoding(EncodingPredefined.VIDEO_OGG); - /** - * Constant alias for string "video/raw" - */ - static readonly VIDEO_RAW: Encoding = new Encoding(EncodingPredefined.VIDEO_RAW); - /** - * Constant alias for string "video/vp8" - */ - static readonly VIDEO_VP8: Encoding = new Encoding(EncodingPredefined.VIDEO_VP8); - /** - * Constant alias for string "video/vp9" - */ - static readonly VIDEO_VP9: Encoding = new Encoding(EncodingPredefined.VIDEO_VP9); + public static deserialize(deserializer: ZBytesDeserializer): Encoding { + // TODO: add id for specifying custom encoding + let id = deserializer.deserializeNumberUint16(); + let schema: string | undefined; + if (deserializer.deserializeBoolean()) { + schema = deserializer.deserializeString(); + } else { + schema = undefined; + } + return new Encoding(id, schema); + } +} + +export function deserializeOptEncoding(deserializer: ZBytesDeserializer): Encoding | undefined { + if (deserializer.deserializeBoolean()) { + return Encoding.deserialize(deserializer); + } else { + return undefined; + } } diff --git a/zenoh-ts/src/enums.ts b/zenoh-ts/src/enums.ts new file mode 100644 index 00000000..6b78f5c6 --- /dev/null +++ b/zenoh-ts/src/enums.ts @@ -0,0 +1,237 @@ +// +// Copyright (c) 2025 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +// Message priority. +export const enum Priority { + REAL_TIME = 1, + INTERACTIVE_HIGH = 2, + INTERACTIVE_LOW = 3, + DATA_HIGH = 4, + DATA = 5, + DATA_LOW = 6, + BACKGROUND = 7, + DEFAULT = DATA, +} + +// Congestion control strategy. +export const enum CongestionControl { + // When transmitting a message in a node with a full queue, the node may drop the message. + DROP = 0, + // When transmitting a message in a node with a full queue, the node will wait for queue to + // progress. + BLOCK = 1, + DEFAULT_PUSH = DROP, + DEFAULT_REQUEST = BLOCK +} + +export const enum Reliability { + BEST_EFFORT = 0, + RELIABLE = 1, + DEFAULT = RELIABLE +} + +// The locality of samples to be received by subscribers or targeted by publishers. +export const enum Locality { + SESSION_LOCAL = 0, + REMOTE = 1, + ANY = 2, + DEFAULT = ANY +} + + +export const enum SampleKind { + Put = 0, + Delete = 1 +} + +export function sampleKindFromUint8(val: number): SampleKind { + switch (val) { + case SampleKind.Put: return SampleKind.Put; + case SampleKind.Delete: return SampleKind.Delete; + default: { + console.warn(`Unsupported SampleKind value ${val}`); + return SampleKind.Put; + } + } +} + +function priorityFromUint8(val: number): Priority { + switch (val) { + case Priority.REAL_TIME: return Priority.REAL_TIME; + case Priority.INTERACTIVE_HIGH: return Priority.INTERACTIVE_HIGH; + case Priority.INTERACTIVE_LOW: return Priority.INTERACTIVE_LOW; + case Priority.DATA_HIGH: return Priority.DATA_HIGH; + case Priority.DATA: return Priority.DATA; + case Priority.DATA_LOW: return Priority.DATA_LOW; + case Priority.BACKGROUND: return Priority.BACKGROUND; + default: { + console.warn(`Unsupported Priority value ${val}`); + return Priority.DEFAULT; + } + } +} + +function congestionControlFromUint8(val: number): CongestionControl { + switch (val) { + case CongestionControl.DROP: return CongestionControl.DROP; + case CongestionControl.BLOCK: return CongestionControl.BLOCK; + default: { + console.warn(`Unsupported CongestionControl value ${val}`); + return CongestionControl.DEFAULT_PUSH; + } + } +} + +function reliabilityFromUint8(val: number): Reliability { + switch (val) { + case Reliability.BEST_EFFORT: return Reliability.BEST_EFFORT; + case Reliability.RELIABLE: return Reliability.RELIABLE; + default: { + console.warn(`Unsupported Reliability value ${val}`); + return Reliability.DEFAULT; + } + } +} + +function localityFromUint8(val: number): Locality { + switch (val) { + case Locality.SESSION_LOCAL: return Locality.SESSION_LOCAL; + case Locality.REMOTE: return Locality.REMOTE; + case Locality.ANY: return Locality.ANY; + default: { + console.warn(`Unsupported Locality value ${val}`); + return Locality.DEFAULT; + } + } +} + +export class Qos { + constructor( + public readonly priority: Priority, + public readonly congestion_control: CongestionControl, + public readonly express: boolean, + public readonly reliability: Reliability, + public readonly locality: Locality + ) {} + + public toUint8(): number { + // llrecppp + let e = this.express ? 1 : 0; + return this.priority | (this.congestion_control << 3) | (e << 4) | (this.reliability << 5) | (this.locality << 6); + } + + public static fromUint8(val: number): Qos { + let p = val & 0b111; + let c = (val >> 3) & 0b1; + let e = (val >> 4) & 0b1; + let r = (val >> 5) & 0b1; + let l = (val >> 6) & 0b11; + return new Qos(priorityFromUint8(p), congestionControlFromUint8(c), e != 0, reliabilityFromUint8(r), localityFromUint8(l)); + } +} + +// The `zenoh::queryable::Queryable`s that should be target of a `zenoh::Session::get()`. +export const enum QueryTarget { + // Let Zenoh find the BestMatching queryable capabale of serving the query. + BEST_MATCHING = 0, + // Deliver the query to all queryables matching the query's key expression. + ALL = 1, + // Deliver the query to all queryables matching the query's key expression that are declared as complete. + ALL_COMPLETE = 2, + DEFAULT = BEST_MATCHING +} + +// The kind of consolidation to apply to a query. +export const enum ConsolidationMode { + // Apply automatic consolidation based on queryable's preferences + AUTO = 0, + // No consolidation applied: multiple samples may be received for the same key-timestamp. + NONE = 1, + // Monotonic consolidation immediately forwards samples, except if one with an equal or more recent timestamp + // has already been sent with the same key. + // + // This optimizes latency while potentially reducing bandwidth. + // + // Note that this doesn't cause re-ordering, but drops the samples for which a more recent timestamp has already + // been observed with the same key. + MONOTONIC = 2, + // Holds back samples to only send the set of samples that had the highest timestamp for their key. + LATEST = 3, + DEFAULT = AUTO +} + +// The kind of accepted query replies. +export const enum ReplyKeyExpr { + // Accept replies whose key expressions may not match the query key expression. + ANY = 0, + // // Accept replies whose key expressions match the query key expression. + MATCHING_QUERY = 1, + DEFAULT = MATCHING_QUERY +} + +function queryTargetFromUint8(val: number): QueryTarget { + switch (val) { + case QueryTarget.BEST_MATCHING: return QueryTarget.BEST_MATCHING; + case QueryTarget.ALL: return QueryTarget.ALL; + case QueryTarget.ALL_COMPLETE: return QueryTarget.ALL_COMPLETE; + default: { + console.warn(`Unsupported QueryTarget value ${val}`); + return QueryTarget.DEFAULT; + } + } +} + +function consolidationModeFromUint8(val: number): ConsolidationMode { + switch (val) { + case ConsolidationMode.AUTO: return ConsolidationMode.AUTO; + case ConsolidationMode.NONE: return ConsolidationMode.NONE; + case ConsolidationMode.MONOTONIC: return ConsolidationMode.MONOTONIC; + case ConsolidationMode.LATEST: return ConsolidationMode.LATEST; + default: { + console.warn(`Unsupported ConsolidationMode value ${val}`); + return ConsolidationMode.DEFAULT; + } + } +} + +function replyKeyExprFromUint8(val: number): ReplyKeyExpr { + switch (val) { + case ReplyKeyExpr.ANY: return ReplyKeyExpr.ANY; + case ReplyKeyExpr.MATCHING_QUERY: return ReplyKeyExpr.MATCHING_QUERY; + default: { + console.warn(`Unsupported ReplyKeyExpr value ${val}`); + return ReplyKeyExpr.DEFAULT; + } + } +} + +export class QuerySettings { + constructor( + public readonly target: QueryTarget, + public readonly consolidation: ConsolidationMode, + public readonly replyKeyExpr: ReplyKeyExpr, + ) {} + + public toUint8(): number { + // rcctt + return this.target | (this.consolidation << 2) | (this.replyKeyExpr << 4); + } + + public static fromUint8(val: number): QuerySettings { + let t = val & 0b11; + let c = (val >> 2) & 0b11; + let r = (val >> 4) & 0b1; + return new QuerySettings(queryTargetFromUint8(t), consolidationModeFromUint8(c), replyKeyExprFromUint8(r)); + } +} \ No newline at end of file diff --git a/zenoh-ts/src/ext/serialization.ts b/zenoh-ts/src/ext/serialization.ts index 18a56db2..a11501dc 100644 --- a/zenoh-ts/src/ext/serialization.ts +++ b/zenoh-ts/src/ext/serialization.ts @@ -745,6 +745,17 @@ export namespace ZD{ ); } + /** + * Indicates that data should be deserialized as an object. + * @param create A function to create an object instance from deserializer. + * @returns Object deserialization tag. + */ + export function objectStatic(create: (deserializer: ZBytesDeserializer) => T): ZDTypeInfo { + return new ZDTypeInfo( + (z: ZBytesDeserializer) => { return create(z); } + ); + } + /** * Indicates that data should be deserialized as an array. * @param t An array element deserialization tag. diff --git a/zenoh-ts/src/message.ts b/zenoh-ts/src/message.ts new file mode 100644 index 00000000..769824be --- /dev/null +++ b/zenoh-ts/src/message.ts @@ -0,0 +1,638 @@ +// +// Copyright (c) 2025 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +import { ZBytesDeserializer, ZBytesSerializer, ZD } from "./ext" +import { Encoding } from "./encoding.js"; +import { KeyExpr } from "./key_expr.js"; +import { Qos, Locality, QuerySettings } from "./enums.js"; +import { Timestamp } from "./timestamp"; +import { Zid } from "./zid"; +import { Sample } from "./sample"; +import { ZBytes } from "./z_bytes"; +import { Query } from "./query"; + +const enum OutRemoteMessageId { + DeclarePublisher = 0, + UndeclarePublisher, + DeclareSubscriber, + UndeclareSubscriber, + DeclareQueryable, + UndeclareQueryable, + DeclareQuerier, + UndeclareQuerier, + DeclareLivelinessToken, + UndeclareLivelinessToken, + DeclareLivelinessSubscriber, + UndeclareLivelinessSubscriber, + GetSessionInfo, + GetTimestamp, + Put, + Delete, + PublisherPut, + PublisherDelete, + Get, + QuerierGet, + LivelinessGet, + ReplyOk, + ReplyDel, + ReplyErr, + QueryResponseFinal, +} + +class DeclarePublisher { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclarePublisher; + public constructor( + public readonly id: number, + public readonly keyexpr: KeyExpr, + public readonly encoding: Encoding, + public readonly qos: Qos + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + serializer.serializeString(this.keyexpr.toString()); + this.encoding.serializeWithZSerializer(serializer); + serializer.serializeNumberUint8(this.qos.toUint8()); + } +} + +class UndeclarePublisher { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclarePublisher; + public constructor( + public readonly id: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + } +} + +class DeclareSubscriber { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareSubscriber; + public constructor( + public readonly id: number, + public readonly keyexpr: KeyExpr, + public readonly allowed_origin: Locality + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + serializer.serializeString(this.keyexpr.toString()); + serializer.serializeNumberUint8(this.allowed_origin); + } +} + +class UndeclareSubscriber { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareSubscriber; + public constructor( + public readonly id: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + } +} + +class DeclareQueryable { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareQueryable; + public constructor( + public readonly id: number, + public readonly keyexpr: KeyExpr, + public readonly complete: boolean, + public readonly allowed_origin: Locality + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + serializer.serializeString(this.keyexpr.toString()); + serializer.serializeBoolean(this.complete); + serializer.serializeNumberUint8(this.allowed_origin); + } +} + +class UndeclareQueryable { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareQueryable; + public constructor( + public readonly id: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + } +} + +class DeclareQuerier { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareQuerier; + public constructor( + public readonly id: number, + public readonly keyexpr: KeyExpr, + public readonly qos: Qos, + public readonly querySettings: QuerySettings, + public readonly timeout_ms: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + serializer.serializeString(this.keyexpr.toString()); + serializer.serializeNumberUint8(this.qos.toUint8()); + serializer.serializeNumberUint8(this.qos.toUint8()); + serializer.serializeNumberUint32(this.timeout_ms); + } +} + +class UndeclareQuerier { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareQuerier; + public constructor( + public readonly id: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + } +} + +class DeclareLivelinessToken { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareLivelinessToken; + public constructor( + public readonly id: number, + public readonly keyexpr: KeyExpr, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + serializer.serializeString(this.keyexpr.toString()); + } +} + +class UndeclareLivelinessToken { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareLivelinessToken; + public constructor( + public readonly id: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + } +} + +class DeclareLivelinessSubscriber { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareLivelinessSubscriber; + public constructor( + public readonly id: number, + public readonly keyexpr: KeyExpr, + public readonly history: boolean, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + serializer.serializeString(this.keyexpr.toString()); + serializer.serializeBoolean(this.history); + } +} + +class GetSessionInfo { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.GetSessionInfo; + public constructor() {} + + public serializeWithZSerializer(_serializer: ZBytesSerializer) {} +} + +class GetTimestamp { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.GetTimestamp; + public constructor() {} + + public serializeWithZSerializer(_serializer: ZBytesSerializer) {} +} + +class Put { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.Put; + public constructor( + public readonly keyexpr: KeyExpr, + public readonly payload: Uint8Array, + public readonly encoding: Encoding, + public readonly attachment: Uint8Array | undefined, + public readonly timestamp: Timestamp | undefined, + public readonly qos: Qos + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeString(this.keyexpr.toString()); + serializer.serializeUint8Array(this.payload); + this.encoding.serializeWithZSerializer(serializer); + if (this.attachment == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeUint8Array(this.attachment); + } + if (this.timestamp == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + this.timestamp.serializeWithZSerializer(serializer); + } + serializer.serializeNumberUint8(this.qos.toUint8()); + } +} + +class Delete { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.Delete; + public constructor( + public readonly keyexpr: KeyExpr, + public readonly attachment: Uint8Array | undefined, + public readonly timestamp: Timestamp | undefined, + public readonly qos: Qos + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeString(this.keyexpr.toString()); + if (this.attachment == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeUint8Array(this.attachment); + } + if (this.timestamp == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + this.timestamp.serializeWithZSerializer(serializer); + } + serializer.serializeNumberUint8(this.qos.toUint8()); + } +} + +class PublisherPut { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.PublisherPut; + public constructor( + public readonly publisherId: number, + public readonly payload: Uint8Array, + public readonly encoding: Encoding | undefined, + public readonly attachment: Uint8Array | undefined, + public readonly timestamp: Timestamp | undefined, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint32(this.publisherId); + serializer.serializeUint8Array(this.payload); + if (this.encoding == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + this.encoding.serializeWithZSerializer(serializer); + } + if (this.attachment == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeUint8Array(this.attachment); + } + if (this.timestamp == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + this.timestamp.serializeWithZSerializer(serializer); + } + } +} + +class PublisherDelete { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.PublisherDelete; + public constructor( + public readonly publisherId: number, + public readonly attachment: Uint8Array | undefined, + public readonly timestamp: Timestamp | undefined, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint32(this.publisherId); + if (this.attachment == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeUint8Array(this.attachment); + } + if (this.timestamp == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + this.timestamp.serializeWithZSerializer(serializer); + } + } +} + +class Get { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.Get; + public constructor( + public readonly id: number, + public readonly keyexpr: KeyExpr, + public readonly parameters: string, + public readonly payload: Uint8Array | undefined, + public readonly encoding: Encoding | undefined, + public readonly attachment: Uint8Array | undefined, + public readonly qos: Qos, + public readonly querySettings: QuerySettings, + public readonly timeout_ms: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint32(this.id); + serializer.serializeString(this.keyexpr.toString()); + serializer.serializeString(this.parameters); + if (this.payload == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(false); + serializer.serializeUint8Array(this.payload); + } + if (this.encoding == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(false); + this.encoding.serializeWithZSerializer(serializer); + } + if (this.attachment == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeUint8Array(this.attachment); + } + serializer.serializeNumberUint8(this.qos.toUint8()); + serializer.serializeNumberUint8(this.querySettings.toUint8()); + serializer.serializeNumberUint32(this.timeout_ms); + } +} + +class QuerierGet { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.QuerierGet; + public constructor( + public readonly querierId: number, + public readonly id: number, + public readonly parameters: string, + public readonly payload: Uint8Array | undefined, + public readonly encoding: Encoding | undefined, + public readonly attachment: Uint8Array | undefined, + public readonly timeout_ms: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint32(this.querierId); + serializer.serializeNumberUint32(this.id); + serializer.serializeString(this.parameters); + if (this.payload == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(false); + serializer.serializeUint8Array(this.payload); + } + if (this.encoding == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(false); + this.encoding.serializeWithZSerializer(serializer); + } + if (this.attachment == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeUint8Array(this.attachment); + } + serializer.serializeNumberUint32(this.timeout_ms); + } +} + +class LivelinessGet { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.LivelinessGet; + public constructor( + public readonly id: number, + public readonly keyexpr: KeyExpr, + public readonly timeout_ms: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint32(this.id); + serializer.serializeString(this.keyexpr.toString()); + serializer.serializeNumberUint32(this.timeout_ms); + } +} + +class ReplyOk { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.ReplyOk; + public constructor( + public readonly queryId: number, + public readonly keyexpr: KeyExpr, + public readonly payload: Uint8Array, + public readonly encoding: Encoding, + public readonly attachment: Uint8Array | undefined, + public readonly timestamp: Timestamp | undefined, + public readonly qos: Qos + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.queryId); + serializer.serializeString(this.keyexpr.toString()); + serializer.serializeUint8Array(this.payload); + this.encoding.serializeWithZSerializer(serializer); + if (this.attachment == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeUint8Array(this.attachment); + } + if (this.timestamp == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + this.timestamp.serializeWithZSerializer(serializer); + } + serializer.serializeNumberUint8(this.qos.toUint8()); + } +} + +class ReplyDel { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.ReplyDel; + public constructor( + public readonly queryId: number, + public readonly keyexpr: KeyExpr, + public readonly attachment: Uint8Array | undefined, + public readonly timestamp: Timestamp | undefined, + public readonly qos: Qos + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.queryId); + serializer.serializeString(this.keyexpr.toString()); + if (this.attachment == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeUint8Array(this.attachment); + } + if (this.timestamp == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + this.timestamp.serializeWithZSerializer(serializer); + } + serializer.serializeNumberUint8(this.qos.toUint8()); + } +} + +class ReplyErr { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.ReplyErr; + public constructor( + public readonly queryId: number, + public readonly payload: Uint8Array, + public readonly encoding: Encoding, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.queryId); + serializer.serializeUint8Array(this.payload); + this.encoding.serializeWithZSerializer(serializer); + } +} + +class QueryResponseFinal { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.QueryResponseFinal; + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.QueryResponseFinal; + + public constructor( + public readonly queryId: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.queryId); + } + + public static deserialize(deserializer: ZBytesDeserializer): QueryResponseFinal { + let queryId = deserializer.deserializeNumberUint8(); + return new QueryResponseFinal(queryId); + } +} + +const enum InRemoteMessageId { + OpenAck = 0, + Ok, + Error, + ResponseTimestamp, + ResponseSessionInfo, + InSample, + InQuery, + InReply, + QueryResponseFinal, +} + +class OpenAck { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.OpenAck; + + public constructor( + public readonly uuid: string, + ) {} + + public static deserialize(deserializer: ZBytesDeserializer): OpenAck { + let uuid = deserializer.deserializeString(); + return new OpenAck(uuid); + } +} + +class Ok { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.Ok; + + public constructor( + public readonly contentId: OutRemoteMessageId, + ) {} + + public static deserialize(deserializer: ZBytesDeserializer): Ok { + let contentId = deserializer.deserializeNumberUint8(); + return new Ok(contentId); + } +} + +class Error { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.Error; + + public constructor( + public readonly contentId: OutRemoteMessageId, + public readonly error: string + ) {} + + public static deserialize(deserializer: ZBytesDeserializer): Error { + let contentId = deserializer.deserializeNumberUint8(); + let error = deserializer.deserializeString(); + return new Error(contentId, error); + } +} + +class ResponseTimestamp { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponseTimestamp; + + public constructor( + public readonly timestamp: Timestamp, + ) {} + + public deserialize(deserializer: ZBytesDeserializer): ResponseTimestamp { + return new ResponseTimestamp(Timestamp.deserialize(deserializer)); + } +} + +class ResponseSessionInfo { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponseSessionInfo; + + public constructor( + public readonly zid: Zid, + public readonly routers: Zid[], + public readonly peers: Zid[], + ) {} + + public deserialize(deserializer: ZBytesDeserializer): ResponseSessionInfo { + let zid = Zid.deserialize(deserializer); + let dt = ZD.array(ZD.objectStatic(Zid.deserialize)); + let routers = deserializer.deserialize(dt); + let peers = deserializer.deserialize(dt); + return new ResponseSessionInfo(zid, routers, peers); + } +} + + + +class InSample { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.InSample; + + public constructor( + public readonly subscriberId: number, + public readonly sample: Sample, + ) {} + + public deserialize(deserializer: ZBytesDeserializer): InSample { + let subscriberId = deserializer.deserializeNumberUint32(); + let sample = Sample.deserialize(deserializer); + return new InSample(subscriberId, sample); + } +} + +class InQuery { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.InQuery; + + public constructor( + public readonly queryableId: number, + public readonly query: Query, + ) {} + + public deserialize(deserializer: ZBytesDeserializer): InSample { + let queryableId = deserializer.deserializeNumberUint32(); + let query = Query.deserialize(deserializer); + return new InQuery(queryableId, query); + } +} \ No newline at end of file diff --git a/zenoh-ts/src/remote_api/interface/types.ts b/zenoh-ts/src/remote_api/interface/types.ts new file mode 100644 index 00000000..bc4ebfa3 --- /dev/null +++ b/zenoh-ts/src/remote_api/interface/types.ts @@ -0,0 +1,19 @@ +// +// Copyright (c) 2025 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +class EncodingBase { + constructor(protected readonly id: number, protected readonly schema: String | undefined) {} + + +} \ No newline at end of file diff --git a/zenoh-ts/src/sample.ts b/zenoh-ts/src/sample.ts index d8cc6f0a..0199fb0c 100644 --- a/zenoh-ts/src/sample.ts +++ b/zenoh-ts/src/sample.ts @@ -12,331 +12,38 @@ // ZettaScale Zenoh Team, // -// Internal -import { KeyExpr } from "./key_expr.js"; -import { OwnedKeyExprWrapper } from "./remote_api/interface/OwnedKeyExprWrapper.js"; -import { SampleKindWS } from "./remote_api/interface/SampleKindWS.js"; -import { SampleWS } from "./remote_api/interface/SampleWS.js"; -import { ZBytes } from "./z_bytes.js"; -import { Encoding } from "./encoding.js"; -// External -import { encode as b64_str_from_bytes, decode as b64_bytes_from_str, } from "base64-arraybuffer"; +import { KeyExpr } from "./key_expr"; +import { deserializeOptZBytes, ZBytes } from "./z_bytes"; +import { Encoding } from "./encoding"; +import { CongestionControl, Priority, Qos, SampleKind, sampleKindFromUint8 } from "./enums"; +import { deserializeOptTimestamp, Timestamp } from "./timestamp"; +import { ZBytesDeserializer } from "./ext"; -/** - * Kinds of Samples that can be received from Zenoh - * @enum - * @default PUT - */ -export enum SampleKind { - PUT = "PUT", - DELETE = "DELETE", -} - -/** - * Congestion Control enum - * @enum - * @default CongestionControl.DROP - */ -export enum CongestionControl { - DROP = "DROP", - BLOCK = "BLOCK", -} - -/** - * The kind of consolidation to apply to a query. - * @enum - * @default ConsolidationMode.Auto - */ -export enum ConsolidationMode { - Auto, - /** - * No consolidation applied: multiple samples may be received for the same key-timestamp. - */ - None, - - Monotonic, - Latest, -} - -/** - * Convenience function to convert between Congestion function and int - * @internal - */ -export function consolidationModeToInt( - congestionControl?: ConsolidationMode, -): number { - switch (congestionControl) { - case ConsolidationMode.Auto: - return 0 - case ConsolidationMode.None: - return 1 - case ConsolidationMode.Monotonic: - return 2 - case ConsolidationMode.Latest: - return 3 - default: - return 0; - } -} - -/** - * Convenience function to convert between Congestion function and int - * @internal - */ -export function congestionControlFromInt( - prioU8?: number, -): CongestionControl { - switch (prioU8) { - case 0: - return CongestionControl.DROP; - case 1: - return CongestionControl.BLOCK; - default: - return CongestionControl.DROP; - } -} - -/** - * Convenience function to convert between Congestion function and int - * @internal - */ -export function congestionControlToInt( - congestionControl?: CongestionControl, -): number { - switch (congestionControl) { - case CongestionControl.DROP: - return 0; - case CongestionControl.BLOCK: - return 1; - // Default is Drop - default: - return 0; - } -} - -/** - * Priority enum - * @default Priority.Data - */ -export enum Priority { - REAL_TIME = "REAL_TIME", - INTERACTIVE_HIGH = "INTERACTIVE_HIGH", - INTERACTIVE_LOW = "INTERACTIVE_LOW", - DATA_HIGH = "DATA_HIGH", - DATA = "DATA", - DATA_LOW = "DATA_LOW", - BACKGROUND = "BACKGROUND", -} - -/** - * Convenience function to convert between Priority function and int - * @internal - */ -export function priorityFromInt(prioU8: number): Priority { - switch (prioU8) { - case 1: - return Priority.REAL_TIME; - case 2: - return Priority.INTERACTIVE_HIGH; - case 3: - return Priority.INTERACTIVE_LOW; - case 4: - return Priority.DATA_HIGH; - case 5: - return Priority.DATA; - case 6: - return Priority.DATA_LOW; - case 7: - return Priority.BACKGROUND; - default: - console.warn("Unknown Priority Variant, default to Data"); - return Priority.DATA; - } -} - -/** - * Convenience function to convert between Priority function and int - * @internal - */ -export function priorityToInt(prio?: Priority): number { - switch (prio) { - case Priority.REAL_TIME: - return 1; - case Priority.INTERACTIVE_HIGH: - return 2; - case Priority.INTERACTIVE_LOW: - return 3; - case Priority.DATA_HIGH: - return 4; - case Priority.DATA: - return 5; - case Priority.DATA_LOW: - return 6; - case Priority.BACKGROUND: - return 7; - default: - // Default is Priority.DATA - return 5; - } -} - -/** - * Reliability Enum - * @default Reliability.RELIABLE - */ -export enum Reliability { - RELIABLE = "RELIABLE", - BEST_EFFORT = "BEST_EFFORT", -} - -/** - * @internal - */ -export function reliabilityToInt(reliability: Reliability) { - switch (reliability) { - case Reliability.RELIABLE: - return 0 - case Reliability.BEST_EFFORT: - return 1 - default: - return 0; - } -} - -/** - * Sample class receieved from Subscriber - * - */ export class Sample { - constructor( - public readonly keyexpr_: KeyExpr, - public readonly payload_: ZBytes, - public readonly kind_: SampleKind, - public readonly encoding_: Encoding, - public readonly priority_: Priority, - public readonly timestamp_: string | undefined, - public readonly congestionControl_: CongestionControl, - public readonly express_: boolean, - public readonly attachment_: ZBytes | undefined, - ) {} - - keyexpr(): KeyExpr { - return this.keyexpr_; - } - payload(): ZBytes { - return this.payload_; - } - kind(): SampleKind { - return this.kind_; - } - encoding(): Encoding { - return this.encoding_; - } - timestamp(): string | undefined { - return this.timestamp_; - } - congestionControl(): CongestionControl { - return this.congestionControl_; - } - priority(): Priority { - return this.priority_; - } - express(): boolean { - return this.express_; - } - attachment(): ZBytes | undefined { - return this.attachment_; - } -} - -/** - * Convenience function to convert between Sample and SampleWS - */ -export function sampleFromSampleWS(sampleWS: SampleWS) { - let sampleKind: SampleKind; - if (sampleWS.kind == "Delete") { - sampleKind = SampleKind.DELETE; - } else { - sampleKind = SampleKind.PUT; - } - - let payload = new ZBytes(new Uint8Array(b64_bytes_from_str(sampleWS.value))); - - let keyExr = new KeyExpr(sampleWS.key_expr); - - let encoding = Encoding.fromString(sampleWS.encoding); - - let priority = priorityFromInt(sampleWS.priority); - - let congestionControl = congestionControlFromInt( - sampleWS.congestion_control, - ); - - let timestamp: string | undefined = sampleWS.timestamp as string | undefined; - - let express: boolean = sampleWS.express; - - let attachment = undefined; - if (sampleWS.attachement != undefined) { - attachment = new ZBytes(new Uint8Array(b64_bytes_from_str(sampleWS.attachement))); - } - - return new Sample( - keyExr, - payload, - sampleKind, - encoding, - priority, - timestamp, - congestionControl, - express, - attachment, - ); -} - -/** - * Convenience function to convert between SampleWS and Sample - */ -export function sampleWSFromSample( - sample: Sample, - encoding: Encoding, - priority: Priority, - congestionControl: CongestionControl, - express: boolean, - attachement: ZBytes | undefined, -): SampleWS { - let keyExpr: OwnedKeyExprWrapper = sample.keyexpr().toString(); - let value: Array = Array.from(sample.payload().toBytes()); - - let sampleKind: SampleKindWS; - if (sample.kind() == SampleKind.DELETE) { - sampleKind = "Delete"; - } else if (sample.kind() == SampleKind.PUT) { - sampleKind = "Put"; - } else { - console.warn( - "Sample Kind not PUT | DELETE, defaulting to PUT: ", - sample.kind(), - ); - sampleKind = "Put"; - } - - let attach = null; - if (attachement != null) { - attach = b64_str_from_bytes(new Uint8Array(attachement.toBytes())); - } - - let sampleWS: SampleWS = { - key_expr: keyExpr, - value: b64_str_from_bytes(new Uint8Array(value)), - kind: sampleKind, - encoding: encoding.toString(), - timestamp: null, - priority: priorityToInt(priority), - congestion_control: congestionControlToInt(congestionControl), - express: express, - attachement: attach, - }; - - return sampleWS; + constructor( + public readonly keyexpr: KeyExpr, + public readonly payload: ZBytes, + public readonly kind: SampleKind, + public readonly encoding: Encoding, + public readonly attachment: ZBytes | undefined, + public readonly timestamp: Timestamp | undefined, + public readonly priority: Priority, + public readonly congestionControl: CongestionControl, + public readonly express: boolean, + ) { } + + public static deserialize(deserializer: ZBytesDeserializer) { + let keyexpr = new KeyExpr(deserializer.deserializeString()); + let payload = new ZBytes(deserializer.deserializeUint8Array()); + let kind = sampleKindFromUint8(deserializer.deserializeNumberUint8()); + let encoding = Encoding.deserialize(deserializer); + let attachment = deserializeOptZBytes(deserializer); + let timestamp = deserializeOptTimestamp(deserializer); + let qos = Qos.fromUint8(deserializer.deserializeNumberUint8()); + + return new Sample( + keyexpr, payload, kind, encoding, attachment, timestamp, + qos.priority, qos.congestion_control, qos.express + ); + } } diff --git a/zenoh-ts/src/timestamp.ts b/zenoh-ts/src/timestamp.ts index 7672a0df..b58e8910 100644 --- a/zenoh-ts/src/timestamp.ts +++ b/zenoh-ts/src/timestamp.ts @@ -1,27 +1,38 @@ -import { UUIDv4 } from "./remote_api/session"; +import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; +import { Zid } from "./zid"; export class Timestamp { - constructor(private timestampId: UUIDv4, private stringRep: string, private msSinceUnixEpoch: bigint) {} + private constructor(private readonly zid: Zid, private readonly ntp64: bigint) {} - // Note: Developers Should not need to use this - getResourceUuid(): UUIDv4 { - return this.timestampId; + getId(): Zid { + return this.zid; } - getId(): string { - return this.stringRep.split("/")[1] as string; + getMsSinceUnixEpoch(): bigint { + return this.ntp64; } - getTime(): string { - return this.stringRep.split("/")[0] as string; + asDate(): Date { + // Note: Values produced by this Bigint should fit into a number as they are ms since Unix Epoch + return new Date(this.ntp64 as unknown as number); } - getMsSinceUnixEpoch(): bigint { - return this.msSinceUnixEpoch; + serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeBigintUint64(this.ntp64); + this.zid.serializeWithZSerializer(serializer); } - asDate(): Date { - // Note: Values produced by this Bigint should fit into a number as they are ms since Unix Epoch - return new Date(this.msSinceUnixEpoch as unknown as number); + static deserialize(deserializer: ZBytesDeserializer): Timestamp { + let ntp64 = deserializer.deserializeBigintUint64(); + let zid = Zid.deserialize(deserializer); + return new Timestamp(zid, ntp64); + } +} + +export function deserializeOptTimestamp(deserializer: ZBytesDeserializer): Timestamp | undefined { + if (deserializer.deserializeBoolean()) { + return Timestamp.deserialize(deserializer); + } else { + return undefined; } -} \ No newline at end of file + } \ No newline at end of file diff --git a/zenoh-ts/src/z_bytes.ts b/zenoh-ts/src/z_bytes.ts index 68b1ed25..002edc7c 100644 --- a/zenoh-ts/src/z_bytes.ts +++ b/zenoh-ts/src/z_bytes.ts @@ -12,6 +12,8 @@ // ZettaScale Zenoh Team, // +import { ZBytesDeserializer } from "./ext"; + /** * Union Type to convert various primitives and containers into ZBytes */ @@ -95,3 +97,10 @@ export class ZBytes { +export function deserializeOptZBytes(deserializer: ZBytesDeserializer): ZBytes | undefined { + if (deserializer.deserializeBoolean()) { + return new ZBytes(deserializer.deserializeUint8Array()); + } else { + return undefined; + } +} \ No newline at end of file diff --git a/zenoh-ts/src/zid.ts b/zenoh-ts/src/zid.ts new file mode 100644 index 00000000..ee88f72a --- /dev/null +++ b/zenoh-ts/src/zid.ts @@ -0,0 +1,41 @@ +// +// Copyright (c) 2024 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; + +export class Zid { + private static KEY = '0123456789abcdef'; + private constructor(private readonly zid: Uint8Array) { + if (zid.length != 16 ) { + throw new Error("Zid should contain exactly 16 bytes"); + } + } + + toString() { + let out: string = ""; + for (let b of this.zid) { + out += Zid.KEY[b >> 4]; + out += Zid.KEY[b & 15]; + } + return out; + } + + static deserialize(deserializer: ZBytesDeserializer): Zid { + return new Zid(deserializer.deserializeUint8Array()); + } + + serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeUint8Array(this.zid); + } +} \ No newline at end of file From f1769dff01897437ee67885ab60374bdca35823b Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 23 May 2025 12:57:18 +0200 Subject: [PATCH 05/23] wip ts --- zenoh-plugin-remote-api/src/interface/mod.rs | 2 +- zenoh-ts/src/encoding.ts | 41 +- zenoh-ts/src/enums.ts | 142 ------ zenoh-ts/src/index.ts | 3 +- zenoh-ts/src/message.ts | 487 +++++++++++++------ zenoh-ts/src/query.ts | 31 +- zenoh-ts/src/sample.ts | 67 ++- zenoh-ts/src/timestamp.ts | 28 +- zenoh-ts/src/z_bytes.ts | 13 - zenoh-ts/src/zid.ts | 18 +- 10 files changed, 421 insertions(+), 411 deletions(-) diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index 6499ba94..a0de1413 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -674,8 +674,8 @@ fn serialize_reply(serializer: &mut ZSerializer, reply: &zenoh::query::Reply) { } Err(e) => { serializer.serialize(false); - serializer.serialize(encoding_to_id_schema(&e.encoding())); serializer.serialize(&e.payload().to_bytes()); + serializer.serialize(encoding_to_id_schema(&e.encoding())); } } } diff --git a/zenoh-ts/src/encoding.ts b/zenoh-ts/src/encoding.ts index 914b6261..9af8cfd6 100644 --- a/zenoh-ts/src/encoding.ts +++ b/zenoh-ts/src/encoding.ts @@ -12,9 +12,7 @@ // ZettaScale Zenoh Team, // -import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; - -enum EncodingPredefined { +export enum EncodingPredefined { ZENOH_BYTES = 0, ZENOH_STRING, ZENOH_SERIALIZED, @@ -104,7 +102,7 @@ export class Encoding { private static readonly SEP = ";"; - private constructor(private id?: EncodingPredefined, private schema?: string) {} + constructor(private id?: EncodingPredefined, private schema?: string) {} withSchema(input: string): Encoding { return new Encoding(this.id, input); @@ -146,6 +144,10 @@ export class Encoding { } } + toIdSchema(): [EncodingPredefined?, string?] { + return [this.id, this.schema]; + } + // Enum Variants /** * Constant alias for string "zenoh/bytes" @@ -405,35 +407,4 @@ export class Encoding { * Constant alias for string "video/vp9" */ static readonly VIDEO_VP9: Encoding = new Encoding(EncodingPredefined.VIDEO_VP9); - - public serializeWithZSerializer(serializer: ZBytesSerializer) { - // TODO: add id for specifying custom encoding - serializer.serializeNumberUint16(this.id ?? EncodingPredefined.ZENOH_BYTES); - if (this.schema == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeString(this.schema); - } - } - - public static deserialize(deserializer: ZBytesDeserializer): Encoding { - // TODO: add id for specifying custom encoding - let id = deserializer.deserializeNumberUint16(); - let schema: string | undefined; - if (deserializer.deserializeBoolean()) { - schema = deserializer.deserializeString(); - } else { - schema = undefined; - } - return new Encoding(id, schema); - } -} - -export function deserializeOptEncoding(deserializer: ZBytesDeserializer): Encoding | undefined { - if (deserializer.deserializeBoolean()) { - return Encoding.deserialize(deserializer); - } else { - return undefined; - } } diff --git a/zenoh-ts/src/enums.ts b/zenoh-ts/src/enums.ts index 6b78f5c6..d65b1714 100644 --- a/zenoh-ts/src/enums.ts +++ b/zenoh-ts/src/enums.ts @@ -55,92 +55,6 @@ export const enum SampleKind { Delete = 1 } -export function sampleKindFromUint8(val: number): SampleKind { - switch (val) { - case SampleKind.Put: return SampleKind.Put; - case SampleKind.Delete: return SampleKind.Delete; - default: { - console.warn(`Unsupported SampleKind value ${val}`); - return SampleKind.Put; - } - } -} - -function priorityFromUint8(val: number): Priority { - switch (val) { - case Priority.REAL_TIME: return Priority.REAL_TIME; - case Priority.INTERACTIVE_HIGH: return Priority.INTERACTIVE_HIGH; - case Priority.INTERACTIVE_LOW: return Priority.INTERACTIVE_LOW; - case Priority.DATA_HIGH: return Priority.DATA_HIGH; - case Priority.DATA: return Priority.DATA; - case Priority.DATA_LOW: return Priority.DATA_LOW; - case Priority.BACKGROUND: return Priority.BACKGROUND; - default: { - console.warn(`Unsupported Priority value ${val}`); - return Priority.DEFAULT; - } - } -} - -function congestionControlFromUint8(val: number): CongestionControl { - switch (val) { - case CongestionControl.DROP: return CongestionControl.DROP; - case CongestionControl.BLOCK: return CongestionControl.BLOCK; - default: { - console.warn(`Unsupported CongestionControl value ${val}`); - return CongestionControl.DEFAULT_PUSH; - } - } -} - -function reliabilityFromUint8(val: number): Reliability { - switch (val) { - case Reliability.BEST_EFFORT: return Reliability.BEST_EFFORT; - case Reliability.RELIABLE: return Reliability.RELIABLE; - default: { - console.warn(`Unsupported Reliability value ${val}`); - return Reliability.DEFAULT; - } - } -} - -function localityFromUint8(val: number): Locality { - switch (val) { - case Locality.SESSION_LOCAL: return Locality.SESSION_LOCAL; - case Locality.REMOTE: return Locality.REMOTE; - case Locality.ANY: return Locality.ANY; - default: { - console.warn(`Unsupported Locality value ${val}`); - return Locality.DEFAULT; - } - } -} - -export class Qos { - constructor( - public readonly priority: Priority, - public readonly congestion_control: CongestionControl, - public readonly express: boolean, - public readonly reliability: Reliability, - public readonly locality: Locality - ) {} - - public toUint8(): number { - // llrecppp - let e = this.express ? 1 : 0; - return this.priority | (this.congestion_control << 3) | (e << 4) | (this.reliability << 5) | (this.locality << 6); - } - - public static fromUint8(val: number): Qos { - let p = val & 0b111; - let c = (val >> 3) & 0b1; - let e = (val >> 4) & 0b1; - let r = (val >> 5) & 0b1; - let l = (val >> 6) & 0b11; - return new Qos(priorityFromUint8(p), congestionControlFromUint8(c), e != 0, reliabilityFromUint8(r), localityFromUint8(l)); - } -} - // The `zenoh::queryable::Queryable`s that should be target of a `zenoh::Session::get()`. export const enum QueryTarget { // Let Zenoh find the BestMatching queryable capabale of serving the query. @@ -178,60 +92,4 @@ export const enum ReplyKeyExpr { // // Accept replies whose key expressions match the query key expression. MATCHING_QUERY = 1, DEFAULT = MATCHING_QUERY -} - -function queryTargetFromUint8(val: number): QueryTarget { - switch (val) { - case QueryTarget.BEST_MATCHING: return QueryTarget.BEST_MATCHING; - case QueryTarget.ALL: return QueryTarget.ALL; - case QueryTarget.ALL_COMPLETE: return QueryTarget.ALL_COMPLETE; - default: { - console.warn(`Unsupported QueryTarget value ${val}`); - return QueryTarget.DEFAULT; - } - } -} - -function consolidationModeFromUint8(val: number): ConsolidationMode { - switch (val) { - case ConsolidationMode.AUTO: return ConsolidationMode.AUTO; - case ConsolidationMode.NONE: return ConsolidationMode.NONE; - case ConsolidationMode.MONOTONIC: return ConsolidationMode.MONOTONIC; - case ConsolidationMode.LATEST: return ConsolidationMode.LATEST; - default: { - console.warn(`Unsupported ConsolidationMode value ${val}`); - return ConsolidationMode.DEFAULT; - } - } -} - -function replyKeyExprFromUint8(val: number): ReplyKeyExpr { - switch (val) { - case ReplyKeyExpr.ANY: return ReplyKeyExpr.ANY; - case ReplyKeyExpr.MATCHING_QUERY: return ReplyKeyExpr.MATCHING_QUERY; - default: { - console.warn(`Unsupported ReplyKeyExpr value ${val}`); - return ReplyKeyExpr.DEFAULT; - } - } -} - -export class QuerySettings { - constructor( - public readonly target: QueryTarget, - public readonly consolidation: ConsolidationMode, - public readonly replyKeyExpr: ReplyKeyExpr, - ) {} - - public toUint8(): number { - // rcctt - return this.target | (this.consolidation << 2) | (this.replyKeyExpr << 4); - } - - public static fromUint8(val: number): QuerySettings { - let t = val & 0b11; - let c = (val >> 2) & 0b11; - let r = (val >> 4) & 0b1; - return new QuerySettings(queryTargetFromUint8(t), consolidationModeFromUint8(c), replyKeyExprFromUint8(r)); - } } \ No newline at end of file diff --git a/zenoh-ts/src/index.ts b/zenoh-ts/src/index.ts index 99ad1f9d..ddd49102 100644 --- a/zenoh-ts/src/index.ts +++ b/zenoh-ts/src/index.ts @@ -15,7 +15,8 @@ // API Layer Files import { KeyExpr, IntoKeyExpr } from "./key_expr.js"; import { ZBytes, IntoZBytes } from "./z_bytes.js"; -import { CongestionControl, ConsolidationMode, Priority, Reliability, Sample, SampleKind } from "./sample.js"; +import { CongestionControl, ConsolidationMode, Priority, Reliability, SampleKind } from "./enums.js"; +import { Sample } from "./sample.js"; import { Publisher, Subscriber } from "./pubsub.js"; import { IntoSelector, Parameters, IntoParameters, Query, Queryable, Reply, ReplyError, Selector } from "./query.js"; import { Session, RecvErr, DeleteOptions, PutOptions, GetOptions, QueryableOptions, PublisherOptions, ZenohId, SessionInfo } from "./session.js"; diff --git a/zenoh-ts/src/message.ts b/zenoh-ts/src/message.ts index 769824be..9346c9c0 100644 --- a/zenoh-ts/src/message.ts +++ b/zenoh-ts/src/message.ts @@ -13,14 +13,296 @@ // import { ZBytesDeserializer, ZBytesSerializer, ZD } from "./ext" -import { Encoding } from "./encoding.js"; +import { Encoding, EncodingPredefined } from "./encoding.js"; import { KeyExpr } from "./key_expr.js"; -import { Qos, Locality, QuerySettings } from "./enums.js"; +import { Locality, Reliability, CongestionControl, Priority, SampleKind, ConsolidationMode, ReplyKeyExpr, QueryTarget } from "./enums.js"; import { Timestamp } from "./timestamp"; -import { Zid } from "./zid"; +import { ZenohId } from "./zid"; import { Sample } from "./sample"; +import { Parameters, QueryInner, Reply, ReplyError } from "./query"; import { ZBytes } from "./z_bytes"; -import { Query } from "./query"; + +function sampleKindFromUint8(val: number): SampleKind { + switch (val) { + case SampleKind.Put: return SampleKind.Put; + case SampleKind.Delete: return SampleKind.Delete; + default: { + console.warn(`Unsupported SampleKind value ${val}`); + return SampleKind.Put; + } + } +} + +function priorityFromUint8(val: number): Priority { + switch (val) { + case Priority.REAL_TIME: return Priority.REAL_TIME; + case Priority.INTERACTIVE_HIGH: return Priority.INTERACTIVE_HIGH; + case Priority.INTERACTIVE_LOW: return Priority.INTERACTIVE_LOW; + case Priority.DATA_HIGH: return Priority.DATA_HIGH; + case Priority.DATA: return Priority.DATA; + case Priority.DATA_LOW: return Priority.DATA_LOW; + case Priority.BACKGROUND: return Priority.BACKGROUND; + default: { + console.warn(`Unsupported Priority value ${val}`); + return Priority.DEFAULT; + } + } +} + +function congestionControlFromUint8(val: number): CongestionControl { + switch (val) { + case CongestionControl.DROP: return CongestionControl.DROP; + case CongestionControl.BLOCK: return CongestionControl.BLOCK; + default: { + console.warn(`Unsupported CongestionControl value ${val}`); + return CongestionControl.DEFAULT_PUSH; + } + } +} + +function reliabilityFromUint8(val: number): Reliability { + switch (val) { + case Reliability.BEST_EFFORT: return Reliability.BEST_EFFORT; + case Reliability.RELIABLE: return Reliability.RELIABLE; + default: { + console.warn(`Unsupported Reliability value ${val}`); + return Reliability.DEFAULT; + } + } +} + +function localityFromUint8(val: number): Locality { + switch (val) { + case Locality.SESSION_LOCAL: return Locality.SESSION_LOCAL; + case Locality.REMOTE: return Locality.REMOTE; + case Locality.ANY: return Locality.ANY; + default: { + console.warn(`Unsupported Locality value ${val}`); + return Locality.DEFAULT; + } + } +} + +export class Qos { + constructor( + public readonly priority: Priority, + public readonly congestion_control: CongestionControl, + public readonly express: boolean, + public readonly reliability: Reliability, + public readonly locality: Locality + ) {} +} + +function qosToUint8(qos: Qos): number { + // llrecppp + let e = qos.express ? 1 : 0; + return qos.priority | (qos.congestion_control << 3) | (e << 4) | (qos.reliability << 5) | (qos.locality << 6); +} + +function qosFromUint8(val: number): Qos { + let p = val & 0b111; + let c = (val >> 3) & 0b1; + let e = (val >> 4) & 0b1; + let r = (val >> 5) & 0b1; + let l = (val >> 6) & 0b11; + return new Qos(priorityFromUint8(p), congestionControlFromUint8(c), e != 0, reliabilityFromUint8(r), localityFromUint8(l)); +} + +function queryTargetFromUint8(val: number): QueryTarget { + switch (val) { + case QueryTarget.BEST_MATCHING: return QueryTarget.BEST_MATCHING; + case QueryTarget.ALL: return QueryTarget.ALL; + case QueryTarget.ALL_COMPLETE: return QueryTarget.ALL_COMPLETE; + default: { + console.warn(`Unsupported QueryTarget value ${val}`); + return QueryTarget.DEFAULT; + } + } +} + +function consolidationModeFromUint8(val: number): ConsolidationMode { + switch (val) { + case ConsolidationMode.AUTO: return ConsolidationMode.AUTO; + case ConsolidationMode.NONE: return ConsolidationMode.NONE; + case ConsolidationMode.MONOTONIC: return ConsolidationMode.MONOTONIC; + case ConsolidationMode.LATEST: return ConsolidationMode.LATEST; + default: { + console.warn(`Unsupported ConsolidationMode value ${val}`); + return ConsolidationMode.DEFAULT; + } + } +} + +function replyKeyExprFromUint8(val: number): ReplyKeyExpr { + switch (val) { + case ReplyKeyExpr.ANY: return ReplyKeyExpr.ANY; + case ReplyKeyExpr.MATCHING_QUERY: return ReplyKeyExpr.MATCHING_QUERY; + default: { + console.warn(`Unsupported ReplyKeyExpr value ${val}`); + return ReplyKeyExpr.DEFAULT; + } + } +} + +function serializeEncoding(e: Encoding, serializer: ZBytesSerializer) { + let [id, schema] = e.toIdSchema(); + // TODO: add id for specifying custom encoding + serializer.serializeNumberUint16(id ?? EncodingPredefined.ZENOH_BYTES); + if (schema == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializer.serializeString(schema); + } +} + +function serializeOptEncoding(e: Encoding | undefined, serializer: ZBytesSerializer) { + if (e == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializeEncoding(e, serializer); + } +} + +function deserializeEncoding(deserializer: ZBytesDeserializer): Encoding { + // TODO: add id for specifying custom encoding + let id = deserializer.deserializeNumberUint16(); + let schema: string | undefined; + if (deserializer.deserializeBoolean()) { + schema = deserializer.deserializeString(); + } else { + schema = undefined; + } + return new Encoding(id, schema); +} + +function deserializeOptEncoding(deserializer: ZBytesDeserializer): Encoding | undefined { + if (deserializer.deserializeBoolean()) { + return deserializeEncoding(deserializer); + } else { + return undefined; + } +} + +export class QuerySettings { + constructor( + public readonly target: QueryTarget, + public readonly consolidation: ConsolidationMode, + public readonly replyKeyExpr: ReplyKeyExpr, + ) {} +} + +function querySettingsToUint8(qs: QuerySettings): number { + // rcctt + return qs.target | (qs.consolidation << 2) | (qs.replyKeyExpr << 4); +} + +function querySettingsFromUint8(val: number): QuerySettings { + let t = val & 0b11; + let c = (val >> 2) & 0b11; + let r = (val >> 4) & 0b1; + return new QuerySettings(queryTargetFromUint8(t), consolidationModeFromUint8(c), replyKeyExprFromUint8(r)); +} + +function deserializeZenohId(deserializer: ZBytesDeserializer): ZenohId { + return new ZenohId(deserializer.deserializeUint8Array()); +} + +function serializeZid(zid: ZenohId, serializer: ZBytesSerializer) { + serializer.serializeUint8Array(zid.toLeBytes()); +} + +function serializeTimestamp(timestamp: Timestamp, serializer: ZBytesSerializer) { + serializer.serializeBigintUint64(timestamp.getMsSinceUnixEpoch()); + serializeZid(timestamp.getId(), serializer); +} + +function serializeOptTimestamp(timestamp: Timestamp | undefined, serializer: ZBytesSerializer) { + if (timestamp == undefined) { + serializer.serializeBoolean(false); + } else { + serializer.serializeBoolean(true); + serializeTimestamp(timestamp, serializer); + } +} + +function deserializeTimestamp(deserializer: ZBytesDeserializer): Timestamp { + let ntp64 = deserializer.deserializeBigintUint64(); + let zid = deserializeZenohId(deserializer); + return new Timestamp(zid, ntp64); +} + +function deserializeOptTimestamp(deserializer: ZBytesDeserializer): Timestamp | undefined { + if (deserializer.deserializeBoolean()) { + return deserializeTimestamp(deserializer); + } else { + return undefined; + } +} + +function serializeOptUint8Array(a: Uint8Array | undefined, serializer: ZBytesSerializer) { + if (a == undefined) { + serializer.serialize(false); + } else { + serializer.serialize(true); + serializer.serializeUint8Array(a); + } +} + +function deserializeOptUint8Array(deserializer: ZBytesDeserializer): Uint8Array | undefined { + if (deserializer.deserializeBoolean()) { + return deserializer.deserializeUint8Array(); + } else { + return undefined; + } +} + +function deserializeOptZBytes(deserializer: ZBytesDeserializer): ZBytes | undefined { + if (deserializer.deserializeBoolean()) { + return new ZBytes(deserializer.deserializeUint8Array()); + } else { + return undefined; + } + } + + +function deserializeSample(deserializer: ZBytesDeserializer): Sample { + let keyexpr = new KeyExpr(deserializer.deserializeString()); + let payload = new ZBytes(deserializer.deserializeUint8Array()); + let kind = sampleKindFromUint8(deserializer.deserializeNumberUint8()); + let encoding = deserializeEncoding(deserializer); + let attachment = deserializeOptZBytes(deserializer); + let timestamp = deserializeOptTimestamp(deserializer); + let qos = qosFromUint8(deserializer.deserializeNumberUint8()); + + return new Sample( + keyexpr, payload, kind, encoding, attachment, timestamp, + qos.priority, qos.congestion_control, qos.express + ); +} + +function deserializeReply(deserializer: ZBytesDeserializer): Reply { + if (deserializer.deserializeBoolean()) { + return new Reply(deserializeSample(deserializer)); + } else { + let payload = new ZBytes(deserializer.deserializeUint8Array()); + let encoding = deserializeEncoding(deserializer); + return new Reply(new ReplyError(payload, encoding)); + } +} + +function deserializeQueryInner(deserializer: ZBytesDeserializer): QueryInner { + let queryId = deserializer.deserializeNumberUint8(); + let keyexpr = new KeyExpr(deserializer.deserializeString()); + let params = new Parameters(deserializer.deserializeString()); + let payload = deserializeOptZBytes(deserializer); + let encoding = deserializeOptEncoding(deserializer); + let attachment = deserializeOptZBytes(deserializer); + let replyKeyExpr = replyKeyExprFromUint8(deserializer.deserializeNumberUint8()); + + return new QueryInner(queryId, keyexpr, params, payload, encoding, attachment, replyKeyExpr); +} const enum OutRemoteMessageId { DeclarePublisher = 0, @@ -62,8 +344,8 @@ class DeclarePublisher { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); serializer.serializeString(this.keyexpr.toString()); - this.encoding.serializeWithZSerializer(serializer); - serializer.serializeNumberUint8(this.qos.toUint8()); + serializeEncoding(this.encoding, serializer); + serializer.serializeNumberUint8(qosToUint8(this.qos)); } } @@ -145,8 +427,9 @@ class DeclareQuerier { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); serializer.serializeString(this.keyexpr.toString()); - serializer.serializeNumberUint8(this.qos.toUint8()); - serializer.serializeNumberUint8(this.qos.toUint8()); + serializer.serializeNumberUint8(qosToUint8(this.qos)); + serializer.serializeNumberUint8(querySettingsToUint8(this.querySettings)); + serializer.serializeNumberUint32(this.timeout_ms); } } @@ -229,20 +512,10 @@ class Put { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeString(this.keyexpr.toString()); serializer.serializeUint8Array(this.payload); - this.encoding.serializeWithZSerializer(serializer); - if (this.attachment == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeUint8Array(this.attachment); - } - if (this.timestamp == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - this.timestamp.serializeWithZSerializer(serializer); - } - serializer.serializeNumberUint8(this.qos.toUint8()); + serializeEncoding(this.encoding, serializer); + serializeOptUint8Array(this.attachment, serializer); + serializeOptTimestamp(this.timestamp, serializer); + serializer.serializeNumberUint8(qosToUint8(this.qos)); } } @@ -257,19 +530,9 @@ class Delete { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeString(this.keyexpr.toString()); - if (this.attachment == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeUint8Array(this.attachment); - } - if (this.timestamp == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - this.timestamp.serializeWithZSerializer(serializer); - } - serializer.serializeNumberUint8(this.qos.toUint8()); + serializeOptUint8Array(this.attachment, serializer); + serializeOptTimestamp(this.timestamp, serializer); + serializer.serializeNumberUint8(qosToUint8(this.qos)); } } @@ -286,24 +549,9 @@ class PublisherPut { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint32(this.publisherId); serializer.serializeUint8Array(this.payload); - if (this.encoding == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - this.encoding.serializeWithZSerializer(serializer); - } - if (this.attachment == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeUint8Array(this.attachment); - } - if (this.timestamp == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - this.timestamp.serializeWithZSerializer(serializer); - } + serializeOptEncoding(this.encoding, serializer); + serializeOptUint8Array(this.attachment, serializer); + serializeOptTimestamp(this.timestamp, serializer); } } @@ -317,18 +565,8 @@ class PublisherDelete { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint32(this.publisherId); - if (this.attachment == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeUint8Array(this.attachment); - } - if (this.timestamp == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - this.timestamp.serializeWithZSerializer(serializer); - } + serializeOptUint8Array(this.attachment, serializer); + serializeOptTimestamp(this.timestamp, serializer); } } @@ -350,26 +588,11 @@ class Get { serializer.serializeNumberUint32(this.id); serializer.serializeString(this.keyexpr.toString()); serializer.serializeString(this.parameters); - if (this.payload == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(false); - serializer.serializeUint8Array(this.payload); - } - if (this.encoding == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(false); - this.encoding.serializeWithZSerializer(serializer); - } - if (this.attachment == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeUint8Array(this.attachment); - } - serializer.serializeNumberUint8(this.qos.toUint8()); - serializer.serializeNumberUint8(this.querySettings.toUint8()); + serializeOptUint8Array(this.payload, serializer); + serializeOptEncoding(this.encoding, serializer); + serializeOptUint8Array(this.attachment, serializer); + serializer.serializeNumberUint8(qosToUint8(this.qos)); + serializer.serializeNumberUint8(querySettingsToUint8(this.querySettings)); serializer.serializeNumberUint32(this.timeout_ms); } } @@ -390,24 +613,9 @@ class QuerierGet { serializer.serializeNumberUint32(this.querierId); serializer.serializeNumberUint32(this.id); serializer.serializeString(this.parameters); - if (this.payload == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(false); - serializer.serializeUint8Array(this.payload); - } - if (this.encoding == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(false); - this.encoding.serializeWithZSerializer(serializer); - } - if (this.attachment == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeUint8Array(this.attachment); - } + serializeOptUint8Array(this.payload, serializer); + serializeOptEncoding(this.encoding, serializer); + serializeOptUint8Array(this.attachment, serializer); serializer.serializeNumberUint32(this.timeout_ms); } } @@ -443,20 +651,10 @@ class ReplyOk { serializer.serializeNumberUint8(this.queryId); serializer.serializeString(this.keyexpr.toString()); serializer.serializeUint8Array(this.payload); - this.encoding.serializeWithZSerializer(serializer); - if (this.attachment == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeUint8Array(this.attachment); - } - if (this.timestamp == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - this.timestamp.serializeWithZSerializer(serializer); - } - serializer.serializeNumberUint8(this.qos.toUint8()); + serializeEncoding(this.encoding, serializer); + serializeOptUint8Array(this.attachment, serializer); + serializeOptTimestamp(this.timestamp, serializer); + serializer.serializeNumberUint8(qosToUint8(this.qos)); } } @@ -473,19 +671,9 @@ class ReplyDel { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.queryId); serializer.serializeString(this.keyexpr.toString()); - if (this.attachment == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeUint8Array(this.attachment); - } - if (this.timestamp == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - this.timestamp.serializeWithZSerializer(serializer); - } - serializer.serializeNumberUint8(this.qos.toUint8()); + serializeOptUint8Array(this.attachment, serializer); + serializeOptTimestamp(this.timestamp, serializer); + serializer.serializeNumberUint8(qosToUint8(this.qos)); } } @@ -500,7 +688,7 @@ class ReplyErr { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.queryId); serializer.serializeUint8Array(this.payload); - this.encoding.serializeWithZSerializer(serializer); + serializeEncoding(this.encoding, serializer); } } @@ -583,7 +771,7 @@ class ResponseTimestamp { ) {} public deserialize(deserializer: ZBytesDeserializer): ResponseTimestamp { - return new ResponseTimestamp(Timestamp.deserialize(deserializer)); + return new ResponseTimestamp(deserializeTimestamp(deserializer)); } } @@ -591,22 +779,20 @@ class ResponseSessionInfo { public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponseSessionInfo; public constructor( - public readonly zid: Zid, - public readonly routers: Zid[], - public readonly peers: Zid[], + public readonly zid: ZenohId, + public readonly routers: ZenohId[], + public readonly peers: ZenohId[], ) {} public deserialize(deserializer: ZBytesDeserializer): ResponseSessionInfo { - let zid = Zid.deserialize(deserializer); - let dt = ZD.array(ZD.objectStatic(Zid.deserialize)); + let zid = deserializeZenohId(deserializer); + let dt = ZD.array(ZD.objectStatic(deserializeZenohId)); let routers = deserializer.deserialize(dt); let peers = deserializer.deserialize(dt); return new ResponseSessionInfo(zid, routers, peers); } } - - class InSample { public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.InSample; @@ -617,7 +803,7 @@ class InSample { public deserialize(deserializer: ZBytesDeserializer): InSample { let subscriberId = deserializer.deserializeNumberUint32(); - let sample = Sample.deserialize(deserializer); + let sample = deserializeSample(deserializer); return new InSample(subscriberId, sample); } } @@ -627,12 +813,27 @@ class InQuery { public constructor( public readonly queryableId: number, - public readonly query: Query, + public readonly query: QueryInner, ) {} - public deserialize(deserializer: ZBytesDeserializer): InSample { + public deserialize(deserializer: ZBytesDeserializer): InQuery { let queryableId = deserializer.deserializeNumberUint32(); - let query = Query.deserialize(deserializer); + let query = deserializeQueryInner(deserializer); return new InQuery(queryableId, query); } +} + +class InReply { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.InReply; + + public constructor( + public readonly queryId: number, + public readonly reply: Reply, + ) {} + + public deserialize(deserializer: ZBytesDeserializer): InReply { + let queryId = deserializer.deserializeNumberUint32(); + let reply = deserializeReply(deserializer); + return new InReply(queryId, reply); + } } \ No newline at end of file diff --git a/zenoh-ts/src/query.ts b/zenoh-ts/src/query.ts index 296eaa9f..b621a494 100644 --- a/zenoh-ts/src/query.ts +++ b/zenoh-ts/src/query.ts @@ -26,6 +26,7 @@ import { congestionControlToInt, CongestionControl, Priority, priorityToInt, Sam import { Encoding } from "./encoding.js"; import { Timestamp } from "./timestamp.js"; import { ChannelReceiver } from "./remote_api/channels.js"; +import { ReplyKeyExpr } from "./enums.js"; @@ -161,6 +162,18 @@ export interface ReplyDelOptions { attachment?: IntoZBytes } +export class QueryInner { + constructor( + public readonly queryId_: number, + public readonly keyexpr_: KeyExpr, + public readonly parameters_: Parameters, + public readonly payload_: ZBytes | undefined, + public readonly encoding_: Encoding | undefined, + public readonly attachment_: ZBytes | undefined, + public readonly replyKeyExpr_: ReplyKeyExpr, + ) {} +} + /** * Query Class to handle */ @@ -504,9 +517,6 @@ export class Parameters { * */ export class ReplyError { - private payload_: ZBytes; - private encoding_: Encoding; - /** * Payload of Error Reply * @returns ZBytes @@ -517,23 +527,16 @@ export class ReplyError { /** * Encoding of Error Reply - * @returns ZBytes + * @returns Encoding */ encoding(): Encoding { return this.encoding_; } /** - * ReplyError gets created by the reply of a `get` on a session - * + * @internal */ - constructor(replyErrWS: ReplyErrorWS) { - let payload = new ZBytes(new Uint8Array(b64_bytes_from_str(replyErrWS.payload))); - let encoding = Encoding.fromString(replyErrWS.encoding); - this.encoding_ = encoding; - this.payload_ = payload; - } - + constructor(private payload_: ZBytes, private encoding_: Encoding) {} } /** @@ -549,7 +552,7 @@ export class Reply { } /** - * @ignore + * @internal */ constructor(private result_: Sample | ReplyError) {} } diff --git a/zenoh-ts/src/sample.ts b/zenoh-ts/src/sample.ts index 0199fb0c..6fdea2a3 100644 --- a/zenoh-ts/src/sample.ts +++ b/zenoh-ts/src/sample.ts @@ -13,37 +13,52 @@ // import { KeyExpr } from "./key_expr"; -import { deserializeOptZBytes, ZBytes } from "./z_bytes"; +import { ZBytes } from "./z_bytes"; import { Encoding } from "./encoding"; -import { CongestionControl, Priority, Qos, SampleKind, sampleKindFromUint8 } from "./enums"; -import { deserializeOptTimestamp, Timestamp } from "./timestamp"; -import { ZBytesDeserializer } from "./ext"; +import { CongestionControl, Priority, SampleKind } from "./enums"; +import { Timestamp } from "./timestamp"; export class Sample { + /** + * @internal + */ constructor( - public readonly keyexpr: KeyExpr, - public readonly payload: ZBytes, - public readonly kind: SampleKind, - public readonly encoding: Encoding, - public readonly attachment: ZBytes | undefined, - public readonly timestamp: Timestamp | undefined, - public readonly priority: Priority, - public readonly congestionControl: CongestionControl, - public readonly express: boolean, + private readonly keyexpr_: KeyExpr, + private readonly payload_: ZBytes, + private readonly kind_: SampleKind, + private readonly encoding_: Encoding, + private readonly attachment_: ZBytes | undefined, + private readonly timestamp_: Timestamp | undefined, + private readonly priority_: Priority, + private readonly congestionControl_: CongestionControl, + private readonly express_: boolean, ) { } - public static deserialize(deserializer: ZBytesDeserializer) { - let keyexpr = new KeyExpr(deserializer.deserializeString()); - let payload = new ZBytes(deserializer.deserializeUint8Array()); - let kind = sampleKindFromUint8(deserializer.deserializeNumberUint8()); - let encoding = Encoding.deserialize(deserializer); - let attachment = deserializeOptZBytes(deserializer); - let timestamp = deserializeOptTimestamp(deserializer); - let qos = Qos.fromUint8(deserializer.deserializeNumberUint8()); - - return new Sample( - keyexpr, payload, kind, encoding, attachment, timestamp, - qos.priority, qos.congestion_control, qos.express - ); + keyexpr(): KeyExpr { + return this.keyexpr_; + } + payload(): ZBytes { + return this.payload_; + } + kind(): SampleKind { + return this.kind_; + } + encoding(): Encoding { + return this.encoding_; + } + timestamp(): Timestamp | undefined { + return this.timestamp_; + } + congestionControl(): CongestionControl { + return this.congestionControl_; + } + priority(): Priority { + return this.priority_; + } + express(): boolean { + return this.express_; + } + attachment(): ZBytes | undefined { + return this.attachment_; } } diff --git a/zenoh-ts/src/timestamp.ts b/zenoh-ts/src/timestamp.ts index b58e8910..b2dd744c 100644 --- a/zenoh-ts/src/timestamp.ts +++ b/zenoh-ts/src/timestamp.ts @@ -1,10 +1,9 @@ -import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; -import { Zid } from "./zid"; +import { ZenohId } from "./zid"; export class Timestamp { - private constructor(private readonly zid: Zid, private readonly ntp64: bigint) {} + constructor(private readonly zid: ZenohId, private readonly ntp64: bigint) {} - getId(): Zid { + getId(): ZenohId { return this.zid; } @@ -16,23 +15,4 @@ export class Timestamp { // Note: Values produced by this Bigint should fit into a number as they are ms since Unix Epoch return new Date(this.ntp64 as unknown as number); } - - serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeBigintUint64(this.ntp64); - this.zid.serializeWithZSerializer(serializer); - } - - static deserialize(deserializer: ZBytesDeserializer): Timestamp { - let ntp64 = deserializer.deserializeBigintUint64(); - let zid = Zid.deserialize(deserializer); - return new Timestamp(zid, ntp64); - } -} - -export function deserializeOptTimestamp(deserializer: ZBytesDeserializer): Timestamp | undefined { - if (deserializer.deserializeBoolean()) { - return Timestamp.deserialize(deserializer); - } else { - return undefined; - } - } \ No newline at end of file +} \ No newline at end of file diff --git a/zenoh-ts/src/z_bytes.ts b/zenoh-ts/src/z_bytes.ts index 002edc7c..443f97f3 100644 --- a/zenoh-ts/src/z_bytes.ts +++ b/zenoh-ts/src/z_bytes.ts @@ -12,8 +12,6 @@ // ZettaScale Zenoh Team, // -import { ZBytesDeserializer } from "./ext"; - /** * Union Type to convert various primitives and containers into ZBytes */ @@ -92,15 +90,4 @@ export class ZBytes { return decoder.decode(this.buffer_) } -} - - - - -export function deserializeOptZBytes(deserializer: ZBytesDeserializer): ZBytes | undefined { - if (deserializer.deserializeBoolean()) { - return new ZBytes(deserializer.deserializeUint8Array()); - } else { - return undefined; - } } \ No newline at end of file diff --git a/zenoh-ts/src/zid.ts b/zenoh-ts/src/zid.ts index ee88f72a..c16eda35 100644 --- a/zenoh-ts/src/zid.ts +++ b/zenoh-ts/src/zid.ts @@ -12,11 +12,9 @@ // ZettaScale Zenoh Team, // -import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; - -export class Zid { +export class ZenohId { private static KEY = '0123456789abcdef'; - private constructor(private readonly zid: Uint8Array) { + constructor(private readonly zid: Uint8Array) { if (zid.length != 16 ) { throw new Error("Zid should contain exactly 16 bytes"); } @@ -25,17 +23,13 @@ export class Zid { toString() { let out: string = ""; for (let b of this.zid) { - out += Zid.KEY[b >> 4]; - out += Zid.KEY[b & 15]; + out += ZenohId.KEY[b >> 4]; + out += ZenohId.KEY[b & 15]; } return out; } - static deserialize(deserializer: ZBytesDeserializer): Zid { - return new Zid(deserializer.deserializeUint8Array()); - } - - serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeUint8Array(this.zid); + toLeBytes() { + return this.zid; } } \ No newline at end of file From f6ff845a3e643f614da8d88f70107e32460aeaf2 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 23 May 2025 20:31:47 +0200 Subject: [PATCH 06/23] wip ts --- zenoh-plugin-remote-api/src/interface/mod.rs | 38 ++- zenoh-plugin-remote-api/src/lib.rs | 9 +- zenoh-plugin-remote-api/src/remote_state.rs | 8 +- zenoh-ts/src/message.ts | 166 ++++++++----- zenoh-ts/src/remote_api/link.ts | 7 +- zenoh-ts/src/session_inner.ts | 240 +++++++++++++++++++ 6 files changed, 389 insertions(+), 79 deletions(-) create mode 100644 zenoh-ts/src/session_inner.ts diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index a0de1413..1db62e8e 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -15,7 +15,6 @@ use std::ops::Not; use uhlc::{Timestamp, NTP64}; -use uuid::Uuid; use zenoh::{ bytes::{Encoding, ZBytes}, config::ZenohId, @@ -53,13 +52,11 @@ pub(crate) fn deserialize_option( } pub(crate) struct Error { - pub(crate) content_id: InRemoteMessageId, pub(crate) error: String, } impl Error { pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { - serializer.serialize(self.content_id as u8); serializer.serialize(&self.error); } } @@ -299,13 +296,23 @@ fn sample_kind_to_u8(k: SampleKind) -> u8 { } } -pub(crate) struct OpenAck { - pub(crate) uuid: Uuid, +pub(crate) struct Ping {} + +impl Ping { + pub(crate) fn from_wire( + _deserializer: &mut ZDeserializer, + ) -> Result { + Ok(Self {}) + } +} + +pub(crate) struct PingAck { + pub(crate) uuid: String, } -impl OpenAck { +impl PingAck { pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { - serializer.serialize(self.uuid.to_string()); + serializer.serialize(&self.uuid); } } @@ -943,10 +950,16 @@ macro_rules! remote_message { let mut serializer = ZSerializer::new(); match self { $($name::$val(x) => { - let t: $typ = $enum_name::$val.into(); - serializer.serialize(t); - if let Some(id) = sequence_id { - serializer.serialize(id); + let mut t: $typ = $enum_name::$val.into(); + match sequence_id { + Some(id) => { + t = t | 0b10000000u8; + serializer.serialize(t); + serializer.serialize(id); + }, + None => { + serializer.serialize(t); + } } x.to_wire(&mut serializer); serializer.finish().to_bytes().to_vec() @@ -986,6 +999,7 @@ remote_message! { ReplyDel, ReplyErr, QueryResponseFinal, + Ping, }, InRemoteMessageId } @@ -994,7 +1008,7 @@ remote_message! { @to_wire #[repr(u8)] pub(crate) enum OutRemoteMessage { - OpenAck, + PingAck, Ok, Error, ResponseTimestamp, diff --git a/zenoh-plugin-remote-api/src/lib.rs b/zenoh-plugin-remote-api/src/lib.rs index 8935aecb..25182d14 100644 --- a/zenoh-plugin-remote-api/src/lib.rs +++ b/zenoh-plugin-remote-api/src/lib.rs @@ -29,7 +29,7 @@ use std::{ }; use futures::{future, pin_mut, StreamExt, TryStreamExt}; -use interface::{InRemoteMessage, OpenAck, OutRemoteMessage, SequenceId}; +use interface::{InRemoteMessage, OutRemoteMessage, SequenceId}; use remote_state::RemoteState; use rustls_pemfile::{certs, private_key}; use serde::Serialize; @@ -302,6 +302,10 @@ impl AdminSpaceClient { pub(crate) fn unregister_querier(&mut self, id: u32) { self.queriers.remove(&id); } + + pub(crate) fn id(&self) -> &str { + &self.uuid + } } async fn run_admin_space_queryable(zenoh_runtime: Runtime, state_map: StateMap, config: Config) { @@ -562,7 +566,6 @@ async fn run_websocket_server( let mut remote_state = RemoteState::new(ws_ch_tx.clone(), admin_client, session); - let _ = ws_ch_tx.send((OutRemoteMessage::OpenAck(OpenAck { uuid: id }), None)); // Incoming message from Websocket let incoming_ws = tokio::task::spawn(async move { let mut non_close_messages = ws_rx.try_filter(|msg| future::ready(!msg.is_close())); @@ -617,7 +620,6 @@ async fn handle_message( // send error response if ack was requested ( OutRemoteMessage::Error(interface::Error { - content_id: header.content_id, error: error.to_string(), }), header.sequence_id, @@ -641,7 +643,6 @@ async fn handle_message( // send error response if ack was requested ( OutRemoteMessage::Error(interface::Error { - content_id: header.content_id, error: error.to_string(), }), header.sequence_id, diff --git a/zenoh-plugin-remote-api/src/remote_state.rs b/zenoh-plugin-remote-api/src/remote_state.rs index 70351324..5b9c488e 100644 --- a/zenoh-plugin-remote-api/src/remote_state.rs +++ b/zenoh-plugin-remote-api/src/remote_state.rs @@ -38,7 +38,7 @@ use zenoh_result::bail; use crate::{ interface::{ self, DeclareLivelinessSubscriber, DeclareLivelinessToken, DeclarePublisher, - DeclareQuerier, DeclareQueryable, DeclareSubscriber, Delete, Get, LivelinessGet, + DeclareQuerier, DeclareQueryable, DeclareSubscriber, Delete, Get, LivelinessGet, PingAck, PublisherDelete, PublisherPut, Put, QuerierGet, QueryResponseFinal, ReplyDel, ReplyErr, ReplyOk, ResponseSessionInfo, ResponseTimestamp, UndeclareLivelinessSubscriber, UndeclareLivelinessToken, UndeclarePublisher, UndeclareQuerier, UndeclareQueryable, @@ -54,6 +54,7 @@ use crate::{ const MAX_NUM_PENDING_QUERIES: usize = 1000; pub(crate) struct RemoteState { + id: String, tx: Sender<(OutRemoteMessage, Option)>, admin_client: Arc>, session: Session, @@ -73,7 +74,9 @@ impl RemoteState { admin_client: Arc>, session: Session, ) -> Self { + let id = admin_client.lock().unwrap().id().to_string(); Self { + id, tx, admin_client, session, @@ -787,6 +790,9 @@ impl RemoteState { self.undeclare_liveliness_subscriber(undeclare_liveliness_subscriber) .await } + InRemoteMessage::Ping(_) => Ok(Some(OutRemoteMessage::PingAck(PingAck { + uuid: self.id.clone(), + }))), } } } diff --git a/zenoh-ts/src/message.ts b/zenoh-ts/src/message.ts index 9346c9c0..a77a286b 100644 --- a/zenoh-ts/src/message.ts +++ b/zenoh-ts/src/message.ts @@ -304,7 +304,7 @@ function deserializeQueryInner(deserializer: ZBytesDeserializer): QueryInner { return new QueryInner(queryId, keyexpr, params, payload, encoding, attachment, replyKeyExpr); } -const enum OutRemoteMessageId { +export const enum OutRemoteMessageId { DeclarePublisher = 0, UndeclarePublisher, DeclareSubscriber, @@ -330,26 +330,31 @@ const enum OutRemoteMessageId { ReplyDel, ReplyErr, QueryResponseFinal, + Ping, } -class DeclarePublisher { +export type PublisherProperties = { + keyexpr: KeyExpr, + encoding: Encoding, + qos: Qos +}; + +export class DeclarePublisher { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclarePublisher; public constructor( public readonly id: number, - public readonly keyexpr: KeyExpr, - public readonly encoding: Encoding, - public readonly qos: Qos + public readonly info: PublisherProperties, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); - serializer.serializeString(this.keyexpr.toString()); - serializeEncoding(this.encoding, serializer); - serializer.serializeNumberUint8(qosToUint8(this.qos)); + serializer.serializeString(this.info.keyexpr.toString()); + serializeEncoding(this.info.encoding, serializer); + serializer.serializeNumberUint8(qosToUint8(this.info.qos)); } } -class UndeclarePublisher { +export class UndeclarePublisher { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclarePublisher; public constructor( public readonly id: number, @@ -360,22 +365,26 @@ class UndeclarePublisher { } } -class DeclareSubscriber { +export type SubscriberProperties = { + keyexpr: KeyExpr, + allowed_origin: Locality +}; + +export class DeclareSubscriber { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareSubscriber; public constructor( public readonly id: number, - public readonly keyexpr: KeyExpr, - public readonly allowed_origin: Locality + public readonly info: SubscriberProperties ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); - serializer.serializeString(this.keyexpr.toString()); - serializer.serializeNumberUint8(this.allowed_origin); + serializer.serializeString(this.info.keyexpr.toString()); + serializer.serializeNumberUint8(this.info.allowed_origin); } } -class UndeclareSubscriber { +export class UndeclareSubscriber { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareSubscriber; public constructor( public readonly id: number, @@ -386,24 +395,27 @@ class UndeclareSubscriber { } } -class DeclareQueryable { +export type QueryableProperties = { + keyexpr: KeyExpr, + complete: boolean, + allowed_origin: Locality +} +export class DeclareQueryable { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareQueryable; public constructor( public readonly id: number, - public readonly keyexpr: KeyExpr, - public readonly complete: boolean, - public readonly allowed_origin: Locality + public readonly info: QueryableProperties, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); - serializer.serializeString(this.keyexpr.toString()); - serializer.serializeBoolean(this.complete); - serializer.serializeNumberUint8(this.allowed_origin); + serializer.serializeString(this.info.keyexpr.toString()); + serializer.serializeBoolean(this.info.complete); + serializer.serializeNumberUint8(this.info.allowed_origin); } } -class UndeclareQueryable { +export class UndeclareQueryable { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareQueryable; public constructor( public readonly id: number, @@ -421,7 +433,7 @@ class DeclareQuerier { public readonly keyexpr: KeyExpr, public readonly qos: Qos, public readonly querySettings: QuerySettings, - public readonly timeout_ms: number, + public readonly timeoutMs: number, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { @@ -430,7 +442,7 @@ class DeclareQuerier { serializer.serializeNumberUint8(qosToUint8(this.qos)); serializer.serializeNumberUint8(querySettingsToUint8(this.querySettings)); - serializer.serializeNumberUint32(this.timeout_ms); + serializer.serializeNumberUint32(this.timeoutMs); } } @@ -581,7 +593,7 @@ class Get { public readonly attachment: Uint8Array | undefined, public readonly qos: Qos, public readonly querySettings: QuerySettings, - public readonly timeout_ms: number, + public readonly timeoutMs: number, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { @@ -593,7 +605,7 @@ class Get { serializeOptUint8Array(this.attachment, serializer); serializer.serializeNumberUint8(qosToUint8(this.qos)); serializer.serializeNumberUint8(querySettingsToUint8(this.querySettings)); - serializer.serializeNumberUint32(this.timeout_ms); + serializer.serializeNumberUint32(this.timeoutMs); } } @@ -606,7 +618,7 @@ class QuerierGet { public readonly payload: Uint8Array | undefined, public readonly encoding: Encoding | undefined, public readonly attachment: Uint8Array | undefined, - public readonly timeout_ms: number, + public readonly timeoutMs: number, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { @@ -616,7 +628,7 @@ class QuerierGet { serializeOptUint8Array(this.payload, serializer); serializeOptEncoding(this.encoding, serializer); serializeOptUint8Array(this.attachment, serializer); - serializer.serializeNumberUint32(this.timeout_ms); + serializer.serializeNumberUint32(this.timeoutMs); } } @@ -625,13 +637,13 @@ class LivelinessGet { public constructor( public readonly id: number, public readonly keyexpr: KeyExpr, - public readonly timeout_ms: number, + public readonly timeoutMs: number, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint32(this.id); serializer.serializeString(this.keyexpr.toString()); - serializer.serializeNumberUint32(this.timeout_ms); + serializer.serializeNumberUint32(this.timeoutMs); } } @@ -710,10 +722,19 @@ class QueryResponseFinal { } } -const enum InRemoteMessageId { - OpenAck = 0, - Ok, - Error, +export class Ping { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.Ping; + + public constructor() {} + + public serializeWithZSerializer(_serializer: ZBytesSerializer) {} +} + + +export const enum InRemoteMessageId { + ResponsePing = 0, + ResponseOk, + ResponseError, ResponseTimestamp, ResponseSessionInfo, InSample, @@ -722,60 +743,58 @@ const enum InRemoteMessageId { QueryResponseFinal, } -class OpenAck { - public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.OpenAck; +export class ResponsePing { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponsePing; public constructor( public readonly uuid: string, ) {} - public static deserialize(deserializer: ZBytesDeserializer): OpenAck { + static deserialize(deserializer: ZBytesDeserializer): ResponsePing { let uuid = deserializer.deserializeString(); - return new OpenAck(uuid); + return new ResponsePing(uuid); } } -class Ok { - public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.Ok; +export class ResponseOk { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponseOk; public constructor( public readonly contentId: OutRemoteMessageId, ) {} - public static deserialize(deserializer: ZBytesDeserializer): Ok { + static deserialize(deserializer: ZBytesDeserializer): ResponseOk { let contentId = deserializer.deserializeNumberUint8(); - return new Ok(contentId); + return new ResponseOk(contentId); } } -class Error { - public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.Error; +export class ResponseError { + public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponseError; public constructor( - public readonly contentId: OutRemoteMessageId, public readonly error: string ) {} - public static deserialize(deserializer: ZBytesDeserializer): Error { - let contentId = deserializer.deserializeNumberUint8(); + static deserialize(deserializer: ZBytesDeserializer): ResponseError { let error = deserializer.deserializeString(); - return new Error(contentId, error); + return new ResponseError(error); } } -class ResponseTimestamp { +export class ResponseTimestamp { public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponseTimestamp; public constructor( public readonly timestamp: Timestamp, ) {} - public deserialize(deserializer: ZBytesDeserializer): ResponseTimestamp { + static deserialize(deserializer: ZBytesDeserializer): ResponseTimestamp { return new ResponseTimestamp(deserializeTimestamp(deserializer)); } } -class ResponseSessionInfo { +export class ResponseSessionInfo { public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponseSessionInfo; public constructor( @@ -784,7 +803,7 @@ class ResponseSessionInfo { public readonly peers: ZenohId[], ) {} - public deserialize(deserializer: ZBytesDeserializer): ResponseSessionInfo { + static deserialize(deserializer: ZBytesDeserializer): ResponseSessionInfo { let zid = deserializeZenohId(deserializer); let dt = ZD.array(ZD.objectStatic(deserializeZenohId)); let routers = deserializer.deserialize(dt); @@ -793,7 +812,7 @@ class ResponseSessionInfo { } } -class InSample { +export class InSample { public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.InSample; public constructor( @@ -801,14 +820,14 @@ class InSample { public readonly sample: Sample, ) {} - public deserialize(deserializer: ZBytesDeserializer): InSample { + static deserialize(deserializer: ZBytesDeserializer): InSample { let subscriberId = deserializer.deserializeNumberUint32(); let sample = deserializeSample(deserializer); return new InSample(subscriberId, sample); } } -class InQuery { +export class InQuery { public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.InQuery; public constructor( @@ -816,14 +835,14 @@ class InQuery { public readonly query: QueryInner, ) {} - public deserialize(deserializer: ZBytesDeserializer): InQuery { + static deserialize(deserializer: ZBytesDeserializer): InQuery { let queryableId = deserializer.deserializeNumberUint32(); let query = deserializeQueryInner(deserializer); return new InQuery(queryableId, query); } } -class InReply { +export class InReply { public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.InReply; public constructor( @@ -831,9 +850,38 @@ class InReply { public readonly reply: Reply, ) {} - public deserialize(deserializer: ZBytesDeserializer): InReply { + static deserialize(deserializer: ZBytesDeserializer): InReply { let queryId = deserializer.deserializeNumberUint32(); let reply = deserializeReply(deserializer); return new InReply(queryId, reply); } +} + + +export interface OutMessageInterface { + readonly outMessageId: OutRemoteMessageId; + serializeWithZSerializer(serializer: ZBytesSerializer): void; +} + +const ID_PRESENCE_FLAG = 0b10000000; +const MESSAGE_ID_MASK = 0b01111111; + +export function deserializeHeader(deserializer: ZBytesDeserializer): [InRemoteMessageId, number?] { + let messageId = deserializer.deserializeNumberUint8(); + if ((messageId & ID_PRESENCE_FLAG) == 0) { + return [messageId, undefined]; + } else { + messageId = messageId & MESSAGE_ID_MASK; + const sequenceId = deserializer.deserializeNumberUint32(); + return [messageId, sequenceId]; + } +} + +export function serializeHeader(header: [OutRemoteMessageId, number?], serializer: ZBytesSerializer) { + if (header[1] == undefined) { + serializer.serializeNumberUint8(header[0]); + } else { + serializer.serializeNumberUint8(header[0] | ID_PRESENCE_FLAG); + serializer.serializeNumberUint32(header[1]); + } } \ No newline at end of file diff --git a/zenoh-ts/src/remote_api/link.ts b/zenoh-ts/src/remote_api/link.ts index 5e19cb1f..8602bb74 100644 --- a/zenoh-ts/src/remote_api/link.ts +++ b/zenoh-ts/src/remote_api/link.ts @@ -31,6 +31,7 @@ export class RemoteLink { while (retries < MAX_RETRIES) { let ws = new WebSocket(websocketEndpoint); + ws.binaryType = "arraybuffer"; ws.onerror = function (event: any) { console.warn("WebSocket error: ", event); @@ -63,13 +64,13 @@ export class RemoteLink { throw new Error(`Failed to connect to locator endpoint: ${locator} after ${MAX_RETRIES}`); } - onmessage(onmessage: (msg: any) => void) { + onmessage(onmessage: (msg: Uint8Array) => void) { this.ws.onmessage = function (event: any) { - onmessage(event.data); + onmessage(new Uint8Array(event.data)); }; } - async send(msg: string | ArrayBufferLike | Blob | ArrayBufferView) { + async send(msg: Uint8Array) { if (!this.isOk) { throw new Error("WebSocket is closed"); } diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts new file mode 100644 index 00000000..46a3f91d --- /dev/null +++ b/zenoh-ts/src/session_inner.ts @@ -0,0 +1,240 @@ +// +// Copyright (c) 2025 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +// Remote API interface + +import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; +import { DeclarePublisher, DeclareQueryable, DeclareSubscriber, deserializeHeader, InQuery, InRemoteMessageId, InReply, InSample, OutMessageInterface, Ping, PublisherProperties, QueryableProperties, ResponseError, ResponseOk, ResponsePing, serializeHeader, SubscriberProperties, UndeclarePublisher, UndeclareQueryable, UndeclareSubscriber } from "./message"; +import { Query, Reply } from "./query"; +import { Closure } from "./remote_api/closure"; +import { RemoteLink } from "./remote_api/link"; +import { Sample } from "./sample"; +import { ZBytes } from "./z_bytes"; + +class IdSource { + private static MAX: number = 1 << 31; + private current: number; + constructor() { + this.current = 0; + } + + get(): number { + const ret = this.current; + if (this.current == IdSource.MAX) { + this.current = 0; + } else { + this.current++; + } + return ret; + } +} + +type OnResponseReceivedCallback = (msg: [InRemoteMessageId, ZBytesDeserializer]) => void; + + +class SessionInner { + private static TIMEOUT_ERROR = new Error("Timeout"); + private static LINK_CLOSED_ERROR = new Error("Link is closed"); + + private link: RemoteLink; + private id: string = ""; + + private messageIdCounter: IdSource = new IdSource(); + private publisherIdCounter: IdSource = new IdSource(); + private subscriberIdCounter: IdSource = new IdSource(); + private queryableIdCounter: IdSource = new IdSource(); + private querierIdCounter: IdSource = new IdSource(); + private livelinessTokenIdCounter: IdSource = new IdSource(); + private getIdCounter: IdSource = new IdSource(); + + private subscribers: Map> = new Map>(); + private queryables: Map> = new Map>(); + private gets: Map> = new Map>(); + + private messageResponseTimeoutMs: number = 100; + + private pendingMessageResponses: Map = new Map(); + + + private constructor(link: RemoteLink) { + this.link = link; + this.link.onmessage((msg: any) => { this.onMessageReceived(msg); }); + } + + private onMessageReceived(msg: Uint8Array) { + let deserializer = new ZBytesDeserializer(new ZBytes(msg)); + let [messageId, sequenceId] = deserializeHeader(deserializer); + if (sequenceId != undefined) { // received response to one of the messages + let res = this.pendingMessageResponses.get(sequenceId); + if (res == undefined) { + console.warn(`Received unexpected response ${messageId}:${sequenceId}`) + } else { + res([messageId, deserializer]); + this.pendingMessageResponses.delete(sequenceId); + } + } else { + switch (messageId) { + case InRemoteMessageId.InQuery: { + const q = InQuery.deserialize(deserializer); + let queryable = this.queryables.get(q.queryableId); + if (queryable == undefined) { + console.warn(`Received query for inexistant queryable ${q.queryableId}`) + } else { + queryable.callback(new Query(q.query)); + } + break; + } + case InRemoteMessageId.InReply: { + const r = InReply.deserialize(deserializer); + let get = this.gets.get(r.queryId); + if (get == undefined) { + console.warn(`Received reply for inexistant query ${r.queryId}`) + } else { + get.callback(r.reply); + } + break; + } + case InRemoteMessageId.InSample: { + const s = InSample.deserialize(deserializer); + let subscriber = this.subscribers.get(s.subscriberId); + if (subscriber == undefined) { + console.warn(`Received sample for inexistant subscriber ${s.subscriberId}`) + } else { + subscriber.callback(s.sample); + } + break; + } + default: throw new Error(`Received unexpected message type ${messageId}`); + } + } + } + + private async sendMessage(msg: OutMessageInterface) { + let serializer = new ZBytesSerializer(); + serializer.serializeNumberUint8(msg.outMessageId); + msg.serializeWithZSerializer(serializer); + return await this.link.send(serializer.finish().toBytes()); + } + + private async sendRequest(msg: OutMessageInterface, expectedResponseId: InRemoteMessageId, deserialize: (deserializer: ZBytesDeserializer) => T): Promise { + let serializer = new ZBytesSerializer(); + const msgId = this.messageIdCounter.get(); + serializeHeader([msg.outMessageId, msgId], serializer); + + serializer.serializeNumberUint8(msgId); + const p = new Promise((resolve: OnResponseReceivedCallback, _) => { + this.pendingMessageResponses.set(msgId, resolve); + }); + + const timeout = new Promise<[InRemoteMessageId, ZBytesDeserializer]>((_, reject) => + setTimeout(() => reject(SessionInner.TIMEOUT_ERROR), this.messageResponseTimeoutMs), + ); + await this.link.send(serializer.finish().toBytes()); + + return await Promise.race([p, timeout]).then((r: [InRemoteMessageId, ZBytesDeserializer]) => { + switch (r[0]) { + case expectedResponseId: return deserialize(r[1]); + case InRemoteMessageId.ResponseError: { + const e = ResponseError.deserialize(r[1]); + throw new Error(e.error); + } + default: throw new Error(`Unexpected InRemoteMessageId ${r[0]}`); + }; + }, (e: Error) => { + this.pendingMessageResponses.delete(msgId); + throw e; + }); + } + + async ping(): Promise { + return await this.sendRequest(new Ping, InRemoteMessageId.ResponsePing, ResponsePing.deserialize); + } + + static async open(locator: string): Promise { + let link = await RemoteLink.new(locator); + let session = new SessionInner(link); + session.id = (await session.ping()).uuid; // verify connection + console.log(`Successfully opened session with id: ${session.id}`); + return session; + } + + async declarePublisher(info: PublisherProperties): Promise { + let publisherId = this.publisherIdCounter.get(); + await this.sendRequest( + new DeclarePublisher(publisherId, info), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + return publisherId; + } + + async undeclarePublisher(publisherId: number) { + await this.sendRequest( + new UndeclarePublisher(publisherId), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + } + + async declareSubscriber(info: SubscriberProperties, closure: Closure): Promise { + let subscriberId = this.subscriberIdCounter.get(); + await this.sendRequest( + new DeclareSubscriber(subscriberId, info), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + this.subscribers.set(subscriberId, closure); + return subscriberId; + } + + async undeclareSubscriber(subscriberId: number) { + const subscriber = this.subscribers.get(subscriberId); + if (subscriber == undefined) { + new Error (`Unknown subscriber id: ${subscriberId}`) + } else { + this.subscribers.delete(subscriberId); + subscriber.drop(); + } + await this.sendRequest( + new UndeclareSubscriber(subscriberId), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + } + + async declareQueryable(info: QueryableProperties, closure: Closure): Promise { + let queryableId = this.queryableIdCounter.get(); + await this.sendRequest( + new DeclareQueryable(queryableId, info), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + this.queryables.set(queryableId, closure); + return queryableId; + } + + async undeclareQueryable(queryableId: number) { + const queryable = this.queryables.get(queryableId); + if (queryable == undefined) { + new Error (`Unknown queryable id: ${queryableId}`) + } else { + this.queryables.delete(queryableId); + queryable.drop(); + } + await this.sendRequest( + new UndeclareQueryable(queryableId), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + } +} \ No newline at end of file From 315307a463dd4d52e89d1c927d5f8daf0350ff2d Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 26 May 2025 17:09:38 +0200 Subject: [PATCH 07/23] binary protocol ts --- zenoh-plugin-remote-api/src/interface/mod.rs | 4 +- zenoh-ts/src/{remote_api => }/channels.ts | 0 zenoh-ts/src/{remote_api => }/closure.ts | 0 zenoh-ts/src/encoding.ts | 37 +- zenoh-ts/src/enums.ts | 8 +- zenoh-ts/src/index.ts | 17 +- zenoh-ts/src/key_expr.ts | 142 +-- zenoh-ts/src/{remote_api => }/link.ts | 3 + zenoh-ts/src/liveliness.ts | 186 +-- zenoh-ts/src/message.ts | 248 ++-- zenoh-ts/src/pubsub.ts | 342 +++--- zenoh-ts/src/querier.ts | 291 ++--- zenoh-ts/src/query.ts | 1033 ++++++++--------- .../src/remote_api/interface/B64String.ts | 3 - .../src/remote_api/interface/ControlMsg.ts | 6 - zenoh-ts/src/remote_api/interface/DataMsg.ts | 8 - .../src/remote_api/interface/LivelinessMsg.ts | 4 - .../interface/OwnedKeyExprWrapper.ts | 3 - .../remote_api/interface/QueryReplyVariant.ts | 5 - .../src/remote_api/interface/QueryReplyWS.ts | 4 - zenoh-ts/src/remote_api/interface/QueryWS.ts | 5 - .../src/remote_api/interface/QueryableMsg.ts | 5 - .../src/remote_api/interface/RemoteAPIMsg.ts | 5 - .../src/remote_api/interface/ReplyErrorWS.ts | 4 - zenoh-ts/src/remote_api/interface/ReplyWS.ts | 6 - .../src/remote_api/interface/SampleKindWS.ts | 3 - zenoh-ts/src/remote_api/interface/SampleWS.ts | 7 - .../src/remote_api/interface/SessionInfo.ts | 3 - zenoh-ts/src/remote_api/interface/types.ts | 19 - zenoh-ts/src/remote_api/pubsub.ts | 142 --- zenoh-ts/src/remote_api/querier.ts | 73 -- zenoh-ts/src/remote_api/query.ts | 50 - zenoh-ts/src/remote_api/session.ts | 709 ----------- zenoh-ts/src/session.ts | 952 +++++++-------- zenoh-ts/src/session_inner.ts | 192 ++- zenoh-ts/src/z_bytes.ts | 122 +- 36 files changed, 1720 insertions(+), 2921 deletions(-) rename zenoh-ts/src/{remote_api => }/channels.ts (100%) rename zenoh-ts/src/{remote_api => }/closure.ts (100%) rename zenoh-ts/src/{remote_api => }/link.ts (97%) delete mode 100644 zenoh-ts/src/remote_api/interface/B64String.ts delete mode 100644 zenoh-ts/src/remote_api/interface/ControlMsg.ts delete mode 100644 zenoh-ts/src/remote_api/interface/DataMsg.ts delete mode 100644 zenoh-ts/src/remote_api/interface/LivelinessMsg.ts delete mode 100644 zenoh-ts/src/remote_api/interface/OwnedKeyExprWrapper.ts delete mode 100644 zenoh-ts/src/remote_api/interface/QueryReplyVariant.ts delete mode 100644 zenoh-ts/src/remote_api/interface/QueryReplyWS.ts delete mode 100644 zenoh-ts/src/remote_api/interface/QueryWS.ts delete mode 100644 zenoh-ts/src/remote_api/interface/QueryableMsg.ts delete mode 100644 zenoh-ts/src/remote_api/interface/RemoteAPIMsg.ts delete mode 100644 zenoh-ts/src/remote_api/interface/ReplyErrorWS.ts delete mode 100644 zenoh-ts/src/remote_api/interface/ReplyWS.ts delete mode 100644 zenoh-ts/src/remote_api/interface/SampleKindWS.ts delete mode 100644 zenoh-ts/src/remote_api/interface/SampleWS.ts delete mode 100644 zenoh-ts/src/remote_api/interface/SessionInfo.ts delete mode 100644 zenoh-ts/src/remote_api/interface/types.ts delete mode 100644 zenoh-ts/src/remote_api/pubsub.ts delete mode 100644 zenoh-ts/src/remote_api/querier.ts delete mode 100644 zenoh-ts/src/remote_api/query.ts delete mode 100644 zenoh-ts/src/remote_api/session.ts diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index 1db62e8e..cfc3506c 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -585,8 +585,8 @@ impl Get { } pub(crate) struct QuerierGet { - pub(crate) querier_id: u32, pub(crate) id: u32, + pub(crate) querier_id: u32, pub(crate) parameters: String, pub(crate) payload: Option>, pub(crate) encoding: Option, @@ -596,8 +596,8 @@ pub(crate) struct QuerierGet { impl QuerierGet { pub(crate) fn from_wire(deserializer: &mut ZDeserializer) -> Result { Ok(QuerierGet { - querier_id: deserializer.deserialize()?, id: deserializer.deserialize()?, + querier_id: deserializer.deserialize()?, parameters: deserializer.deserialize()?, payload: deserialize_option(deserializer)?, encoding: opt_encoding_from_id_schema(deserialize_option(deserializer)?), diff --git a/zenoh-ts/src/remote_api/channels.ts b/zenoh-ts/src/channels.ts similarity index 100% rename from zenoh-ts/src/remote_api/channels.ts rename to zenoh-ts/src/channels.ts diff --git a/zenoh-ts/src/remote_api/closure.ts b/zenoh-ts/src/closure.ts similarity index 100% rename from zenoh-ts/src/remote_api/closure.ts rename to zenoh-ts/src/closure.ts diff --git a/zenoh-ts/src/encoding.ts b/zenoh-ts/src/encoding.ts index 9af8cfd6..e80ca6e7 100644 --- a/zenoh-ts/src/encoding.ts +++ b/zenoh-ts/src/encoding.ts @@ -66,14 +66,14 @@ export enum EncodingPredefined { VIDEO_RAW, VIDEO_VP8, VIDEO_VP9, - // TODO: add id for specifying custom encoding + CUSTOM = 0xFFFF, } function createIdToEncodingMap(): Map { let out = new Map(); for (let e in EncodingPredefined) { let n = Number(e); - if (!isNaN(n)) { + if (!isNaN(n) && n != EncodingPredefined.CUSTOM) { out.set(n as EncodingPredefined, (EncodingPredefined[n] as string).toLocaleLowerCase().replaceAll('_', '/')); } } @@ -84,7 +84,7 @@ function createEncodingToIdMap(): Map { let out = new Map(); for (let e in EncodingPredefined) { let n = Number(e); - if (!isNaN(n)) { + if (!isNaN(n) && n != EncodingPredefined.CUSTOM) { out.set((EncodingPredefined[n] as string).toLocaleLowerCase().replaceAll('_', '/'), n as EncodingPredefined); } } @@ -102,7 +102,7 @@ export class Encoding { private static readonly SEP = ";"; - constructor(private id?: EncodingPredefined, private schema?: string) {} + constructor(private id: EncodingPredefined, private schema?: string) {} withSchema(input: string): Encoding { return new Encoding(this.id, input); @@ -114,11 +114,11 @@ export class Encoding { toString(): string { let out: string = ""; - if (this.id != undefined) { + if (this.id != EncodingPredefined.CUSTOM) { out += Encoding.ID_TO_ENCODING.get(this.id) as string; } if (this.schema != undefined) { - if (this.id != undefined) { + if (this.id != EncodingPredefined.CUSTOM) { out += ";"; } out += this.schema; @@ -131,20 +131,27 @@ export class Encoding { return new Encoding(EncodingPredefined.ZENOH_BYTES, undefined) } const idx = input.indexOf(Encoding.SEP); + let key: string; + let schema: string | undefined = undefined; if (idx == -1) { - return new Encoding(undefined, input); + key = input; } else { - const id = Encoding.ENCODING_TO_ID.get(input.substring(0, idx + 1)); - if (id != undefined) { - let schema = input.substring(idx + 1) - return new Encoding(id, schema.length == 0 ? undefined : schema); - } else { - return new Encoding(undefined, input); - } + key = input.substring(0, idx + 1); + schema = input.substring(idx + 1); + } + const id = Encoding.ENCODING_TO_ID.get(key) ?? EncodingPredefined.CUSTOM; + return new Encoding(id, schema); + } + + static from(input: IntoEncoding): Encoding { + if (input instanceof Encoding) { + return input; + } else { + return Encoding.fromString(input.toString()); } } - toIdSchema(): [EncodingPredefined?, string?] { + toIdSchema(): [EncodingPredefined, string?] { return [this.id, this.schema]; } diff --git a/zenoh-ts/src/enums.ts b/zenoh-ts/src/enums.ts index d65b1714..79d9a36b 100644 --- a/zenoh-ts/src/enums.ts +++ b/zenoh-ts/src/enums.ts @@ -32,9 +32,13 @@ export const enum CongestionControl { // progress. BLOCK = 1, DEFAULT_PUSH = DROP, - DEFAULT_REQUEST = BLOCK + DEFAULT_REQUEST = BLOCK, + DEFAULT_RESPONSE = BLOCK } +// The publisher reliability. +// Currently `reliability` does not trigger any data retransmission on the wire. +// It is rather used as a marker on the wire and it may be used to select the best link available (e.g. TCP for reliable data and UDP for best effort data). export const enum Reliability { BEST_EFFORT = 0, RELIABLE = 1, @@ -55,7 +59,7 @@ export const enum SampleKind { Delete = 1 } -// The `zenoh::queryable::Queryable`s that should be target of a `zenoh::Session::get()`. +// The `zenoh.queryable.Queryables that should be target of a `zenoh.Session.get()`. export const enum QueryTarget { // Let Zenoh find the BestMatching queryable capabale of serving the query. BEST_MATCHING = 0, diff --git a/zenoh-ts/src/index.ts b/zenoh-ts/src/index.ts index ddd49102..030efc56 100644 --- a/zenoh-ts/src/index.ts +++ b/zenoh-ts/src/index.ts @@ -15,16 +15,18 @@ // API Layer Files import { KeyExpr, IntoKeyExpr } from "./key_expr.js"; import { ZBytes, IntoZBytes } from "./z_bytes.js"; -import { CongestionControl, ConsolidationMode, Priority, Reliability, SampleKind } from "./enums.js"; +import { CongestionControl, ConsolidationMode, Locality, Priority, Reliability, SampleKind } from "./enums.js"; import { Sample } from "./sample.js"; +import { Timestamp } from "./timestamp.js"; +import { ZenohId } from "./zid.js"; import { Publisher, Subscriber } from "./pubsub.js"; import { IntoSelector, Parameters, IntoParameters, Query, Queryable, Reply, ReplyError, Selector } from "./query.js"; -import { Session, RecvErr, DeleteOptions, PutOptions, GetOptions, QueryableOptions, PublisherOptions, ZenohId, SessionInfo } from "./session.js"; +import { Session, DeleteOptions, PutOptions, GetOptions, QuerierOptions, QueryableOptions, PublisherOptions, SessionInfo } from "./session.js"; import { Config } from "./config.js"; import { Encoding, IntoEncoding } from "./encoding.js"; import { Liveliness, LivelinessToken } from "./liveliness.js"; -import { Querier, QueryTarget, Locality, ReplyKeyExpr, QuerierOptions, QuerierGetOptions } from './querier.js' -import { FifoChannel, RingChannel, ChannelReceiver, ChannelSender, TryReceived, TryReceivedKind, ChannelState } from "./remote_api/channels.js"; +import { Querier, QuerierGetOptions } from './querier.js' +import { FifoChannel, RingChannel, ChannelReceiver, ChannelSender, TryReceived, TryReceivedKind, ChannelState } from "./channels.js"; // Re-export duration external library import { Duration } from 'typed-duration' @@ -33,13 +35,14 @@ import { Duration } from 'typed-duration' // Exports export { KeyExpr, IntoKeyExpr }; export { ZBytes, IntoZBytes }; -export { CongestionControl, ConsolidationMode, Priority, Reliability, Sample, SampleKind }; +export { CongestionControl, ConsolidationMode, Locality, Priority, Reliability, Sample, SampleKind }; export { Publisher, Subscriber}; export { IntoSelector, Parameters, IntoParameters, Query, Queryable, Reply, ReplyError, Selector }; -export { Session, RecvErr, DeleteOptions as DeleteOpts, PutOptions, GetOptions, QueryableOptions, PublisherOptions, ZenohId, SessionInfo}; +export { Session, DeleteOptions, PutOptions, GetOptions, QuerierOptions, QueryableOptions, PublisherOptions, SessionInfo}; +export { ZenohId, Timestamp } export { Config }; export { Encoding, IntoEncoding }; export { Liveliness, LivelinessToken }; export { Duration }; -export { Querier, QueryTarget, Locality, ReplyKeyExpr, QuerierOptions, QuerierGetOptions } +export { Querier, QuerierGetOptions } export { FifoChannel, RingChannel, ChannelReceiver, ChannelSender, TryReceived, TryReceivedKind, ChannelState} \ No newline at end of file diff --git a/zenoh-ts/src/key_expr.ts b/zenoh-ts/src/key_expr.ts index c103bdb5..88da07fe 100644 --- a/zenoh-ts/src/key_expr.ts +++ b/zenoh-ts/src/key_expr.ts @@ -17,90 +17,90 @@ // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ // ██ ██ ███████ ██ ███████ ██ ██ ██ ██ ██ -import { new_key_expr, join, concat, includes, intersects, autocanonize} from "./key_expr/zenoh_keyexpr_wrapper.js" +import { new_key_expr, join, concat, includes, intersects, autocanonize } from "./key_expr/zenoh_keyexpr_wrapper.js" export type IntoKeyExpr = KeyExpr | String | string; export class KeyExpr { - /** - * Class to represent a Key Expression in Zenoh - */ + /** + * Class to represent a Key Expression in Zenoh + */ - // not to be used directly - private inner_: string; + // not to be used directly + private inner_: string; - constructor(keyexpr: IntoKeyExpr) { - let ke; - if (keyexpr instanceof KeyExpr) { - this.inner_ = keyexpr.inner_; - return - } else if (keyexpr instanceof String) { - ke = keyexpr.toString(); - } else { - ke = keyexpr; + constructor(keyexpr: IntoKeyExpr) { + let ke; + if (keyexpr instanceof KeyExpr) { + this.inner_ = keyexpr.inner_; + return + } else if (keyexpr instanceof String) { + ke = keyexpr.toString(); + } else { + ke = keyexpr; + } + // `new_key_expr` calls the `key_expr::OwnedKeyExpr::new` in Rust + // if this function fails, the keyexpr is invalid, and an exception is thrown in Wasm and propagated here + // else the Key Expression is valid and we can store the string it represents in the class + new_key_expr(ke); + this.inner_ = ke; } - // `new_key_expr` calls the `key_expr::OwnedKeyExpr::new` in Rust - // if this function fails, the keyexpr is invalid, and an exception is thrown in Wasm and propagated here - // else the Key Expression is valid and we can store the string it represents in the class - new_key_expr(ke); - this.inner_ = ke; - } - toString(): string { - return this.inner_; - } + toString(): string { + return this.inner_; + } - /** - * Joins both sides, inserting a / in between them. - * This should be your preferred method when concatenating path segments. - * @returns KeyExpr - */ - join(other: IntoKeyExpr): KeyExpr { - const keyExpr = join(this.inner_, KeyExpr.intoString(other)); - return new KeyExpr(keyExpr) - } + /** + * Joins both sides, inserting a / in between them. + * This should be your preferred method when concatenating path segments. + * @returns KeyExpr + */ + join(other: IntoKeyExpr): KeyExpr { + const keyExpr = join(this.inner_, KeyExpr.intoString(other)); + return new KeyExpr(keyExpr) + } - /** - * Performs string concatenation and returns the result as a KeyExpr if possible. - * @returns KeyExpr - */ - concat(other: IntoKeyExpr): KeyExpr { - const keyExpr = concat(this.inner_, KeyExpr.intoString(other)); - return new KeyExpr(keyExpr) - } + /** + * Performs string concatenation and returns the result as a KeyExpr if possible. + * @returns KeyExpr + */ + concat(other: IntoKeyExpr): KeyExpr { + const keyExpr = concat(this.inner_, KeyExpr.intoString(other)); + return new KeyExpr(keyExpr) + } - /** - * Returns true if this includes other, i.e. the set defined by this contains every key belonging to the set defined by other. - * @returns KeyExpr - */ - includes(other: IntoKeyExpr): boolean { - return includes(this.inner_, KeyExpr.intoString(other)) - } + /** + * Returns true if this includes other, i.e. the set defined by this contains every key belonging to the set defined by other. + * @returns KeyExpr + */ + includes(other: IntoKeyExpr): boolean { + return includes(this.inner_, KeyExpr.intoString(other)) + } - /** - * Returns true if the keyexprs intersect, i.e. there exists at least one key which is contained in both of the sets defined by self and other. - * @returns KeyExpr - */ - intersects(other: IntoKeyExpr): boolean { - return intersects(this.inner_, KeyExpr.intoString(other)) - } + /** + * Returns true if the keyexprs intersect, i.e. there exists at least one key which is contained in both of the sets defined by self and other. + * @returns KeyExpr + */ + intersects(other: IntoKeyExpr): boolean { + return intersects(this.inner_, KeyExpr.intoString(other)) + } - /** - * Returns the canon form of a key_expr - * @returns KeyExpr - */ - static autocanonize(other: IntoKeyExpr): KeyExpr { - const keyExpr = autocanonize(KeyExpr.intoString(other)); - return new KeyExpr(keyExpr) - } + /** + * Returns the canon form of a key_expr + * @returns KeyExpr + */ + static autocanonize(other: IntoKeyExpr): KeyExpr { + const keyExpr = autocanonize(KeyExpr.intoString(other)); + return new KeyExpr(keyExpr) + } - private static intoString(other: IntoKeyExpr): string { - if (other instanceof KeyExpr) { - return other.inner_; - } else if (other instanceof String) { - return other.toString(); - } else { - return other; + private static intoString(other: IntoKeyExpr): string { + if (other instanceof KeyExpr) { + return other.inner_; + } else if (other instanceof String) { + return other.toString(); + } else { + return other; + } } - } } diff --git a/zenoh-ts/src/remote_api/link.ts b/zenoh-ts/src/link.ts similarity index 97% rename from zenoh-ts/src/remote_api/link.ts rename to zenoh-ts/src/link.ts index 8602bb74..ca2bca24 100644 --- a/zenoh-ts/src/remote_api/link.ts +++ b/zenoh-ts/src/link.ts @@ -76,6 +76,9 @@ export class RemoteLink { } while (this.ws.bufferedAmount > MAX_WS_BUFFER_SIZE) { await sleep(10); + if (!this.isOk) { + throw new Error("WebSocket is closed"); + } } this.ws.send(msg); } diff --git a/zenoh-ts/src/liveliness.ts b/zenoh-ts/src/liveliness.ts index 7a4f8d36..96a65a72 100644 --- a/zenoh-ts/src/liveliness.ts +++ b/zenoh-ts/src/liveliness.ts @@ -1,116 +1,118 @@ -// -import { - RemoteSession, - UUIDv4 -} from "./remote_api/session.js"; +// +// Copyright (c) 2024 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + import { IntoKeyExpr, KeyExpr } from "./key_expr.js"; -import { Sample, sampleFromSampleWS } from "./sample.js"; -import { Reply, replyFromReplyWS } from "./query.js"; +import { Sample } from "./sample.js"; +import { Reply } from "./query.js"; -// Import interface -import { ControlMsg } from "./remote_api/interface/ControlMsg.js"; -import { SampleWS } from "./remote_api/interface/SampleWS.js"; import { Subscriber } from "./pubsub.js"; - -// Liveliness API -import { ReplyWS } from "./remote_api/interface/ReplyWS.js"; - -// External import { Duration, TimeDuration } from 'typed-duration' -import { ChannelReceiver, FifoChannel, Handler, intoCbDropReceiver } from "./remote_api/channels.js"; - +import { ChannelReceiver, FifoChannel, Handler, intoCbDropReceiver } from "./channels.js"; +import { SessionInner } from "./session_inner.js"; +import { DEFAULT_QUERY_TIMEOUT_MS } from "./session.js"; + +/** + * Options for a Liveliness Subscriber + * @prop {boolean=} history - If true, subscriber will receive the state change notifications for liveliness tokens that were declared before its declaration + * @prop {Handler=} handler - Handler for this subscriber + */ interface LivelinessSubscriberOptions { - handler?: Handler, - history: boolean, + history?: boolean, + handler?: Handler, } +/** + * Options for a Liveliness Subscriber + * @prop {TimeDuration=} timeout - This liveliness query timeout value + * @prop {Handler=} handler - Handler for this liveliness query + */ interface LivelinessGetOptions { - handler?: Handler, - timeout?: TimeDuration, + timeout?: TimeDuration, + handler?: Handler, } export class Liveliness { - constructor(private remoteSession: RemoteSession) {} - - async declareToken(intoKeyExpr: IntoKeyExpr): Promise { - let keyExpr: KeyExpr = new KeyExpr(intoKeyExpr); - let uuid = await this.remoteSession.declareLivelinessToken(keyExpr.toString()); - - return new LivelinessToken(this.remoteSession, uuid) - } + constructor(private session: SessionInner) { } - async declareSubscriber(intoKeyExpr: IntoKeyExpr, options?: LivelinessSubscriberOptions): Promise { - - let keyExpr = new KeyExpr(intoKeyExpr); - - let history = false; - if (options?.history != undefined) { - history = options?.history; - }; - - let handler = options?.handler ?? new FifoChannel(256); - let [callback, drop, receiver] = intoCbDropReceiver(handler); - - let callbackWS = (sampleWS: SampleWS): void => { - let sample: Sample = sampleFromSampleWS(sampleWS); - callback(sample); + async declareToken(intoKeyExpr: IntoKeyExpr): Promise { + let tokenId = await this.session.declareLivelinessToken(new KeyExpr(intoKeyExpr)); + return new LivelinessToken(this.session, tokenId); } - let remoteSubscriber = await this.remoteSession.declareLivelinessSubscriber(keyExpr.toString(), history, callbackWS, drop); - - let subscriber = new Subscriber( - remoteSubscriber, - keyExpr, - receiver - ); - - return subscriber; - } - - async get(intoKeyExpr: IntoKeyExpr, options?: LivelinessGetOptions): Promise| undefined> { - - let keyExpr = new KeyExpr(intoKeyExpr); - - let timeoutMillis: number | undefined = undefined; - - if (options?.timeout !== undefined) { - timeoutMillis = Duration.milliseconds.from(options?.timeout); + /** + * Declares a subscriber on liveliness tokens that intersect specified keyexpr + * + * @param {IntoKeyExpr} intoKeyExpr - The key expression to subscribe to + * @param {LivelinessSubscriberOptions=} livelinessSubscriberOpts - options for the liveliness subscriber + * + * @returns Liveliness Subscriber + */ + async declareSubscriber( + intoKeyExpr: IntoKeyExpr, livelinessSubscriberOpts?: LivelinessSubscriberOptions + ): Promise { + let handler = livelinessSubscriberOpts?.handler ?? new FifoChannel(256); + let [callback, drop, receiver] = intoCbDropReceiver(handler); + let keyexpr = new KeyExpr(intoKeyExpr); + let subscriberId = await this.session.declareLivelinessSubscriber( + { + keyexpr, + history: livelinessSubscriberOpts?.history ?? false, + }, + { callback, drop } + ); + + return new Subscriber(this.session, subscriberId, keyexpr, receiver); } - let handler = options?.handler ?? new FifoChannel(256); - let [callback, drop, receiver] = intoCbDropReceiver(handler); - - let callbackWS = (replyWS: ReplyWS): void => { - let reply: Reply = replyFromReplyWS(replyWS); - callback(reply); + /** + * Queries liveliness tokens currently on the network intersecting with specified key expression + * @param intoKeyExpr - key expression to query + * @param livelinessGetOpts - options passed to get operation + * + */ + async get(intoKeyExpr: IntoKeyExpr, livelinessGetOpts?: LivelinessGetOptions): Promise | undefined> { + let handler = livelinessGetOpts?.handler ?? new FifoChannel(256); + let [callback, drop, receiver] = intoCbDropReceiver(handler); + await this.session.livelinessGet( + { + keyexpr: new KeyExpr(intoKeyExpr), + timeoutMs: livelinessGetOpts?.timeout ? Duration.milliseconds.from(livelinessGetOpts.timeout) : DEFAULT_QUERY_TIMEOUT_MS, + }, + { callback, drop } + ); + return receiver; } - - await this.remoteSession.getLiveliness( - keyExpr.toString(), - callbackWS, - drop, - timeoutMillis, - ); - - return receiver; - } } +/** A token whose liveliness is tied to the Zenoh [`Session`](Session). + * + * A declared liveliness token will be seen as alive by any other Zenoh + * application in the system that monitors it while the liveliness token + * is not undeclared or dropped, while the Zenoh application that declared + * it is alive (didn't stop or crashed) and while the Zenoh application + * that declared the token has Zenoh connectivity with the Zenoh application + * that monitors it. + */ export class LivelinessToken { - constructor( - private remoteSession: RemoteSession, - private uuid: UUIDv4 - ) { - this.remoteSession = remoteSession; - this.uuid = uuid; - } + constructor(private session: SessionInner, private id: number) { } - async undeclare() { - let controlMsg: ControlMsg = { - Liveliness: { "UndeclareToken": this.uuid.toString() }, - }; + async undeclare() { + await this.session.undeclareLivelinessToken(this.id); + } - await this.remoteSession.sendCtrlMessage(controlMsg); - } + async [Symbol.asyncDispose]() { + await this.undeclare(); + } } \ No newline at end of file diff --git a/zenoh-ts/src/message.ts b/zenoh-ts/src/message.ts index a77a286b..4c6d52b6 100644 --- a/zenoh-ts/src/message.ts +++ b/zenoh-ts/src/message.ts @@ -21,6 +21,7 @@ import { ZenohId } from "./zid"; import { Sample } from "./sample"; import { Parameters, QueryInner, Reply, ReplyError } from "./query"; import { ZBytes } from "./z_bytes"; +import { SessionInfo } from "./session"; function sampleKindFromUint8(val: number): SampleKind { switch (val) { @@ -86,7 +87,7 @@ function localityFromUint8(val: number): Locality { export class Qos { constructor( public readonly priority: Priority, - public readonly congestion_control: CongestionControl, + public readonly congestionControl: CongestionControl, public readonly express: boolean, public readonly reliability: Reliability, public readonly locality: Locality @@ -96,7 +97,7 @@ export class Qos { function qosToUint8(qos: Qos): number { // llrecppp let e = qos.express ? 1 : 0; - return qos.priority | (qos.congestion_control << 3) | (e << 4) | (qos.reliability << 5) | (qos.locality << 6); + return qos.priority | (qos.congestionControl << 3) | (e << 4) | (qos.reliability << 5) | (qos.locality << 6); } function qosFromUint8(val: number): Qos { @@ -146,8 +147,7 @@ function replyKeyExprFromUint8(val: number): ReplyKeyExpr { function serializeEncoding(e: Encoding, serializer: ZBytesSerializer) { let [id, schema] = e.toIdSchema(); - // TODO: add id for specifying custom encoding - serializer.serializeNumberUint16(id ?? EncodingPredefined.ZENOH_BYTES); + serializer.serializeNumberUint16(id); if (schema == undefined) { serializer.serializeBoolean(false); } else { @@ -166,7 +166,6 @@ function serializeOptEncoding(e: Encoding | undefined, serializer: ZBytesSeriali } function deserializeEncoding(deserializer: ZBytesDeserializer): Encoding { - // TODO: add id for specifying custom encoding let id = deserializer.deserializeNumberUint16(); let schema: string | undefined; if (deserializer.deserializeBoolean()) { @@ -241,20 +240,12 @@ function deserializeOptTimestamp(deserializer: ZBytesDeserializer): Timestamp | } } -function serializeOptUint8Array(a: Uint8Array | undefined, serializer: ZBytesSerializer) { +function serializeOptZBytes(a: ZBytes | undefined, serializer: ZBytesSerializer) { if (a == undefined) { serializer.serialize(false); } else { serializer.serialize(true); - serializer.serializeUint8Array(a); - } -} - -function deserializeOptUint8Array(deserializer: ZBytesDeserializer): Uint8Array | undefined { - if (deserializer.deserializeBoolean()) { - return deserializer.deserializeUint8Array(); - } else { - return undefined; + serializer.serializeUint8Array(a.toBytes()); } } @@ -278,7 +269,7 @@ function deserializeSample(deserializer: ZBytesDeserializer): Sample { return new Sample( keyexpr, payload, kind, encoding, attachment, timestamp, - qos.priority, qos.congestion_control, qos.express + qos.priority, qos.congestionControl, qos.express ); } @@ -343,14 +334,14 @@ export class DeclarePublisher { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclarePublisher; public constructor( public readonly id: number, - public readonly info: PublisherProperties, + public readonly properties: PublisherProperties, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); - serializer.serializeString(this.info.keyexpr.toString()); - serializeEncoding(this.info.encoding, serializer); - serializer.serializeNumberUint8(qosToUint8(this.info.qos)); + serializer.serializeString(this.properties.keyexpr.toString()); + serializeEncoding(this.properties.encoding, serializer); + serializer.serializeNumberUint8(qosToUint8(this.properties.qos)); } } @@ -367,20 +358,20 @@ export class UndeclarePublisher { export type SubscriberProperties = { keyexpr: KeyExpr, - allowed_origin: Locality + allowedOrigin: Locality }; export class DeclareSubscriber { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareSubscriber; public constructor( public readonly id: number, - public readonly info: SubscriberProperties + public readonly properties: SubscriberProperties ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); - serializer.serializeString(this.info.keyexpr.toString()); - serializer.serializeNumberUint8(this.info.allowed_origin); + serializer.serializeString(this.properties.keyexpr.toString()); + serializer.serializeNumberUint8(this.properties.allowedOrigin); } } @@ -398,20 +389,20 @@ export class UndeclareSubscriber { export type QueryableProperties = { keyexpr: KeyExpr, complete: boolean, - allowed_origin: Locality + allowedOrigin: Locality } export class DeclareQueryable { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareQueryable; public constructor( public readonly id: number, - public readonly info: QueryableProperties, + public readonly properties: QueryableProperties, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); - serializer.serializeString(this.info.keyexpr.toString()); - serializer.serializeBoolean(this.info.complete); - serializer.serializeNumberUint8(this.info.allowed_origin); + serializer.serializeString(this.properties.keyexpr.toString()); + serializer.serializeBoolean(this.properties.complete); + serializer.serializeNumberUint8(this.properties.allowedOrigin); } } @@ -426,27 +417,30 @@ export class UndeclareQueryable { } } -class DeclareQuerier { +export type QuerierProperties = { + keyexpr: KeyExpr, + qos: Qos, + querySettings: QuerySettings, + timeoutMs: number, +} + +export class DeclareQuerier { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareQuerier; public constructor( public readonly id: number, - public readonly keyexpr: KeyExpr, - public readonly qos: Qos, - public readonly querySettings: QuerySettings, - public readonly timeoutMs: number, + public readonly properties: QuerierProperties, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); - serializer.serializeString(this.keyexpr.toString()); - serializer.serializeNumberUint8(qosToUint8(this.qos)); - serializer.serializeNumberUint8(querySettingsToUint8(this.querySettings)); - - serializer.serializeNumberUint32(this.timeoutMs); + serializer.serializeString(this.properties.keyexpr.toString()); + serializer.serializeNumberUint8(qosToUint8(this.properties.qos)); + serializer.serializeNumberUint8(querySettingsToUint8(this.properties.querySettings)); + serializer.serializeNumberUint32(this.properties.timeoutMs); } } -class UndeclareQuerier { +export class UndeclareQuerier { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareQuerier; public constructor( public readonly id: number, @@ -457,7 +451,7 @@ class UndeclareQuerier { } } -class DeclareLivelinessToken { +export class DeclareLivelinessToken { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareLivelinessToken; public constructor( public readonly id: number, @@ -470,7 +464,7 @@ class DeclareLivelinessToken { } } -class UndeclareLivelinessToken { +export class UndeclareLivelinessToken { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareLivelinessToken; public constructor( public readonly id: number, @@ -481,180 +475,204 @@ class UndeclareLivelinessToken { } } -class DeclareLivelinessSubscriber { +export type LivelinessSubscriberProperties = { + keyexpr: KeyExpr, + history: boolean, +} + +export class DeclareLivelinessSubscriber { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.DeclareLivelinessSubscriber; public constructor( public readonly id: number, - public readonly keyexpr: KeyExpr, - public readonly history: boolean, + public readonly properties: LivelinessSubscriberProperties, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.id); - serializer.serializeString(this.keyexpr.toString()); - serializer.serializeBoolean(this.history); + serializer.serializeString(this.properties.keyexpr.toString()); + serializer.serializeBoolean(this.properties.history); } } -class GetSessionInfo { +export class UndeclareLivelinessSubscriber { + public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.UndeclareLivelinessSubscriber; + public constructor( + public readonly id: number, + ) {} + + public serializeWithZSerializer(serializer: ZBytesSerializer) { + serializer.serializeNumberUint8(this.id); + } +} + +export class GetSessionInfo { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.GetSessionInfo; public constructor() {} public serializeWithZSerializer(_serializer: ZBytesSerializer) {} } -class GetTimestamp { +export class GetTimestamp { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.GetTimestamp; public constructor() {} public serializeWithZSerializer(_serializer: ZBytesSerializer) {} } -class Put { +export class Put { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.Put; public constructor( public readonly keyexpr: KeyExpr, - public readonly payload: Uint8Array, + public readonly payload: ZBytes, public readonly encoding: Encoding, - public readonly attachment: Uint8Array | undefined, + public readonly attachment: ZBytes | undefined, public readonly timestamp: Timestamp | undefined, public readonly qos: Qos ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeString(this.keyexpr.toString()); - serializer.serializeUint8Array(this.payload); + serializer.serializeUint8Array(this.payload.toBytes()); serializeEncoding(this.encoding, serializer); - serializeOptUint8Array(this.attachment, serializer); + serializeOptZBytes(this.attachment, serializer); serializeOptTimestamp(this.timestamp, serializer); serializer.serializeNumberUint8(qosToUint8(this.qos)); } } -class Delete { +export class Delete { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.Delete; public constructor( public readonly keyexpr: KeyExpr, - public readonly attachment: Uint8Array | undefined, + public readonly attachment: ZBytes | undefined, public readonly timestamp: Timestamp | undefined, public readonly qos: Qos ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeString(this.keyexpr.toString()); - serializeOptUint8Array(this.attachment, serializer); + serializeOptZBytes(this.attachment, serializer); serializeOptTimestamp(this.timestamp, serializer); serializer.serializeNumberUint8(qosToUint8(this.qos)); } } -class PublisherPut { +export class PublisherPut { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.PublisherPut; public constructor( public readonly publisherId: number, - public readonly payload: Uint8Array, + public readonly payload: ZBytes, public readonly encoding: Encoding | undefined, - public readonly attachment: Uint8Array | undefined, + public readonly attachment: ZBytes | undefined, public readonly timestamp: Timestamp | undefined, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint32(this.publisherId); - serializer.serializeUint8Array(this.payload); + serializer.serializeUint8Array(this.payload.toBytes()); serializeOptEncoding(this.encoding, serializer); - serializeOptUint8Array(this.attachment, serializer); + serializeOptZBytes(this.attachment, serializer); serializeOptTimestamp(this.timestamp, serializer); } } -class PublisherDelete { +export class PublisherDelete { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.PublisherDelete; public constructor( public readonly publisherId: number, - public readonly attachment: Uint8Array | undefined, + public readonly attachment: ZBytes | undefined, public readonly timestamp: Timestamp | undefined, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint32(this.publisherId); - serializeOptUint8Array(this.attachment, serializer); + serializeOptZBytes(this.attachment, serializer); serializeOptTimestamp(this.timestamp, serializer); } } -class Get { +export type GetProperties = { + keyexpr: KeyExpr, + parameters: string, + payload: ZBytes | undefined, + encoding: Encoding | undefined, + attachment: ZBytes | undefined, + qos: Qos, + querySettings: QuerySettings, + timeoutMs: number, +} +export class Get { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.Get; public constructor( public readonly id: number, - public readonly keyexpr: KeyExpr, - public readonly parameters: string, - public readonly payload: Uint8Array | undefined, - public readonly encoding: Encoding | undefined, - public readonly attachment: Uint8Array | undefined, - public readonly qos: Qos, - public readonly querySettings: QuerySettings, - public readonly timeoutMs: number, + public readonly properties: GetProperties ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint32(this.id); - serializer.serializeString(this.keyexpr.toString()); - serializer.serializeString(this.parameters); - serializeOptUint8Array(this.payload, serializer); - serializeOptEncoding(this.encoding, serializer); - serializeOptUint8Array(this.attachment, serializer); - serializer.serializeNumberUint8(qosToUint8(this.qos)); - serializer.serializeNumberUint8(querySettingsToUint8(this.querySettings)); - serializer.serializeNumberUint32(this.timeoutMs); + serializer.serializeString(this.properties.keyexpr.toString()); + serializer.serializeString(this.properties.parameters); + serializeOptZBytes(this.properties.payload, serializer); + serializeOptEncoding(this.properties.encoding, serializer); + serializeOptZBytes(this.properties.attachment, serializer); + serializer.serializeNumberUint8(qosToUint8(this.properties.qos)); + serializer.serializeNumberUint8(querySettingsToUint8(this.properties.querySettings)); + serializer.serializeNumberUint32(this.properties.timeoutMs); } } -class QuerierGet { +export type QuerierGetProperties = { + querierId: number, + parameters: string, + payload: ZBytes | undefined, + encoding: Encoding | undefined, + attachment: ZBytes | undefined, +} + +export class QuerierGet { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.QuerierGet; public constructor( - public readonly querierId: number, public readonly id: number, - public readonly parameters: string, - public readonly payload: Uint8Array | undefined, - public readonly encoding: Encoding | undefined, - public readonly attachment: Uint8Array | undefined, - public readonly timeoutMs: number, + public readonly properties: QuerierGetProperties ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint32(this.querierId); serializer.serializeNumberUint32(this.id); - serializer.serializeString(this.parameters); - serializeOptUint8Array(this.payload, serializer); - serializeOptEncoding(this.encoding, serializer); - serializeOptUint8Array(this.attachment, serializer); - serializer.serializeNumberUint32(this.timeoutMs); + serializer.serializeNumberUint32(this.properties.querierId); + serializer.serializeString(this.properties.parameters); + serializeOptZBytes(this.properties.payload, serializer); + serializeOptEncoding(this.properties.encoding, serializer); + serializeOptZBytes(this.properties.attachment, serializer); } } -class LivelinessGet { +export type LivelinessGetProperties = { + keyexpr: KeyExpr, + timeoutMs: number, +} + +export class LivelinessGet { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.LivelinessGet; public constructor( public readonly id: number, - public readonly keyexpr: KeyExpr, - public readonly timeoutMs: number, + public readonly properties: LivelinessGetProperties, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint32(this.id); - serializer.serializeString(this.keyexpr.toString()); - serializer.serializeNumberUint32(this.timeoutMs); + serializer.serializeString(this.properties.keyexpr.toString()); + serializer.serializeNumberUint32(this.properties.timeoutMs); } } -class ReplyOk { +export class ReplyOk { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.ReplyOk; public constructor( public readonly queryId: number, public readonly keyexpr: KeyExpr, - public readonly payload: Uint8Array, + public readonly payload: ZBytes, public readonly encoding: Encoding, - public readonly attachment: Uint8Array | undefined, + public readonly attachment: ZBytes | undefined, public readonly timestamp: Timestamp | undefined, public readonly qos: Qos ) {} @@ -662,20 +680,20 @@ class ReplyOk { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.queryId); serializer.serializeString(this.keyexpr.toString()); - serializer.serializeUint8Array(this.payload); + serializer.serializeUint8Array(this.payload.toBytes()); serializeEncoding(this.encoding, serializer); - serializeOptUint8Array(this.attachment, serializer); + serializeOptZBytes(this.attachment, serializer); serializeOptTimestamp(this.timestamp, serializer); serializer.serializeNumberUint8(qosToUint8(this.qos)); } } -class ReplyDel { +export class ReplyDel { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.ReplyDel; public constructor( public readonly queryId: number, public readonly keyexpr: KeyExpr, - public readonly attachment: Uint8Array | undefined, + public readonly attachment: ZBytes | undefined, public readonly timestamp: Timestamp | undefined, public readonly qos: Qos ) {} @@ -683,28 +701,28 @@ class ReplyDel { public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.queryId); serializer.serializeString(this.keyexpr.toString()); - serializeOptUint8Array(this.attachment, serializer); + serializeOptZBytes(this.attachment, serializer); serializeOptTimestamp(this.timestamp, serializer); serializer.serializeNumberUint8(qosToUint8(this.qos)); } } -class ReplyErr { +export class ReplyErr { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.ReplyErr; public constructor( public readonly queryId: number, - public readonly payload: Uint8Array, + public readonly payload: ZBytes, public readonly encoding: Encoding, ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { serializer.serializeNumberUint8(this.queryId); - serializer.serializeUint8Array(this.payload); + serializer.serializeUint8Array(this.payload.toBytes()); serializeEncoding(this.encoding, serializer); } } -class QueryResponseFinal { +export class QueryResponseFinal { public readonly outMessageId: OutRemoteMessageId = OutRemoteMessageId.QueryResponseFinal; public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.QueryResponseFinal; @@ -798,9 +816,7 @@ export class ResponseSessionInfo { public readonly inMessageId: InRemoteMessageId = InRemoteMessageId.ResponseSessionInfo; public constructor( - public readonly zid: ZenohId, - public readonly routers: ZenohId[], - public readonly peers: ZenohId[], + public readonly info: SessionInfo, ) {} static deserialize(deserializer: ZBytesDeserializer): ResponseSessionInfo { @@ -808,7 +824,7 @@ export class ResponseSessionInfo { let dt = ZD.array(ZD.objectStatic(deserializeZenohId)); let routers = deserializer.deserialize(dt); let peers = deserializer.deserialize(dt); - return new ResponseSessionInfo(zid, routers, peers); + return new ResponseSessionInfo(new SessionInfo(zid, peers, routers)); } } diff --git a/zenoh-ts/src/pubsub.ts b/zenoh-ts/src/pubsub.ts index f3f7d9c5..b4843aea 100644 --- a/zenoh-ts/src/pubsub.ts +++ b/zenoh-ts/src/pubsub.ts @@ -11,22 +11,15 @@ // Contributors: // ZettaScale Zenoh Team, // - -// Remote API -import { RemoteSubscriber, RemotePublisher } from "./remote_api/pubsub.js"; - -// API import { KeyExpr } from "./key_expr.js"; import { IntoZBytes, ZBytes } from "./z_bytes.js"; -import { - CongestionControl, - Priority, - Reliability, - Sample, -} from "./sample.js"; +import { Sample } from "./sample.js"; import { Encoding, IntoEncoding } from "./encoding.js"; import { Timestamp } from "./timestamp.js"; -import { ChannelReceiver } from "./remote_api/channels.js"; +import { ChannelReceiver } from "./channels.js"; +import { SessionInner } from "./session_inner.js"; +import { PublisherDelete, PublisherProperties, PublisherPut } from "./message.js"; +import { CongestionControl, Priority, Reliability } from "./enums.js"; // ███████ ██ ██ ██████ ███████ ██████ ██████ ██ ██████ ███████ ██████ @@ -42,44 +35,45 @@ import { ChannelReceiver } from "./remote_api/channels.js"; */ export class Subscriber { - /** - * @ignore - */ - async [Symbol.asyncDispose]() { - await this.undeclare(); - } - /** - * @ignore - */ - constructor( - private remoteSubscriber: RemoteSubscriber, - private keyExpr_: KeyExpr, - private receiver_?: ChannelReceiver, - ) {} - - /** - * returns the key expression of an object - * @returns KeyExpr - */ - keyExpr(): KeyExpr { - return this.keyExpr_ - } - /** - * returns a sample receiver for non-callback subscriber, undefined otherwise. - * - * @returns ChannelReceiver | undefined - */ - receiver(): ChannelReceiver | undefined { - return this.receiver_; - } + /** + * @ignore + */ + async [Symbol.asyncDispose]() { + await this.undeclare(); + } + /** + * @ignore + */ + constructor( + private session: SessionInner, + private id: number, + private keyExpr_: KeyExpr, + private receiver_?: ChannelReceiver, + ) { } + + /** + * returns the key expression of an object + * @returns KeyExpr + */ + keyExpr(): KeyExpr { + return this.keyExpr_ + } + /** + * returns a sample receiver for non-callback subscriber, undefined otherwise. + * + * @returns ChannelReceiver | undefined + */ + receiver(): ChannelReceiver | undefined { + return this.receiver_; + } - /** - * Undeclares a subscriber on the session - * - */ - async undeclare() { - await this.remoteSubscriber.undeclare(); - } + /** + * Undeclares a subscriber on the session + * + */ + async undeclare() { + await this.session.undeclareSubscriber(this.id); + } } // ██████ ██ ██ ██████ ██ ██ ███████ ██ ██ ███████ ██████ @@ -93,173 +87,131 @@ export class Subscriber { * @param {IntoZBytes=} attachment - optional extra data to send with Payload */ export interface PublisherPutOptions { - encoding?: IntoEncoding, - attachment?: IntoZBytes, - timestamp?: Timestamp; + encoding?: IntoEncoding, + attachment?: IntoZBytes, + timestamp?: Timestamp; } /** * @param {IntoZBytes=} attachment - optional extra data to send with Payload */ export interface PublisherDeleteOptions { - attachment?: IntoZBytes, - timestamp?: Timestamp + attachment?: IntoZBytes, + timestamp?: Timestamp } +/** + * Class that represents a Zenoh Publisher, + * created by calling `Session.declarePublisher()` + */ export class Publisher { - /** - * Class that represents a Zenoh Publisher, - * created by calling `declare_publisher()` on a `session` - */ - - /** - * @ignore - */ - async [Symbol.asyncDispose]() { - await this.undeclare(); - } - - /** - * @ignore - * - * Creates a new Publisher on a session - * Note: this should never be called directly by the user. - * please use `declare_publisher` on a session. - * - * @param {KeyExpr} keyExpr_ - A Key Expression - * @param {RemotePublisher} remotePublisher - A Session to create the publisher on - * @param {CongestionControl} congestionControl_ - Congestion control - * @param {Priority} priority_ - Priority for Zenoh Data - * @param {Reliability} reliability_ - Reliability for publishing data - * - * @returns {Publisher} a new instance of a publisher - * - */ - constructor( - private remotePublisher: RemotePublisher, - private keyExpr_: KeyExpr, - private congestionControl_: CongestionControl, - private priority_: Priority, - private reliability_: Reliability, - private encoding_: Encoding, - ) {} - - /** - * gets the Key Expression from Publisher - * - * @returns {KeyExpr} instance - */ - keyExpr(): KeyExpr { - return this.keyExpr_; - } - - /** - * Puts a payload on the publisher associated with this class instance - * - * @param {IntoZBytes} payload - * @param {PublisherPutOptions} putOptions - * - * @returns void - */ - async put( - payload: IntoZBytes, - putOptions?: PublisherPutOptions, - ) { - let zbytes: ZBytes = new ZBytes(payload); - let encoding; - let timestamp = null; - if (putOptions?.timestamp != null) { - timestamp = putOptions.timestamp.getResourceUuid() as unknown as string; + /** + * @ignore + */ + async [Symbol.asyncDispose]() { + await this.undeclare(); } - if (putOptions?.encoding != null) { - encoding = Encoding.fromString(putOptions.encoding.toString()); - } else { - encoding = Encoding.default(); + /** + * @ignore + */ + constructor( + private session: SessionInner, + private publisherId: number, + private properties: PublisherProperties, + ) { } + + /** + * gets the Key Expression from Publisher + * + * @returns {KeyExpr} instance + */ + keyExpr(): KeyExpr { + return this.properties.keyexpr; } - let attachment = null; - if (putOptions?.attachment != null) { - let attBytes = new ZBytes(putOptions.attachment); - attachment = Array.from(attBytes.toBytes()); + /** + * Puts a payload on the publisher associated with this class instance + * + * @param {IntoZBytes} payload + * @param {PublisherPutOptions} putOptions + * + * @returns void + */ + async put( + payload: IntoZBytes, + putOptions?: PublisherPutOptions, + ) { + await this.session.publisherPut( + new PublisherPut( + this.publisherId, + new ZBytes(payload), + putOptions?.encoding ? Encoding.from(putOptions.encoding) : undefined, + putOptions?.attachment ? new ZBytes(putOptions.attachment) : undefined, + putOptions?.timestamp + ) + ); } - return await this.remotePublisher.put( - Array.from(zbytes.toBytes()), - attachment, - encoding.toString(), - timestamp, - ); - } - - /** - * get Encoding declared for Publisher - * - * @returns {Encoding} - */ - encoding(): Encoding { - return this.encoding_; - } - - /** - * get Priority declared for Publisher - * - * @returns {Priority} - */ - priority(): Priority { - return this.priority_; - } - - /** - * get Reliability declared for Publisher - * - * @returns {Reliability} - */ - reliability(): Reliability { - return this.reliability_; - } - - /** - * get Congestion Control for a Publisher - * - * @returns {CongestionControl} - */ - congestionControl(): CongestionControl { - return this.congestionControl_; - } + /** + * get Encoding declared for Publisher + * + * @returns {Encoding} + */ + encoding(): Encoding { + return this.properties.encoding; + } - /** - * - * executes delete on publisher - * @param {PublisherDeleteOptions} deleteOptions: Options associated with a publishers delete - * @returns void - */ - async delete(deleteOptions: PublisherDeleteOptions) { + /** + * get Priority declared for Publisher + * + * @returns {Priority} + */ + priority(): Priority { + return this.properties.qos.priority; + } - let attachment = null; - if (deleteOptions.attachment != null) { - let attBytes = new ZBytes(deleteOptions.attachment); - attachment = Array.from(attBytes.toBytes()); + /** + * get Reliability declared for Publisher + * + * @returns {Reliability} + */ + reliability(): Reliability { + return this.properties.qos.reliability; } - let timestamp = null; - if (deleteOptions.timestamp != null) { - timestamp = deleteOptions.timestamp.getResourceUuid() as unknown as string; + /** + * get Congestion Control declared for a Publisher + * + * @returns {CongestionControl} + */ + congestionControl(): CongestionControl { + return this.properties.qos.congestionControl; } - return await this.remotePublisher.delete( - attachment, - timestamp - ); - } + /** + * + * executes delete on publisher + * @param {PublisherDeleteOptions=} deleteOptions: Options associated with a publishers delete + * @returns void + */ + async delete(deleteOptions?: PublisherDeleteOptions) { + await this.session.publisherDelete( + new PublisherDelete( + this.publisherId, + deleteOptions?.attachment ? new ZBytes(deleteOptions.attachment) : undefined, + deleteOptions?.timestamp + ) + ) + } - /** - * undeclares publisher - * - * @returns void - */ - async undeclare() { - await this.remotePublisher.undeclare(); - } + /** + * undeclares publisher + * + * @returns void + */ + async undeclare() { + await this.session.undeclarePublisher(this.publisherId); + } } diff --git a/zenoh-ts/src/querier.ts b/zenoh-ts/src/querier.ts index 44f7b32c..b471743e 100644 --- a/zenoh-ts/src/querier.ts +++ b/zenoh-ts/src/querier.ts @@ -11,117 +11,28 @@ // Contributors: // ZettaScale Zenoh Team, // - -// Remote API -import { ReplyWS } from "./remote_api/interface/ReplyWS.js"; -// API import { IntoZBytes, ZBytes } from "./z_bytes.js"; -import { CongestionControl, ConsolidationMode, Priority, } from "./sample.js"; -import { TimeDuration } from "typed-duration"; -import { RemoteQuerier } from "./remote_api/querier.js"; import { KeyExpr } from "./key_expr.js"; -import { Parameters, Reply, replyFromReplyWS } from "./query.js"; -import { ChannelReceiver, FifoChannel, Handler, intoCbDropReceiver } from "./remote_api/channels.js"; -import { Encoding } from "./encoding.js"; - -/** - * Target For Get queries - * @default BestMatching - */ -export enum QueryTarget { - /// Let Zenoh find the BestMatching queryable capabale of serving the query. - BestMatching, - /// Deliver the query to all queryables matching the query's key expression. - All, - /// Deliver the query to all queryables matching the query's key expression that are declared as complete. - AllComplete, -} - -/** - * Convenience function to convert between QueryTarget and int - * @internal - */ -export function queryTargetToInt(queryTarget?: QueryTarget): number { - switch (queryTarget) { - case QueryTarget.BestMatching: - return 0; - case QueryTarget.All: - return 1; - case QueryTarget.AllComplete: - return 2; - default: - // Default is QueryTarget.BestMatching - return 1; - } -} - -export enum Locality { - SessionLocal, - Remote, - Any, -} +import { IntoParameters, Parameters, Reply } from "./query.js"; +import { ChannelReceiver, FifoChannel, Handler, intoCbDropReceiver } from "./channels.js"; +import { Encoding, IntoEncoding } from "./encoding.js"; +import { SessionInner } from "./session_inner.js"; +import { CongestionControl, Priority, ReplyKeyExpr } from "./enums.js"; /** - * Convenience function to convert between Locality and int - * @internal - */ -export function localityToInt(queryTarget?: Locality): number { - switch (queryTarget) { - case Locality.SessionLocal: - return 0; - case Locality.Remote: - return 1; - case Locality.Any: - return 2; - default: - // Default is Locality.Any - return 2; - } -} - -export enum ReplyKeyExpr { - /// Accept replies whose key expressions may not match the query key expression. - Any, - /// Accept replies whose key expressions match the query key expression. - MatchingQuery, -} - -/** - * Convenience function to convert between QueryTarget function and int - * @internal - */ -export function replyKeyExprToInt(queryTarget?: ReplyKeyExpr): number { - switch (queryTarget) { - case ReplyKeyExpr.Any: - return 0; - case ReplyKeyExpr.MatchingQuery: - return 1; - default: - // Default is ReplyKeyExpr.MatchingQuery - return 1; - } -} - -/** - * QuerierOptions When initializing a Querier - * - */ -export interface QuerierOptions { - congestionControl?: CongestionControl, - consolidation?: ConsolidationMode, - priority?: Priority, - express?: boolean, - target: QueryTarget - timeout?: TimeDuration, - allowedDestination?: Locality - acceptReplies?: ReplyKeyExpr -} - + * Options for a Querier Get operation + * @prop {IntoParameters=} parameters - Optional query parameters + * @prop {IntoEncoding=} encoding - Encoding type of payload + * @prop {IntoZBytes=} payload - Payload associated with the query + * @prop {IntoZBytes=} attachment - Additional Data sent with the query + * @prop {Handler=} handler - A reply handler +*/ export interface QuerierGetOptions { - encoding?: Encoding, - payload?: IntoZBytes, - attachment?: IntoZBytes, - handler?: Handler + parameters?: IntoParameters, + encoding?: IntoEncoding, + payload?: IntoZBytes, + attachment?: IntoZBytes, + handler?: Handler } /** @@ -129,110 +40,84 @@ export interface QuerierGetOptions { * created by Session.declare_queryable */ export class Querier { - private undeclared: boolean = false; - /** - * @ignore - */ - async [Symbol.asyncDispose]() { - await this.undeclare(); - } - - /** - * @ignore - * Returns a Querier - * Note! : user must use declare_querier on a session - */ - constructor( - private remoteQuerier: RemoteQuerier, - private keyExpr_: KeyExpr, - private congestionControl_: CongestionControl, - private priority_: Priority, - private acceptReplies_: ReplyKeyExpr, - ) {} - - /** - * Undeclares Queryable - * @returns void - */ - async undeclare() { - this.undeclared = true; - await this.remoteQuerier.undeclare() - } - - /** - * returns key expression for this Querier - * @returns KeyExpr - */ - keyExpr() { - return this.keyExpr_; - } - - /** - * returns Congestion Control for this Querier - * @returns CongestionControl - */ - congestionControl() { - return this.congestionControl_; - } - - /** - * returns Priority for this Querier - * @returns Priority - */ - priority() { - return this.priority_; - } - - /** - * returns ReplyKeyExpr for this Querier - * @returns ReplyKeyExpr - */ - acceptReplies() { - return this.acceptReplies_; - } - - /** - * Issue a Get request on this querier - * @returns Promise - */ - async get( - parameters?: Parameters, - getOptions?: QuerierGetOptions): Promise | undefined> { - if (this.undeclared == true) { - return undefined; + /** + * @ignore + */ + async [Symbol.asyncDispose]() { + await this.undeclare(); } - let payload; - let attachment; - let parametersStr; - let encoding = getOptions?.encoding?.toString() - if (getOptions?.attachment != undefined) { - attachment = Array.from(new ZBytes(getOptions?.attachment).toBytes()) + /** + * @ignore + */ + constructor( + private session: SessionInner, + private querierId: number, + private keyExpr_: KeyExpr, + private congestionControl_: CongestionControl, + private priority_: Priority, + private acceptReplies_: ReplyKeyExpr, + ) { } + + /** + * Undeclares Queryable + * @returns void + */ + async undeclare() { + await this.session.undeclareQuerier(this.querierId); } - if (getOptions?.payload != undefined) { - payload = Array.from(new ZBytes(getOptions?.payload).toBytes()) + + /** + * returns key expression for this Querier + * @returns KeyExpr + */ + keyExpr() { + return this.keyExpr_; } - if (parameters != undefined) { - parametersStr = parameters.toString(); + + /** + * returns Congestion Control for this Querier + * @returns CongestionControl + */ + congestionControl() { + return this.congestionControl_; } - let handler = getOptions?.handler ?? new FifoChannel(256); - let [callback, drop, receiver] = intoCbDropReceiver(handler); - - let callbackWS = (replyWS: ReplyWS): void => { - let reply: Reply = replyFromReplyWS(replyWS); - callback(reply); + /** + * returns Priority for this Querier + * @returns Priority + */ + priority() { + return this.priority_; } - await this.remoteQuerier.get( - callbackWS, - drop, - encoding, - parametersStr, - attachment, - payload, - ); + /** + * returns ReplyKeyExpr for this Querier + * @returns ReplyKeyExpr + */ + acceptReplies() { + return this.acceptReplies_; + } - return receiver; - } + /** + * Issue a Get request on this querier + * @returns Promise + */ + async get(getOpts?: QuerierGetOptions): Promise | undefined> { + + let handler = getOpts?.handler ?? new FifoChannel(256); + let [callback, drop, receiver] = intoCbDropReceiver(handler); + + await this.session.querierGet( + { + querierId: this.querierId, + parameters: getOpts?.parameters ? new Parameters(getOpts.parameters).toString() : "", + payload: getOpts?.payload ? new ZBytes(getOpts.payload) : undefined, + encoding: getOpts?.encoding ? Encoding.from(getOpts.encoding) : undefined, + attachment: getOpts?.attachment ? new ZBytes(getOpts.attachment) : undefined, + }, + { callback, drop } + ); + return receiver; + } } diff --git a/zenoh-ts/src/query.ts b/zenoh-ts/src/query.ts index b621a494..8cf23f9a 100644 --- a/zenoh-ts/src/query.ts +++ b/zenoh-ts/src/query.ts @@ -12,21 +12,15 @@ // ZettaScale Zenoh Team, // -import { decode as b64_bytes_from_str, } from "base64-arraybuffer"; -// Remote API -import { RemoteQueryable } from "./remote_api/query.js"; -import { ReplyWS } from "./remote_api/interface/ReplyWS.js"; -import { ReplyErrorWS } from "./remote_api/interface/ReplyErrorWS.js"; -import { RemoteSession, UUIDv4 } from "./remote_api/session.js"; -import { QueryWS } from "./remote_api/interface/QueryWS.js"; -// API import { IntoKeyExpr, KeyExpr } from "./key_expr.js"; import { IntoZBytes, ZBytes } from "./z_bytes.js"; -import { congestionControlToInt, CongestionControl, Priority, priorityToInt, Sample, sampleFromSampleWS } from "./sample.js"; -import { Encoding } from "./encoding.js"; +import { Sample } from "./sample.js"; +import { Encoding, IntoEncoding } from "./encoding.js"; import { Timestamp } from "./timestamp.js"; -import { ChannelReceiver } from "./remote_api/channels.js"; -import { ReplyKeyExpr } from "./enums.js"; +import { ChannelReceiver } from "./channels.js"; +import { CongestionControl, Locality, Priority, Reliability, ReplyKeyExpr } from "./enums.js"; +import { SessionInner } from "./session_inner.js"; +import { Qos, ReplyDel, ReplyErr, ReplyOk } from "./message.js"; @@ -43,71 +37,48 @@ import { ReplyKeyExpr } from "./enums.js"; * created by Session.declare_queryable */ export class Queryable { - /** - * @ignore - */ - async [Symbol.asyncDispose]() { - await this.undeclare(); - } - /** - * @ignore - * Returns a Queryable - * Note! : user must use declare_queryable on a session - */ - constructor(private remoteQueryable: RemoteQueryable, private receiver_?: ChannelReceiver) {} - - /** - * returns a sample receiver for non-callback subscriber, undefined otherwise. - * - * @returns ChannelReceiver | undefined - */ - receiver(): ChannelReceiver | undefined { - return this.receiver_; - } - - /** - * Undeclares Queryable - * @returns void - */ - async undeclare() { - this.remoteQueryable.undeclare(); - } + /** + * @ignore + */ + async [Symbol.asyncDispose]() { + await this.undeclare(); + } + /** + * @ignore + * Returns a Queryable + * Note! : user must use declare_queryable on a session + */ + constructor( + private session: SessionInner, + private id: number, + private keyExpr_: KeyExpr, + private receiver_?: ChannelReceiver + ) { } + + /** + * returns the key expression of an object + * @returns KeyExpr + */ + keyExpr(): KeyExpr { + return this.keyExpr_ + } -} + /** + * returns a sample receiver for non-callback subscriber, undefined otherwise. + * + * @returns ChannelReceiver | undefined + */ + receiver(): ChannelReceiver | undefined { + return this.receiver_; + } -/** - * Convenience function to convert between QueryWS and Query - * @ignore - */ -export function queryFromQueryWS( - queryWS: QueryWS, - sessionRef: RemoteSession -): Query { - let keyExpr: KeyExpr = new KeyExpr(queryWS.key_expr); - let payload: ZBytes | undefined = undefined; - let attachment: ZBytes | undefined = undefined; - let parameters: Parameters = new Parameters(queryWS.parameters); - let encoding: Encoding | undefined = undefined; - - if (queryWS.payload != null) { - payload = new ZBytes(new Uint8Array(b64_bytes_from_str(queryWS.payload))); - } - if (queryWS.attachment != null) { - attachment = new ZBytes(new Uint8Array(b64_bytes_from_str(queryWS.attachment))); - } - if (queryWS.encoding != null) { - encoding = Encoding.fromString(queryWS.encoding); - } - - return new Query( - queryWS.query_uuid, - keyExpr, - parameters, - payload, - attachment, - encoding, - sessionRef, - ); + /** + * Undeclares Queryable + * @returns void + */ + async undeclare() { + await this.session.undeclareQueryable(this.id); + } } // ██████ ██ ██ ███████ ██████ ██ ██ @@ -118,194 +89,197 @@ export function queryFromQueryWS( // ▀▀ /** - * Options for a Query::reply operation - * @prop {Encoding=} encoding - Encoding type of payload - * @prop {Priority=} priority - priority of the written data - * @prop {CongestionControl=} congestion_control - congestion_control applied when routing the data - * @prop {boolean=} express - Express - * @prop {Timestamp=} timestamp - Timestamp of the message - * @prop {ConsolidationMode=} consolidation - consolidation mode - * @prop {IntoZBytes=} attachment - Additional Data sent with the request + * Options for a Query.Reply operation + * @prop {IntoEncoding=} encoding - Encoding type of reply payload + * @prop {CongestionControl=} congestionControl - congestionControl applied when routing the reply + * @prop {Priority=} priority - priority of the reply + * @prop {boolean=} express - Express: if set to `true`, this reply will not be batched. This usually has a positive impact on latency but negative impact on throughput. + * @prop {Timestamp=} timestamp - Timestamp of the reply + * @prop {IntoZBytes=} attachment - Additional Data sent with the reply */ export interface ReplyOptions { - encoding?: Encoding, - priority?: Priority, - congestionControl?: CongestionControl, - express?: boolean, - timestamp?: Timestamp; - attachment?: IntoZBytes + encoding?: IntoEncoding, + congestionControl?: CongestionControl, + priority?: Priority, + express?: boolean, + timestamp?: Timestamp; + attachment?: IntoZBytes } /** - * Options for a Query::reply_err operation - * @prop {Encoding=} encoding - Encoding type of payload + * Options for a Query.replyErr operation + * @prop {IntoEncoding=} encoding - Encoding type of reply payload */ export interface ReplyErrOptions { - encoding?: Encoding, + encoding?: Encoding, } /** - * Options for a Query::reply_del operation - * @prop {Priority=} priority - priority of the written data - * @prop {CongestionControl=} congestion_control - congestion_control applied when routing the data - * @prop {boolean=} express - Express - * @prop {Timestamp=} timestamp - Timestamp of the message - * @prop {IntoZBytes=} attachment - Additional Data sent with the request - * @prop {ConsolidationMode=} consolidation - consolidation mode - * @prop {IntoZBytes=} attachment - Additional Data sent with the request + * Options for a Query.ReplyDel operation + * @prop {CongestionControl=} congestionControl - congestion control applied when routing the reply + * @prop {Priority=} priority - priority of the reply + * @prop {boolean=} express - Express: if set to `true`, this reply will not be batched. This usually has a positive impact on latency but negative impact on throughput. + * @prop {Timestamp=} timestamp - Timestamp of the reply + * @prop {IntoZBytes=} attachment - Additional Data sent with the reply */ export interface ReplyDelOptions { - priority?: Priority, - congestionControl?: CongestionControl, - express?: boolean, - timestamp?: Timestamp; - attachment?: IntoZBytes + priority?: Priority, + congestionControl?: CongestionControl, + express?: boolean, + timestamp?: Timestamp; + attachment?: IntoZBytes } export class QueryInner { - constructor( - public readonly queryId_: number, - public readonly keyexpr_: KeyExpr, - public readonly parameters_: Parameters, - public readonly payload_: ZBytes | undefined, - public readonly encoding_: Encoding | undefined, - public readonly attachment_: ZBytes | undefined, - public readonly replyKeyExpr_: ReplyKeyExpr, - ) {} + constructor( + public readonly queryId: number, + public readonly keyexpr_: KeyExpr, + public readonly parameters_: Parameters, + public readonly payload_: ZBytes | undefined, + public readonly encoding_: Encoding | undefined, + public readonly attachment_: ZBytes | undefined, + public readonly replyKeyExpr_: ReplyKeyExpr, + ) { } } /** * Query Class to handle */ export class Query { - /** - * @ignore - * New Function Used to Construct Query, - * Note: Users should not need to call this function - * But will receieve 'Query's from Queryables - */ - constructor( - private queryId: UUIDv4, - private keyExpr_: KeyExpr, - private parameters_: Parameters, - private payload_: ZBytes | undefined, - private attachment_: ZBytes | undefined, - private encoding_: Encoding | undefined, - private sessionRef: RemoteSession, - ) {} - - /** - * gets an selector of Query - * @returns Selector - */ - selector() { - return new Selector(this.keyExpr_, this.parameters_) - } - /** - * gets the KeyExpr of Query - * @returns KeyExpr - */ - keyExpr(): KeyExpr { - return this.keyExpr_; - } - /** - * gets the Parameters of Query - * @returns Parameters - */ - parameters(): Parameters { - return this.parameters_; - } - /** - * gets the Optioanl payload of Query - * @returns ZBytes | undefined - */ - payload(): ZBytes | undefined { - return this.payload_; - } - /** - * gets the Optional Encoding of a Query - * @returns Encoding | undefined - */ - encoding(): Encoding | undefined { - return this.encoding_; - } - /** - * gets the Optional Attachment of a Query - * @returns ZBytes | undefined - */ - attachment(): ZBytes | undefined { - return this.attachment_; - } + /** + * @ignore + * + */ + constructor( + private session: SessionInner, + private inner: QueryInner, + ) { } + + /** + * gets an selector of Query + * @returns Selector + */ + selector() { + return new Selector(this.inner.keyexpr_, this.inner.parameters_) + } + /** + * gets the KeyExpr of Query + * @returns KeyExpr + */ + keyExpr(): KeyExpr { + return this.inner.keyexpr_; + } + /** + * gets the Parameters of Query + * @returns Parameters + */ + parameters(): Parameters { + return this.inner.parameters_; + } + /** + * gets the Optioanl payload of Query + * @returns ZBytes | undefined + */ + payload(): ZBytes | undefined { + return this.inner.payload_; + } + /** + * gets the Optional Encoding of a Query + * @returns Encoding | undefined + */ + encoding(): Encoding | undefined { + return this.inner.encoding_; + } + /** + * gets the Optional Attachment of a Query + * @returns ZBytes | undefined + */ + attachment(): ZBytes | undefined { + return this.inner.attachment_; + } - /** - * Sends a Reply to for Query - * @param {IntoKeyExpr} intoKeyExpr + /** + * Sends a Reply to for Query + * @param {IntoKeyExpr} intoKeyExpr + * @param {IntoZBytes} payload + * @param {ReplyOptions=} replyOpts + * @returns void + */ + async reply(intoKeyExpr: IntoKeyExpr, payload: IntoZBytes, replyOpts?: ReplyOptions) { + await this.session.replyOk( + new ReplyOk( + this.inner.queryId, + new KeyExpr(intoKeyExpr), + new ZBytes(payload), + replyOpts?.encoding ? Encoding.from(replyOpts.encoding) : Encoding.default(), + replyOpts?.attachment ? new ZBytes(replyOpts.attachment) : undefined, + replyOpts?.timestamp, + new Qos( + replyOpts?.priority ?? Priority.DEFAULT, + replyOpts?.congestionControl ?? CongestionControl.DEFAULT_RESPONSE, + replyOpts?.express ?? false, + Reliability.DEFAULT, + Locality.DEFAULT + ) + ) + ); + } + /** + * Sends an Error Reply for a query * @param {IntoZBytes} payload - * @param {ReplyOptions=} options + * @param {ReplyErrOptions=} replyErrOpts * @returns void */ - async reply(intoKeyExpr: IntoKeyExpr, payload: IntoZBytes, options?: ReplyOptions) { - let keyExpr: KeyExpr = new KeyExpr(intoKeyExpr); - - let optAttachment: Uint8Array | null = null; - if (options?.attachment != undefined) { - optAttachment = new ZBytes(options?.attachment).toBytes(); - } - - await this.sessionRef.reply( - this.queryId, - keyExpr.toString(), - new ZBytes(payload).toBytes(), - options?.encoding?.toString() ?? null, - congestionControlToInt(options?.congestionControl), - priorityToInt(options?.priority), - options?.express ?? false, - optAttachment, - options?.timestamp?.toString() ?? null, - ); - } - /** - * Sends an Error Reply to a query - * @param {IntoZBytes} payload - * @param {ReplyErrOptions=} options - * @returns void - */ - async replyErr(payload: IntoZBytes, options?: ReplyErrOptions) { - await this.sessionRef.replyErr( - this.queryId, - new ZBytes(payload).toBytes(), - options?.encoding?.toString() ?? null, - ); - } - - /** - * Sends an Error Reply to a query - * @param intoKeyExpr IntoKeyExpr - * @param {ReplyDelOptions=} options - * @returns void - */ - async replyDel(intoKeyExpr: IntoKeyExpr, options?: ReplyDelOptions) { - let keyExpr: KeyExpr = new KeyExpr(intoKeyExpr); - - let optAttachment: Uint8Array | null = null; - if (options?.attachment != undefined) { - optAttachment = new ZBytes(options?.attachment).toBytes(); - } - - await this.sessionRef.replyDel( - this.queryId, - keyExpr.toString(), - congestionControlToInt(options?.congestionControl), - priorityToInt(options?.priority), - options?.express ?? false, - optAttachment, - options?.timestamp?.toString() ?? null, - ); - } - - toString(): string { - return this.keyExpr.toString() + "?" + this.parameters.toString() - } + async replyErr(payload: IntoZBytes, replyErrOpts?: ReplyErrOptions) { + await this.session.replyErr( + new ReplyErr( + this.inner.queryId, + new ZBytes(payload), + replyErrOpts?.encoding ? Encoding.from(replyErrOpts.encoding) : Encoding.default(), + ) + ); + } + + /** + * Sends an Error Reply for a query + * @param intoKeyExpr IntoKeyExpr + * @param {ReplyDelOptions=} replyDelOpts + * @returns void + */ + async replyDel(intoKeyExpr: IntoKeyExpr, replyDelOpts?: ReplyDelOptions) { + await this.session.replyDel( + new ReplyDel( + this.inner.queryId, + new KeyExpr(intoKeyExpr), + replyDelOpts?.attachment ? new ZBytes(replyDelOpts.attachment) : undefined, + replyDelOpts?.timestamp, + new Qos( + replyDelOpts?.priority ?? Priority.DEFAULT, + replyDelOpts?.congestionControl ?? CongestionControl.DEFAULT_RESPONSE, + replyDelOpts?.express ?? false, + Reliability.DEFAULT, + Locality.DEFAULT + ) + ) + ); + } + + /** + * Finalize query, signaling that all replies have been sent. + * This function MUST be called once all replies to the query have been sent. + * @returns void + */ + async finalize() { + this.session.sendResponseFinal(this.inner.queryId); + } + + toString(): string { + return this.keyExpr.toString() + "?" + this.parameters.toString() + } + + async [Symbol.asyncDispose]() { + await this.finalize(); + } } @@ -320,195 +294,195 @@ export type IntoParameters = Parameters | string | String | Map * `let p = Parameters.new(a)` */ export class Parameters { - private source: string; - - constructor(intoParameters: IntoParameters) { - if (intoParameters instanceof Parameters) { - this.source = intoParameters.source; - } else if (intoParameters instanceof Map) { - // Convert Map to string format, handling empty values - this.source = Array.from(intoParameters.entries()) - .map(([k, v]) => v ? `${k}=${v}` : k) - .join(';'); - } else { - this.source = intoParameters.toString(); - } - } - - private *iterByKeyValuePos(): Generator<[number, number, number, number]> { - if (this.source.length === 0) return; - - let pos = 0; - while (pos < this.source.length) { - // Skip leading semicolons - while (pos < this.source.length && this.source[pos] === ';') pos++; - if (pos >= this.source.length) break; - - const keyStart = pos; - // Find end of key (semicolon or equals sign) - while (pos < this.source.length && this.source[pos] !== ';' && this.source[pos] !== '=') pos++; - const keyLen = pos - keyStart; - if (keyLen === 0) continue; // Skip empty keys - - let valueStart = -1; - let valueLen = 0; - - // If we found an equals sign, look for the value - if (pos < this.source.length && this.source[pos] === '=') { - pos++; // Skip equals sign - valueStart = pos; - // Find end of value (semicolon or end of string) - while (pos < this.source.length && this.source[pos] !== ';') pos++; - valueLen = pos - valueStart; - } - - yield [keyStart, keyLen, valueStart, valueLen]; - pos++; // Skip past semicolon or increment if at end - } - } - - /** - * Creates empty Parameters Structs - * @returns Parameters - */ - static empty(): Parameters { - return new Parameters(""); - } - - /** - * removes a key from the parameters - * @returns boolean - */ - remove(key: string): boolean { - let found = false; - let newSource = ''; - let lastPos = 0; - - for (const [keyStart, keyLen, valueStart, valueLen] of this.iterByKeyValuePos()) { - const currentKey = this.source.slice(keyStart, keyStart + keyLen); - if (currentKey == key) { - // Add the part between last position and current key - newSource += this.source.slice(lastPos, keyStart); - // Calculate where the next parameter starts - lastPos = valueStart >= 0 ? - valueStart + valueLen + 1 : // +1 for semicolon - keyStart + keyLen + 1; - found = true; - } - } - - if (found) { - // Add remaining part of string - newSource += this.source.slice(lastPos); - // Clean up consecutive semicolons and trailing/leading semicolons - this.source = newSource.replace(/;+/g, ';').replace(/^;|;$/g, ''); - } - - return found; - } - /** - * gets an generator over the pairs (key,value) of the Parameters - * @returns Generator - */ - *iter(): Generator<[string, string]> { - for (const [keyStart, keyLen, valueStart, valueLen] of this.iterByKeyValuePos()) { - let key = this.source.slice(keyStart, keyStart + keyLen); - let value = valueStart >= 0 ? this.source.slice(valueStart, valueStart + valueLen) : ''; - yield [key, value]; - } - } - - /** - * gets an generator over the values separated by `|` in multivalue parameters - * @returns Generator - */ - *values(key: string): Generator { - let value = this.get(key); - if (value != undefined) { - let values = value.split('|'); - for (const v of values) { - yield v; - } - } - } - - /** - * Returns true if properties does not contain anything. - * @returns boolean - */ - isEmpty(): boolean { - // Quick check for empty string - if (!this.source) return true; - // Otherwise check if there are any valid entries - for (const _ of this.iterByKeyValuePos()) { - return false; - } - return true; - } - - /** - * checks if parameters contains key - * @returns boolean - */ - containsKey(key: string): boolean { - for (const [keyStart, keyLen] of this.iterByKeyValuePos()) { - if (this.source.slice(keyStart, keyStart + keyLen) === key) { + private source: string; + + constructor(intoParameters: IntoParameters) { + if (intoParameters instanceof Parameters) { + this.source = intoParameters.source; + } else if (intoParameters instanceof Map) { + // Convert Map to string format, handling empty values + this.source = Array.from(intoParameters.entries()) + .map(([k, v]) => v ? `${k}=${v}` : k) + .join(';'); + } else { + this.source = intoParameters.toString(); + } + } + + private *iterByKeyValuePos(): Generator<[number, number, number, number]> { + if (this.source.length === 0) return; + + let pos = 0; + while (pos < this.source.length) { + // Skip leading semicolons + while (pos < this.source.length && this.source[pos] === ';') pos++; + if (pos >= this.source.length) break; + + const keyStart = pos; + // Find end of key (semicolon or equals sign) + while (pos < this.source.length && this.source[pos] !== ';' && this.source[pos] !== '=') pos++; + const keyLen = pos - keyStart; + if (keyLen === 0) continue; // Skip empty keys + + let valueStart = -1; + let valueLen = 0; + + // If we found an equals sign, look for the value + if (pos < this.source.length && this.source[pos] === '=') { + pos++; // Skip equals sign + valueStart = pos; + // Find end of value (semicolon or end of string) + while (pos < this.source.length && this.source[pos] !== ';') pos++; + valueLen = pos - valueStart; + } + + yield [keyStart, keyLen, valueStart, valueLen]; + pos++; // Skip past semicolon or increment if at end + } + } + + /** + * Creates empty Parameters Structs + * @returns Parameters + */ + static empty(): Parameters { + return new Parameters(""); + } + + /** + * removes a key from the parameters + * @returns boolean + */ + remove(key: string): boolean { + let found = false; + let newSource = ''; + let lastPos = 0; + + for (const [keyStart, keyLen, valueStart, valueLen] of this.iterByKeyValuePos()) { + const currentKey = this.source.slice(keyStart, keyStart + keyLen); + if (currentKey == key) { + // Add the part between last position and current key + newSource += this.source.slice(lastPos, keyStart); + // Calculate where the next parameter starts + lastPos = valueStart >= 0 ? + valueStart + valueLen + 1 : // +1 for semicolon + keyStart + keyLen + 1; + found = true; + } + } + + if (found) { + // Add remaining part of string + newSource += this.source.slice(lastPos); + // Clean up consecutive semicolons and trailing/leading semicolons + this.source = newSource.replace(/;+/g, ';').replace(/^;|;$/g, ''); + } + + return found; + } + /** + * gets an generator over the pairs (key,value) of the Parameters + * @returns Generator + */ + *iter(): Generator<[string, string]> { + for (const [keyStart, keyLen, valueStart, valueLen] of this.iterByKeyValuePos()) { + let key = this.source.slice(keyStart, keyStart + keyLen); + let value = valueStart >= 0 ? this.source.slice(valueStart, valueStart + valueLen) : ''; + yield [key, value]; + } + } + + /** + * gets an generator over the values separated by `|` in multivalue parameters + * @returns Generator + */ + *values(key: string): Generator { + let value = this.get(key); + if (value != undefined) { + let values = value.split('|'); + for (const v of values) { + yield v; + } + } + } + + /** + * Returns true if properties does not contain anything. + * @returns boolean + */ + isEmpty(): boolean { + // Quick check for empty string + if (!this.source) return true; + // Otherwise check if there are any valid entries + for (const _ of this.iterByKeyValuePos()) { + return false; + } return true; - } - } - return false; - } - - /** - * gets first found value with associated key, returning undefined if key does not exist - * @returns string | undefined - */ - get(key: string): string | undefined { - for (const [keyStart, keyLen, valueStart, valueLen] of this.iterByKeyValuePos()) { - if (this.source.slice(keyStart, keyStart + keyLen) === key) { - return valueStart >= 0 ? this.source.slice(valueStart, valueStart + valueLen) : ''; - } - } - return undefined; - } - - /** - * Inserts new key,value pair into parameter - * @returns void - */ - insert(key: string, value: string): void { - // Remove any existing instances of the key - this.remove(key); - - // Add new key-value pair - if (this.source && !this.source.endsWith(';') && this.source.length > 0) { - this.source += ';'; - } - this.source += `${key}=${value}`; - } - - /** - * extends this Parameters with the value of other parameters, overwriting `this` if keys match. - * @returns void - */ - extend(other: IntoParameters): void { - const otherParams = new Parameters(other); - for (const [keyStart, keyLen, valueStart, valueLen] of otherParams.iterByKeyValuePos()) { - const key = otherParams.source.slice(keyStart, keyStart + keyLen); - const value = valueStart >= 0 ? - otherParams.source.slice(valueStart, valueStart + valueLen) : - ''; - this.insert(key, value); - } - } - - /** - * returns the string representation of the parameters - * @returns string - */ - toString(): string { - return this.source; - } + } + + /** + * checks if parameters contains key + * @returns boolean + */ + containsKey(key: string): boolean { + for (const [keyStart, keyLen] of this.iterByKeyValuePos()) { + if (this.source.slice(keyStart, keyStart + keyLen) === key) { + return true; + } + } + return false; + } + + /** + * gets first found value with associated key, returning undefined if key does not exist + * @returns string | undefined + */ + get(key: string): string | undefined { + for (const [keyStart, keyLen, valueStart, valueLen] of this.iterByKeyValuePos()) { + if (this.source.slice(keyStart, keyStart + keyLen) === key) { + return valueStart >= 0 ? this.source.slice(valueStart, valueStart + valueLen) : ''; + } + } + return undefined; + } + + /** + * Inserts new key,value pair into parameter + * @returns void + */ + insert(key: string, value: string): void { + // Remove any existing instances of the key + this.remove(key); + + // Add new key-value pair + if (this.source && !this.source.endsWith(';') && this.source.length > 0) { + this.source += ';'; + } + this.source += `${key}=${value}`; + } + + /** + * extends this Parameters with the value of other parameters, overwriting `this` if keys match. + * @returns void + */ + extend(other: IntoParameters): void { + const otherParams = new Parameters(other); + for (const [keyStart, keyLen, valueStart, valueLen] of otherParams.iterByKeyValuePos()) { + const key = otherParams.source.slice(keyStart, keyStart + keyLen); + const value = valueStart >= 0 ? + otherParams.source.slice(valueStart, valueStart + valueLen) : + ''; + this.insert(key, value); + } + } + + /** + * returns the string representation of the parameters + * @returns string + */ + toString(): string { + return this.source; + } } @@ -517,59 +491,44 @@ export class Parameters { * */ export class ReplyError { - /** - * Payload of Error Reply - * @returns ZBytes - */ - payload(): ZBytes { - return this.payload_; - } - - /** - * Encoding of Error Reply - * @returns Encoding - */ - encoding(): Encoding { - return this.encoding_; - } - - /** - * @internal - */ - constructor(private payload_: ZBytes, private encoding_: Encoding) {} + /** + * Payload of Error Reply + * @returns ZBytes + */ + payload(): ZBytes { + return this.payload_; + } + + /** + * Encoding of Error Reply + * @returns Encoding + */ + encoding(): Encoding { + return this.encoding_; + } + + /** + * @internal + */ + constructor(private payload_: ZBytes, private encoding_: Encoding) { } } /** * Reply object from a zenoh `get` */ export class Reply { - /** - * Payload of Error Reply - * @returns Sample or ReplyError - */ - result(): Sample | ReplyError { - return this.result_; - } - - /** - * @internal - */ - constructor(private result_: Sample | ReplyError) {} -} + /** + * Payload of Error Reply + * @returns Sample or ReplyError + */ + result(): Sample | ReplyError { + return this.result_; + } -/** - * Convenience function to convert between Reply and ReplyWS - */ -export function replyFromReplyWS(replyWS: ReplyWS) { - if ("Ok" in replyWS.result) { - let sampleWS = replyWS.result["Ok"]; - let sample = sampleFromSampleWS(sampleWS); - return new Reply(sample); - } else { - let sampleWSEerr: ReplyErrorWS = replyWS.result["Err"]; - let replyError = new ReplyError(sampleWSEerr); - return new Reply(replyError); - } + /** + * @internal + */ + constructor(private result_: Sample | ReplyError) { } } // ███████ ███████ ██ ███████ ██████ ████████ ██████ ██████ @@ -580,69 +539,83 @@ export function replyFromReplyWS(replyWS: ReplyWS) { -export type IntoSelector = Selector | IntoKeyExpr | String | string; +export type IntoSelector = Selector | KeyExpr | [KeyExpr, Parameters] | String | string; /** * Selector class, holding a key expression and optional Parameters * in the following format `?` * example: `demo/key/expr?arg1=lol;arg2=hi` */ export class Selector { - // KeyExpr object - private keyExpr_: KeyExpr; - - // Optional : parameter field - private parameters_?: Parameters; - - /** - * gets Key Expression part of Selector - * @returns KeyExpr - */ - keyExpr(): KeyExpr { - return this.keyExpr_; - } - - /** - * gets Parameters part of Selector - * @returns Parameters - */ - parameters(): Parameters { - if (this.parameters_ == undefined) { - return new Parameters(""); - } else { - return this.parameters_; - } - } - - toString(): string { - if (this.parameters_ != undefined) { - return this.keyExpr_.toString() + "?" + this.parameters_?.toString() - } else { - return this.keyExpr_.toString() - } - } - - /** - * New Function to create a selector from Selector / KeyExpr and Parameters - * @returns Selector - */ - constructor(selector: IntoSelector, parameters?: IntoParameters) { - let keyExpr: KeyExpr; - if (selector instanceof Selector) { - this.keyExpr_ = selector.keyExpr_; - this.parameters_ = selector.parameters_; - return; - } else if (selector instanceof KeyExpr) { - keyExpr = selector; - } else { - keyExpr = new KeyExpr(selector); - } - this.keyExpr_ = keyExpr; - - if (parameters == undefined) { - this.parameters_ = new Parameters("") - } else { - this.parameters_ = new Parameters(parameters); - } - } + // KeyExpr object + private keyExpr_: KeyExpr; + + // Optional : parameter field + private parameters_?: Parameters; + + /** + * gets Key Expression part of Selector + * @returns KeyExpr + */ + keyExpr(): KeyExpr { + return this.keyExpr_; + } + + /** + * gets Parameters part of Selector + * @returns Parameters + */ + parameters(): Parameters { + if (this.parameters_ == undefined) { + return new Parameters(""); + } else { + return this.parameters_; + } + } + + toString(): string { + if (this.parameters_ != undefined) { + return this.keyExpr_.toString() + "?" + this.parameters_?.toString() + } else { + return this.keyExpr_.toString() + } + } + + + + /** + * New Function to create a selector from Selector / KeyExpr and Parameters + * @returns Selector + */ + static from(selector: IntoSelector): Selector { + if (selector instanceof Selector) { + return selector; + } else if (selector instanceof KeyExpr) { + return new Selector(selector, undefined); + } else if (Array.isArray(selector)) { + return new Selector(selector[0], selector[1]); + } else { + let splitString = selector.split("?") + if (splitString.length == 1) { + return new Selector(new KeyExpr(selector)); + } else if (splitString.length == 2 && splitString[0] != undefined && splitString[1] != undefined) { + return new Selector(new KeyExpr(splitString[0]), new Parameters(splitString[1])); + } else { + throw "Error: Invalid Selector, expected format ?"; + } + } + } + + /** + * New Function to create a selector from Selector / KeyExpr and Parameters + * @returns Selector + */ + constructor(keyexpr: KeyExpr, parameters?: IntoParameters) { + this.keyExpr_ = keyexpr; + if (parameters == undefined) { + this.parameters_ = new Parameters("") + } else { + this.parameters_ = new Parameters(parameters); + } + } } \ No newline at end of file diff --git a/zenoh-ts/src/remote_api/interface/B64String.ts b/zenoh-ts/src/remote_api/interface/B64String.ts deleted file mode 100644 index fbd7b470..00000000 --- a/zenoh-ts/src/remote_api/interface/B64String.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type B64String = string; diff --git a/zenoh-ts/src/remote_api/interface/ControlMsg.ts b/zenoh-ts/src/remote_api/interface/ControlMsg.ts deleted file mode 100644 index 6456520f..00000000 --- a/zenoh-ts/src/remote_api/interface/ControlMsg.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { B64String } from "./B64String.js"; -import type { LivelinessMsg } from "./LivelinessMsg.js"; -import type { OwnedKeyExprWrapper } from "./OwnedKeyExprWrapper.js"; - -export type ControlMsg = "OpenSession" | "CloseSession" | { "Session": string } | { "NewTimestamp": string } | "SessionInfo" | { "Get": { key_expr: OwnedKeyExprWrapper, parameters: string | null, id: string, consolidation: number | undefined, timeout: number | undefined, congestion_control: number | undefined, priority: number | undefined, target: number | undefined, express: boolean | undefined, encoding: string | undefined, payload: string | undefined, attachment: string | undefined, } } | { "GetFinished": { id: string, } } | { "Put": { key_expr: OwnedKeyExprWrapper, payload: B64String, encoding: string | undefined, congestion_control: number | undefined, priority: number | undefined, express: boolean | undefined, attachment: string | undefined, timestamp: string | undefined, } } | { "Delete": { key_expr: OwnedKeyExprWrapper, congestion_control: number | undefined, priority: number | undefined, express: boolean | undefined, attachment: string | undefined, timestamp: string | undefined, } } | { "DeclareSubscriber": { key_expr: OwnedKeyExprWrapper, id: string, } } | { "Subscriber": string } | { "UndeclareSubscriber": string } | { "DeclarePublisher": { key_expr: OwnedKeyExprWrapper, encoding: string | undefined, congestion_control: number | undefined, priority: number | undefined, reliability: number | undefined, express: boolean | undefined, id: string, } } | { "UndeclarePublisher": string } | { "DeclareQueryable": { key_expr: OwnedKeyExprWrapper, id: string, complete: boolean, } } | { "UndeclareQueryable": string } | { "DeclareQuerier": { id: string, key_expr: OwnedKeyExprWrapper, target: number | undefined, timeout: number | undefined, accept_replies: number | undefined, allowed_destination: number | undefined, congestion_control: number | undefined, priority: number | undefined, consolidation: number | undefined, express: boolean | undefined, } } | { "UndeclareQuerier": string } | { "QuerierGet": { querier_id: string, get_id: string, parameters: string | undefined, encoding: string | undefined, payload: string | undefined, attachment: string | undefined, } } | { "Liveliness": LivelinessMsg }; diff --git a/zenoh-ts/src/remote_api/interface/DataMsg.ts b/zenoh-ts/src/remote_api/interface/DataMsg.ts deleted file mode 100644 index 7286e6c8..00000000 --- a/zenoh-ts/src/remote_api/interface/DataMsg.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { B64String } from "./B64String.js"; -import type { QueryableMsg } from "./QueryableMsg.js"; -import type { ReplyWS } from "./ReplyWS.js"; -import type { SampleWS } from "./SampleWS.js"; -import type { SessionInfo } from "./SessionInfo.js"; - -export type DataMsg = { "PublisherPut": { id: string, payload: B64String, attachment: B64String | null, encoding: string | null, timestamp: string | null, } } | { "PublisherDelete": { id: string, attachment: B64String | null, timestamp: string | null, } } | { "Sample": [SampleWS, string] } | { "GetReply": ReplyWS } | { "SessionInfo": SessionInfo } | { "NewTimestamp": { id: string, string_rep: string, millis_since_epoch: bigint, } } | { "Queryable": QueryableMsg }; diff --git a/zenoh-ts/src/remote_api/interface/LivelinessMsg.ts b/zenoh-ts/src/remote_api/interface/LivelinessMsg.ts deleted file mode 100644 index c00693be..00000000 --- a/zenoh-ts/src/remote_api/interface/LivelinessMsg.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { OwnedKeyExprWrapper } from "./OwnedKeyExprWrapper.js"; - -export type LivelinessMsg = { "DeclareToken": { key_expr: OwnedKeyExprWrapper, id: string, } } | { "UndeclareToken": string } | { "DeclareSubscriber": { key_expr: OwnedKeyExprWrapper, id: string, history: boolean, } } | { "UndeclareSubscriber": string } | { "Get": { key_expr: OwnedKeyExprWrapper, id: string, timeout: number | undefined, } }; diff --git a/zenoh-ts/src/remote_api/interface/OwnedKeyExprWrapper.ts b/zenoh-ts/src/remote_api/interface/OwnedKeyExprWrapper.ts deleted file mode 100644 index aea479d1..00000000 --- a/zenoh-ts/src/remote_api/interface/OwnedKeyExprWrapper.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type OwnedKeyExprWrapper = string; diff --git a/zenoh-ts/src/remote_api/interface/QueryReplyVariant.ts b/zenoh-ts/src/remote_api/interface/QueryReplyVariant.ts deleted file mode 100644 index 41cc2bf4..00000000 --- a/zenoh-ts/src/remote_api/interface/QueryReplyVariant.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { B64String } from "./B64String.js"; -import type { OwnedKeyExprWrapper } from "./OwnedKeyExprWrapper.js"; - -export type QueryReplyVariant = { "Reply": { key_expr: OwnedKeyExprWrapper, payload: B64String, encoding: string | null, priority: number, congestion_control: number, express: boolean, timestamp: string | null, attachment: B64String | null, } } | { "ReplyErr": { payload: B64String, encoding: string | null, } } | { "ReplyDelete": { key_expr: OwnedKeyExprWrapper, priority: number, congestion_control: number, express: boolean, timestamp: string | null, attachment: B64String | null, } }; diff --git a/zenoh-ts/src/remote_api/interface/QueryReplyWS.ts b/zenoh-ts/src/remote_api/interface/QueryReplyWS.ts deleted file mode 100644 index 783431e9..00000000 --- a/zenoh-ts/src/remote_api/interface/QueryReplyWS.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { QueryReplyVariant } from "./QueryReplyVariant.js"; - -export type QueryReplyWS = { query_uuid: string, result: QueryReplyVariant, }; diff --git a/zenoh-ts/src/remote_api/interface/QueryWS.ts b/zenoh-ts/src/remote_api/interface/QueryWS.ts deleted file mode 100644 index 27dc78d1..00000000 --- a/zenoh-ts/src/remote_api/interface/QueryWS.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { OwnedKeyExprWrapper } from "./OwnedKeyExprWrapper.js"; - -export type QueryWS = { query_uuid: string, key_expr: OwnedKeyExprWrapper, parameters: string, encoding: string | null, attachment: string | undefined, payload: string | undefined, }; -export type QueryCallback = (query: QueryWS) => void; \ No newline at end of file diff --git a/zenoh-ts/src/remote_api/interface/QueryableMsg.ts b/zenoh-ts/src/remote_api/interface/QueryableMsg.ts deleted file mode 100644 index e2b996f6..00000000 --- a/zenoh-ts/src/remote_api/interface/QueryableMsg.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { QueryReplyWS } from "./QueryReplyWS.js"; -import type { QueryWS } from "./QueryWS.js"; - -export type QueryableMsg = { "Query": { queryable_uuid: string, query: QueryWS, } } | { "Reply": { reply: QueryReplyWS, } }; diff --git a/zenoh-ts/src/remote_api/interface/RemoteAPIMsg.ts b/zenoh-ts/src/remote_api/interface/RemoteAPIMsg.ts deleted file mode 100644 index 13b0bf23..00000000 --- a/zenoh-ts/src/remote_api/interface/RemoteAPIMsg.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ControlMsg } from "./ControlMsg.js"; -import type { DataMsg } from "./DataMsg.js"; - -export type RemoteAPIMsg = { "Data": DataMsg } | { "Control": ControlMsg }; diff --git a/zenoh-ts/src/remote_api/interface/ReplyErrorWS.ts b/zenoh-ts/src/remote_api/interface/ReplyErrorWS.ts deleted file mode 100644 index 19c86a61..00000000 --- a/zenoh-ts/src/remote_api/interface/ReplyErrorWS.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { B64String } from "./B64String.js"; - -export type ReplyErrorWS = { payload: B64String, encoding: string, }; diff --git a/zenoh-ts/src/remote_api/interface/ReplyWS.ts b/zenoh-ts/src/remote_api/interface/ReplyWS.ts deleted file mode 100644 index 5335d5da..00000000 --- a/zenoh-ts/src/remote_api/interface/ReplyWS.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReplyErrorWS } from "./ReplyErrorWS.js"; -import type { SampleWS } from "./SampleWS.js"; - -export type ReplyWS = { query_uuid: string, result: { Ok : SampleWS } | { Err : ReplyErrorWS }, }; -export type ReplyCallback = (reply: ReplyWS) => void; diff --git a/zenoh-ts/src/remote_api/interface/SampleKindWS.ts b/zenoh-ts/src/remote_api/interface/SampleKindWS.ts deleted file mode 100644 index ac535708..00000000 --- a/zenoh-ts/src/remote_api/interface/SampleKindWS.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SampleKindWS = "Put" | "Delete"; diff --git a/zenoh-ts/src/remote_api/interface/SampleWS.ts b/zenoh-ts/src/remote_api/interface/SampleWS.ts deleted file mode 100644 index 1625c790..00000000 --- a/zenoh-ts/src/remote_api/interface/SampleWS.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { B64String } from "./B64String.js"; -import type { OwnedKeyExprWrapper } from "./OwnedKeyExprWrapper.js"; -import type { SampleKindWS } from "./SampleKindWS.js"; - -export type SampleWS = { key_expr: OwnedKeyExprWrapper, value: B64String, kind: SampleKindWS, encoding: string, timestamp: string | null, congestion_control: number, priority: number, express: boolean, attachement: B64String | null, }; -export type SampleCallback = (sample: SampleWS) => void; \ No newline at end of file diff --git a/zenoh-ts/src/remote_api/interface/SessionInfo.ts b/zenoh-ts/src/remote_api/interface/SessionInfo.ts deleted file mode 100644 index 3407f686..00000000 --- a/zenoh-ts/src/remote_api/interface/SessionInfo.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SessionInfo = { zid: string, z_routers: Array, z_peers: Array, }; diff --git a/zenoh-ts/src/remote_api/interface/types.ts b/zenoh-ts/src/remote_api/interface/types.ts deleted file mode 100644 index bc4ebfa3..00000000 --- a/zenoh-ts/src/remote_api/interface/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) 2025 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -class EncodingBase { - constructor(protected readonly id: number, protected readonly schema: String | undefined) {} - - -} \ No newline at end of file diff --git a/zenoh-ts/src/remote_api/pubsub.ts b/zenoh-ts/src/remote_api/pubsub.ts deleted file mode 100644 index 139b4388..00000000 --- a/zenoh-ts/src/remote_api/pubsub.ts +++ /dev/null @@ -1,142 +0,0 @@ -// -// Copyright (c) 2024 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -import { encode as b64_str_from_bytes } from "base64-arraybuffer"; - -// Import interface -import { DataMsg } from "./interface/DataMsg.js"; -import { ControlMsg } from "./interface/ControlMsg.js"; - -// Remote Api -import { RemoteSession, UUIDv4 } from "./session.js"; - -// ██████ ███████ ███ ███ ██████ ████████ ███████ ██████ ██ ██ ██████ ██ ██ ███████ ██ ██ ███████ ██████ -// ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██████ █████ ██ ████ ██ ██ ██ ██ █████ ██████ ██ ██ ██████ ██ ██ ███████ ███████ █████ ██████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ███████ ██ ██ ██████ ██ ███████ ██ ██████ ██████ ███████ ██ ███████ ██ ██ ███████ ██ ██ - -export class RemotePublisher { - private undeclared: boolean = false; - constructor( - private keyExpr: String, - private publisherId: UUIDv4, - private sessionRef: RemoteSession, - ) {} - - async put( - payload: Array, - attachment: Array | null, - encoding: string | null, - timestamp: string | null, - ) { - if (this.undeclared == true) { - let message = - "Publisher keyexpr:`" + - this.keyExpr + - "` id:`" + - this.publisherId + - "` already undeclared"; - console.warn(message); - return; - } - - let optionalAttachment = null; - if (attachment != null) { - optionalAttachment = b64_str_from_bytes(new Uint8Array(attachment)); - } - - let dataMsg: DataMsg = { - PublisherPut: { - id: this.publisherId.toString(), - payload: b64_str_from_bytes(new Uint8Array(payload)), - attachment: optionalAttachment, - encoding: encoding, - timestamp: timestamp - }, - }; - await this.sessionRef.sendDataMessage(dataMsg); - } - - // Delete - async delete( - attachment: Array | null, - timestamp: string | null, - ) { - - let optionalAttachment = null; - if (attachment != null) { - optionalAttachment = b64_str_from_bytes(new Uint8Array(attachment)); - } - - let dataMsg: DataMsg = { - PublisherDelete: { - id: this.publisherId.toString(), - attachment: optionalAttachment, - timestamp: timestamp, - }, - }; - await this.sessionRef.sendDataMessage(dataMsg); - } - - async undeclare() { - if (this.undeclared == true) { - let message = - "Publisher keyexpr:`" + - this.keyExpr + - "` id:`" + - this.publisherId + - "` already undeclared"; - console.warn(message); - return; - } - this.undeclared = true; - let ctrlMessage: ControlMsg = { - UndeclarePublisher: this.publisherId.toString(), - }; - await this.sessionRef.sendCtrlMessage(ctrlMessage); - } -} - -// ██████ ███████ ███ ███ ██████ ████████ ███████ ███████ ██ ██ ██████ ███████ ██████ ██████ ██ ██████ ███████ ██████ -// ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██████ █████ ██ ████ ██ ██ ██ ██ █████ ███████ ██ ██ ██████ ███████ ██ ██████ ██ ██████ █████ ██████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ███████ ██ ██ ██████ ██ ███████ ███████ ██████ ██████ ███████ ██████ ██ ██ ██ ██████ ███████ ██ ██ - -// If defined with a Callback, All samples passed to the Callback, -// else, must call receive on the -export class RemoteSubscriber { - constructor( - private keyExpr: String, - private subscriberId: UUIDv4, - private sessionRef: RemoteSession, - ) {} - - async undeclare() { - if (!this.sessionRef.undeclareSubscriber(this.subscriberId)) { - console.warn("Subscriber keyexpr:`" + - this.keyExpr + - "` id:`" + - this.subscriberId + - "` already closed"); - return; - } - - let ctrlMessage: ControlMsg = { - UndeclareSubscriber: this.subscriberId.toString(), - }; - await this.sessionRef.sendCtrlMessage(ctrlMessage); - } -} diff --git a/zenoh-ts/src/remote_api/querier.ts b/zenoh-ts/src/remote_api/querier.ts deleted file mode 100644 index 360dac07..00000000 --- a/zenoh-ts/src/remote_api/querier.ts +++ /dev/null @@ -1,73 +0,0 @@ -// -// Copyright (c) 2024 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -import { v4 as uuidv4 } from "uuid"; -import { RemoteSession, UUIDv4 } from "./session.js"; -import { ControlMsg } from "./interface/ControlMsg.js" -import { encode as b64_str_from_bytes } from "base64-arraybuffer"; -import { ReplyCallback } from "./interface/ReplyWS.js"; -import { Drop } from "./closure.js"; - -export class RemoteQuerier { - constructor( - private querierId: UUIDv4, - private sessionRef: RemoteSession, - ) {} - - async undeclare() { - - let controlMsg: ControlMsg = { - UndeclareQuerier: this.querierId as string - }; - - await this.sessionRef.sendCtrlMessage(controlMsg); - } - - async get( - callback: ReplyCallback, - drop: Drop, - encoding?: string, - parameters?: string, - attachment?: Array, - payload?: Array, - ) { - let getId = uuidv4(); - this.sessionRef.getReceivers.set(getId, { callback, drop }); - - let payloadStr = undefined; - if (payload != undefined) { - payloadStr = b64_str_from_bytes(new Uint8Array(payload)) - } - let attachmentStr = undefined; - if (attachment != undefined) { - attachmentStr = b64_str_from_bytes(new Uint8Array(attachment)) - } - - let controlMsg: ControlMsg = { - QuerierGet: { - querier_id: this.querierId as string, - get_id: getId, - parameters: parameters, - encoding: encoding, - payload: payloadStr, - attachment: attachmentStr, - } - }; - - await this.sessionRef.sendCtrlMessage(controlMsg); - } - -} - - diff --git a/zenoh-ts/src/remote_api/query.ts b/zenoh-ts/src/remote_api/query.ts deleted file mode 100644 index fe10cf01..00000000 --- a/zenoh-ts/src/remote_api/query.ts +++ /dev/null @@ -1,50 +0,0 @@ -// -// Copyright (c) 2024 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -// Import interface -import { ControlMsg } from "./interface/ControlMsg.js"; - -// Remote Api -import { RemoteSession, UUIDv4 } from "./session.js"; - -// ██████ ███████ ███ ███ ██████ ████████ ███████ ██████ ██ ██ ███████ ██████ ██ ██ █████ ██████ ██ ███████ -// ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██████ █████ ██ ████ ██ ██ ██ ██ █████ ██ ██ ██ ██ █████ ██████ ████ ███████ ██████ ██ █████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ███████ ██ ██ ██████ ██ ███████ ██████ ██████ ███████ ██ ██ ██ ██ ██ ██████ ███████ ███████ -// ▀▀ - -export class RemoteQueryable { - constructor( - private keyExpr: String, - private queryableId: UUIDv4, - private sessionRef: RemoteSession, - ) {} - - async undeclare() { - if (!this.sessionRef.undeclareQueryable(this.queryableId.toString())) { - console.warn("Queryable keyexpr:`" + - this.keyExpr + - "` id:`" + - this.queryableId + - "` already closed"); - return; - } - - let ctrlMessage: ControlMsg = { - UndeclareQueryable: this.queryableId.toString(), - }; - await this.sessionRef.sendCtrlMessage(ctrlMessage); - } -} diff --git a/zenoh-ts/src/remote_api/session.ts b/zenoh-ts/src/remote_api/session.ts deleted file mode 100644 index c2c601fc..00000000 --- a/zenoh-ts/src/remote_api/session.ts +++ /dev/null @@ -1,709 +0,0 @@ -// -// Copyright (c) 2024 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -import { v4 as uuidv4 } from "uuid"; -import { Logger } from "tslog"; -import { encode as b64_str_from_bytes } from "base64-arraybuffer"; - -const log = new Logger({ stylePrettyLogs: false }); - -// Import interface -import { RemoteAPIMsg } from "./interface/RemoteAPIMsg.js"; -import { SampleWS, SampleCallback } from "./interface/SampleWS.js"; -import { DataMsg } from "./interface/DataMsg.js"; -import { ControlMsg } from "./interface/ControlMsg.js"; -import { OwnedKeyExprWrapper } from "./interface/OwnedKeyExprWrapper.js"; -import { QueryCallback, QueryWS } from "./interface/QueryWS.js"; -import { RemotePublisher, RemoteSubscriber } from "./pubsub.js"; -import { RemoteQueryable } from "./query.js"; -import { ReplyCallback, ReplyWS } from "./interface/ReplyWS.js"; -import { QueryableMsg } from "./interface/QueryableMsg.js"; -import { SessionInfo as SessionInfoIface } from "./interface/SessionInfo.js"; -import { RemoteQuerier } from "./querier.js" -import { B64String } from "./interface/B64String.js"; -import { QueryReplyVariant } from "./interface/QueryReplyVariant.js"; -import { RemoteLink } from "./link.js"; -import { Closure, Drop } from "./closure.js"; - - -// ██████ ███████ ███ ███ ██████ ████████ ███████ ███████ ███████ ███████ ███████ ██ ██████ ███ ██ -// ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ -// ██████ █████ ██ ████ ██ ██ ██ ██ █████ ███████ █████ ███████ ███████ ██ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ███████ ██ ██ ██████ ██ ███████ ███████ ███████ ███████ ███████ ██ ██████ ██ ████ - -export interface TimestampIface { - id: string, - // eslint-disable-next-line @typescript-eslint/naming-convention - string_rep: string, - // eslint-disable-next-line @typescript-eslint/naming-convention - millis_since_epoch: bigint -} - -export enum RemoteRecvErr { - Disconnected, -} - -type JSONMessage = string; -/** - * @ignore - */ -export type UUIDv4 = String | string; - -export class RemoteSession { - link: RemoteLink; - session: UUIDv4 | null; - subscribers: Map>; - queryables: Map>; - getReceivers: Map>; - livelinessSubscribers: Map>; - livelinessGetReceivers: Map>; - pendingQueries: Set; - sessionInfo: SessionInfoIface | null; - newTimestamp_: TimestampIface | null; - - private constructor(link: RemoteLink) { - this.link = link; - this.session = null; - this.subscribers = new Map>(); - this.queryables = new Map>(); - this.getReceivers = new Map>(); - this.livelinessSubscribers = new Map>(); - this.livelinessGetReceivers = new Map>(); - this.pendingQueries = new Set; - this.sessionInfo = null; - this.newTimestamp_ = null; - } - - // - // Initialize Class - // - static async new(locator: string): Promise { - let link = await RemoteLink.new(locator); - let session = new RemoteSession(link); - session.link.onmessage((msg: any) => { session.onMessageReceived(msg); }); - - let open: ControlMsg = "OpenSession"; - await session.sendCtrlMessage(open); - while (session.session == null) { - await sleep(10); - } - console.log("Successfully opened session:", session.session); - - return session; - } - - // - // Zenoh Session Functions - // - // Info - async info(): Promise { - let ctrlMessage: ControlMsg = "SessionInfo"; - this.sessionInfo = null; - await this.sendCtrlMessage(ctrlMessage); - - while (this.sessionInfo === null) { - await sleep(10); - } - return this.sessionInfo; - } - - // Put - async put(keyExpr: string, - payload: Array, - encoding?: string, - congestionControl?: number, - priority?: number, - express?: boolean, - attachment?: Array, - timestamp?:string, - ) { - let ownedKeyexpr: OwnedKeyExprWrapper = keyExpr; - - let optAttachment = undefined; - if (attachment != undefined) { - optAttachment = b64_str_from_bytes(new Uint8Array(attachment)) - } - - let ctrlMessage: ControlMsg = { - Put: { - key_expr: ownedKeyexpr, - payload: b64_str_from_bytes(new Uint8Array(payload)), - encoding: encoding, - congestion_control: congestionControl, - priority: priority, - express: express, - attachment: optAttachment, - timestamp: timestamp - }, - }; - await this.sendCtrlMessage(ctrlMessage); - } - - // get - async get( - keyExpr: string, - parameters: string | null, - callback: ReplyCallback, - drop: Drop, - consolidation?: number, - congestionControl?: number, - priority?: number, - express?: boolean, - target?: number, - encoding?: string, - payload?: Array, - attachment?: Array, - timeoutMs?: number, - ) { - let uuid = uuidv4(); - this.getReceivers.set(uuid, {callback, drop}); - - let optPayload = undefined; - if (payload != undefined) { - optPayload = b64_str_from_bytes(new Uint8Array(payload)) - } - let optAttachment = undefined; - if (attachment != undefined) { - optAttachment = b64_str_from_bytes(new Uint8Array(attachment)) - } - - let controlMessage: ControlMsg = { - Get: { - key_expr: keyExpr, - parameters: parameters, - id: uuid, - consolidation: consolidation, - congestion_control: congestionControl, - priority: priority, - express: express, - target: target, - encoding: encoding, - timeout: timeoutMs, - payload: optPayload, - attachment: optAttachment - }, - }; - await this.sendCtrlMessage(controlMessage); - } - - // delete - async delete( - keyExpr: string, - congestionControl?: number, - priority?: number, - express?: boolean, - attachment?: Array, - timestamp?: string, - ) { - let ownedKeyexpr: OwnedKeyExprWrapper = keyExpr; - let optAttachment = undefined; - if (attachment != undefined) { - optAttachment = b64_str_from_bytes(new Uint8Array(attachment)) - } - let dataMessage: ControlMsg = { - Delete: { - key_expr: ownedKeyexpr, - congestion_control: congestionControl, - priority: priority, - express: express, - attachment: optAttachment, - timestamp: timestamp - } - }; - await this.sendCtrlMessage(dataMessage); - } - - async replyFinal(queryUuid: UUIDv4): Promise { - return this.pendingQueries.delete(queryUuid); - } - - async reply(uuid: UUIDv4, - keyExpr: string, - payload: Uint8Array, - encoding: string | null, - congestionControl: number, - priority: number, - express: boolean, - attachment: Uint8Array | null, - timestamp: string | null - ): Promise { - if (!this.pendingQueries.has(uuid)) { - console.warn("Attempt to reply to unknown query:", uuid); - return false; - } - - let optAttachment = null; - if (attachment != undefined) { - optAttachment = b64_str_from_bytes(attachment) - } - - let qrVariant: QueryReplyVariant = { - Reply: { - key_expr: keyExpr.toString(), - payload: b64_str_from_bytes(payload), - encoding: encoding, - priority: priority, - congestion_control: congestionControl, - express: express, - timestamp: timestamp, - attachment: optAttachment - }, - }; - - let queryableMsg: QueryableMsg = { Reply: { reply: {query_uuid: uuid.toString(), result: qrVariant} } }; - let dataMsg: DataMsg = { Queryable: queryableMsg }; - await this.sendDataMessage(dataMsg); - - return await this.replyFinal(uuid); - } - - async replyErr(uuid: UUIDv4, payload: Uint8Array, encoding: string | null): Promise { - if (!this.pendingQueries.has(uuid)) { - return false; - } - - let qrVariant: QueryReplyVariant = { - ReplyErr: { - payload: b64_str_from_bytes(payload), - encoding: encoding, - }, - }; - - let queryableMsg: QueryableMsg = { Reply: { reply: {query_uuid: uuid.toString(), result: qrVariant} } }; - let dataMsg: DataMsg = { Queryable: queryableMsg }; - await this.sendDataMessage(dataMsg); - - return await this.replyFinal(uuid); - } - - async replyDel(uuid: UUIDv4, - keyExpr: string, - congestionControl: number, - priority: number, - express: boolean, - attachment: Uint8Array | null, - timestamp: string | null): Promise { - if (!this.pendingQueries.has(uuid)) { - return false; - } - - let optAttachment : B64String | null = null; - if (attachment != undefined) { - optAttachment = b64_str_from_bytes(attachment); - } - - let qrVariant: QueryReplyVariant = { - ReplyDelete: { - key_expr: keyExpr, - priority: priority, - congestion_control: congestionControl, - express: express, - timestamp: timestamp, - attachment: optAttachment - }, - }; - - let queryableMsg: QueryableMsg = { Reply: { reply: {query_uuid: uuid.toString(), result: qrVariant} } }; - let dataMsg: DataMsg = { Queryable: queryableMsg }; - await this.sendDataMessage(dataMsg); - - return await this.replyFinal(uuid); -} - - async close() { - let dataMessage: ControlMsg = "CloseSession"; - await this.sendCtrlMessage(dataMessage); - this.link.close(); - - this.pendingQueries.clear(); - for (let v of this.subscribers.values()) { - v.drop(); - } - this.subscribers.clear(); - - for (let v of this.livelinessSubscribers.values()) { - v.drop(); - } - this.livelinessSubscribers.clear(); - - for (let v of this.getReceivers.values()) { - v.drop(); - } - this.getReceivers.clear(); - - for (let v of this.livelinessGetReceivers.values()) { - v.drop(); - } - - for (let v of this.queryables.values()) { - v.drop(); - } - this.queryables.clear(); - } - - async declareRemoteSubscriber( - keyExpr: string, - callback: SampleCallback, - drop: Drop, - ): Promise { - let uuid = uuidv4(); - - let controlMessage: ControlMsg = { - DeclareSubscriber: { key_expr: keyExpr, id: uuid}, - }; - - this.subscribers.set(uuid, {callback, drop}); - - await this.sendCtrlMessage(controlMessage); - - let subscriber = new RemoteSubscriber( - keyExpr, - uuid, - this - ); - return subscriber; - } - - - async declareRemoteQueryable( - keyExpr: string, - complete: boolean, - callback: QueryCallback, - drop: Drop - ): Promise { - let uuid = uuidv4(); - - let controlMessage: ControlMsg = { - DeclareQueryable: { key_expr: keyExpr, complete: complete, id: uuid}, - }; - - this.queryables.set(uuid, {callback, drop}); - - await this.sendCtrlMessage(controlMessage); - - let queryable = new RemoteQueryable( - keyExpr, - uuid, - this, - ); - - return queryable; - } - - async declareRemotePublisher( - keyExpr: string, - encoding?: string, - congestionControl?: number, - priority?: number, - express?: boolean, - reliability?: number, - ): Promise { - let uuid: string = uuidv4(); - let publisher = new RemotePublisher(keyExpr, uuid, this); - let controlMessage: ControlMsg = { - DeclarePublisher: { - key_expr: keyExpr, - encoding: encoding, - congestion_control: congestionControl, - priority: priority, - express: express, - reliability: reliability, - id: uuid, - }, - }; - await this.sendCtrlMessage(controlMessage); - return publisher; - } - - async declareRemoteQuerier( - keyExpr: string, - consolidation?: number, - congestionControl?: number, - priority?: number, - express?: boolean, - target?: number, - allowedDestination?: number, - acceptReplies?: number, - timeoutMilliseconds?: number, - ): Promise { - let timeout = undefined; - if (timeoutMilliseconds !== undefined) { - timeout = timeoutMilliseconds; - } - - let uuid: string = uuidv4(); - let querier = new RemoteQuerier(uuid, this); - - let controlMessage: ControlMsg = { - DeclareQuerier: { - id: uuid, - key_expr: keyExpr, - congestion_control: congestionControl, - priority: priority, - express: express, - target: target, - timeout: timeout, - accept_replies: acceptReplies, - allowed_destination: allowedDestination, - consolidation: consolidation, - }, - }; - - await this.sendCtrlMessage(controlMessage); - return querier; - } - - - // Liveliness - async declareLivelinessToken( - keyExpr: string, - ): Promise { - let uuid = uuidv4(); - - let controlMessage: ControlMsg = { - Liveliness: { DeclareToken: { key_expr: keyExpr, id: uuid } } - }; - - await this.sendCtrlMessage(controlMessage); - - return uuid; - } - - async declareLivelinessSubscriber( - keyExpr: string, - history: boolean, - callback: SampleCallback, - drop: Drop - ): Promise { - let uuid = uuidv4(); - - let controlMessage: ControlMsg = { - Liveliness: { DeclareSubscriber: { key_expr: keyExpr, id: uuid, history: history } } - }; - - this.livelinessSubscribers.set(uuid, {callback, drop}); - - await this.sendCtrlMessage(controlMessage); - - let subscriber = new RemoteSubscriber( - keyExpr, - uuid, - this, - ); - - return subscriber; - } - - async getLiveliness( - keyExpr: string, - callback: ReplyCallback, - drop: Drop, - timeoutMilliseconds?: number - ) { - let uuid = uuidv4(); - - let timeout = undefined; - if (timeoutMilliseconds !== undefined) { - timeout = timeoutMilliseconds; - } - - let controlMessage: ControlMsg = { - Liveliness: { Get: { key_expr: keyExpr, id: uuid, timeout: timeout } } - }; - - this.livelinessGetReceivers.set(uuid, {callback, drop}); - - await this.sendCtrlMessage(controlMessage); - } - - // Note: This method blocks until Timestamp has been created - // The correct way to do this would be with a request / response - async newTimestamp(): Promise { - let uuid = uuidv4(); - let controlMessage: ControlMsg = { "NewTimestamp": uuid }; - this.newTimestamp_ = null; - await this.sendCtrlMessage(controlMessage); - while (this.newTimestamp_ === null) { - await sleep(10); - } - return this.newTimestamp_; - } - - // - // Sending Messages - // - async sendDataMessage(dataMessage: DataMsg) { - let remoteApiMmessage: RemoteAPIMsg = { Data: dataMessage }; - await this.sendRemoteApiMessage(remoteApiMmessage); - } - - async sendCtrlMessage(ctrlMessage: ControlMsg) { - let remoteApiMessage: RemoteAPIMsg = { Control: ctrlMessage }; - await this.sendRemoteApiMessage(remoteApiMessage); - } - - private async sendRemoteApiMessage(remoteApiMessage: RemoteAPIMsg) { - await this.link.send(JSON.stringify(remoteApiMessage)); - } - - // - // Manage Session and handle messages - // - private onMessageReceived(message: JSONMessage) { - let remoteApiMessage: RemoteAPIMsg = JSON.parse( - message, - ) as RemoteAPIMsg; - if ("Session" in remoteApiMessage) { - console.warn("Continue Ignore Session Messages"); - } else if ("Control" in remoteApiMessage) { - this.handleControlMessage(remoteApiMessage["Control"]); - } else if ("Data" in remoteApiMessage) { - this.handleDataMessage(remoteApiMessage["Data"]); - } else { - log.error( - `RemoteAPIMsg Does not contain known Members`, - remoteApiMessage, - ); - } - } - - undeclareQueryable(id: UUIDv4): boolean{ - let handler = this.queryables.get(id); - if (handler != undefined) { - handler.drop(); - this.queryables.delete(id); - return true; - } - return false; - } - - undeclareSubscriber(id: UUIDv4): boolean{ - let handler = this.subscribers.get(id); - if (handler != undefined) { - handler.drop(); - this.subscribers.delete(id); - return true; - } - return false; - } - - undeclareLivelinessSubscriber(id: UUIDv4): boolean{ - let handler = this.livelinessSubscribers.get(id); - if (handler != undefined) { - handler.drop(); - this.livelinessSubscribers.delete(id); - return true; - } - return false; - } - - private removeGetReceiver(id: UUIDv4): boolean{ - let handler = this.getReceivers.get(id); - if (handler != undefined) { - handler.drop(); - this.getReceivers.delete(id); - return true; - } - return false; - } - - private removeLivelinessGetReceiver(id: UUIDv4): boolean{ - let handler = this.livelinessGetReceivers.get(id); - if (handler != undefined) { - handler.drop(); - this.livelinessGetReceivers.delete(id); - return true; - } - return false; - } - - private handleControlMessage(controlMsg: ControlMsg) { - if (typeof controlMsg === "string") { - console.warn("unhandled Control Message:", controlMsg); - } else if (typeof controlMsg === "object") { - if ("Session" in controlMsg) { - this.session = controlMsg["Session"]; - } else if ("GetFinished" in controlMsg) { - let id = controlMsg["GetFinished"].id; - this.removeGetReceiver(id) || this.removeLivelinessGetReceiver(id); - } else if ("UndeclareSubscriber" in controlMsg) { - let subscriberUuid = controlMsg["UndeclareSubscriber"]; - this.undeclareSubscriber(subscriberUuid); - } else if ("UndeclareQueryable" in controlMsg) { - let queryableUuid = controlMsg["UndeclareQueryable"]; - this.undeclareQueryable(queryableUuid); - } - } - } - - private handleDataMessage(dataMsg: DataMsg) { - if ("Sample" in dataMsg) { - let subscriptionUuid: UUIDv4 = dataMsg["Sample"][1]; - - let subscriber = this.subscribers.get(subscriptionUuid) ?? this.livelinessSubscribers.get(subscriptionUuid); - - if (subscriber != undefined) { - let sample: SampleWS = dataMsg["Sample"][0]; - subscriber.callback(sample); - } else { - console.warn("Subscrption UUID not in map", subscriptionUuid); - } - } else if ("GetReply" in dataMsg) { - let getReply: ReplyWS = dataMsg["GetReply"]; - - let receiver = this.getReceivers.get(getReply.query_uuid) ?? this.livelinessGetReceivers.get(getReply.query_uuid); - - if (receiver != undefined) { - receiver.callback(getReply); - } else { - console.warn("Get receiver UUID not in map", getReply.query_uuid); - } - } else if ("Queryable" in dataMsg) { - let queryableMsg: QueryableMsg = dataMsg["Queryable"]; - if ("Query" in queryableMsg) { - let queryableUuid: UUIDv4 = queryableMsg.Query.queryable_uuid; - let queryable = this.queryables.get(queryableUuid); - if (queryable != undefined) { - this.pendingQueries.add(queryableMsg.Query.query.query_uuid); - queryable.callback(queryableMsg.Query.query) - } else { - console.warn("Queryable Message UUID not in map", queryableUuid); - } - } else if ("Reply" in queryableMsg) { - // Server - console.warn("Client should not receive Reply in Queryable Message"); - console.warn("Replies to get queries should come via Get Reply"); - } else { - console.warn("Queryable message Variant not recognized"); - } - } else if ("SessionInfo" in dataMsg) { - let sessionInfo: SessionInfoIface = dataMsg["SessionInfo"]; - this.sessionInfo = sessionInfo; - } else if ("NewTimestamp" in dataMsg) { - let newTimestamp: TimestampIface = dataMsg["NewTimestamp"]; - this.newTimestamp_ = newTimestamp; - } else { - console.warn("Data Message not recognized Expected Variant", dataMsg); - } - } - - isClosed(): boolean { - return !this.link.isOk(); - } -} - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/zenoh-ts/src/session.ts b/zenoh-ts/src/session.ts index 38eb40b6..bd81b272 100644 --- a/zenoh-ts/src/session.ts +++ b/zenoh-ts/src/session.ts @@ -11,144 +11,164 @@ // Contributors: // ZettaScale Zenoh Team, // -// Remote API interface -import { - RemoteSession, - TimestampIface as TimestampIface, -} from "./remote_api/session.js"; -import { ReplyWS } from "./remote_api/interface/ReplyWS.js"; -import { RemotePublisher, RemoteSubscriber } from "./remote_api/pubsub.js"; -import { SampleWS } from "./remote_api/interface/SampleWS.js"; -import { QueryWS } from "./remote_api/interface/QueryWS.js"; -// API interface import { IntoKeyExpr, KeyExpr } from "./key_expr.js"; import { IntoZBytes, ZBytes } from "./z_bytes.js"; import { Liveliness } from "./liveliness.js"; import { - IntoSelector, - Parameters, - Query, - queryFromQueryWS, - Queryable, - Reply, - replyFromReplyWS, - Selector, + IntoSelector, + Query, + Queryable, + Reply, + Selector, } from "./query.js"; import { Publisher, Subscriber } from "./pubsub.js"; -import { - priorityToInt, - congestionControlToInt, - CongestionControl, - Priority, - Sample, - sampleFromSampleWS, - consolidationModeToInt, - ConsolidationMode, - Reliability, - reliabilityToInt, -} from "./sample.js"; import { Config } from "./config.js"; -import { Encoding } from "./encoding.js"; -import { SessionInfo as SessionInfoIface } from "./remote_api/interface/SessionInfo.js"; +import { Encoding, IntoEncoding } from "./encoding.js"; // External deps import { Duration, TimeDuration } from 'typed-duration' -import { localityToInt, Querier, QuerierOptions, queryTargetToInt, QueryTarget, replyKeyExprToInt, ReplyKeyExpr } from "./querier.js"; import { Timestamp } from "./timestamp.js"; -import { ChannelReceiver, FifoChannel, Handler, intoCbDropReceiver } from "./remote_api/channels.js"; - +import { ChannelReceiver, FifoChannel, Handler, intoCbDropReceiver } from "./channels.js"; +import { ZenohId } from "./zid.js"; +import { CongestionControl, ConsolidationMode, Locality, Priority, QueryTarget, Reliability, ReplyKeyExpr } from "./enums.js"; +import { Sample } from "./sample.js"; +import { SessionInner } from "./session_inner.js"; +import { Delete, Put, Qos, QuerierProperties, QuerySettings } from "./message.js"; +import { Querier } from "./querier.js"; + +export const DEFAULT_QUERY_TIMEOUT_MS = 10000; /** - * Options for a Put function - * @prop {Encoding=} encoding - encoding type - * @prop {CongestionControl=} congestion_control - congestion_control applied when routing the data - * @prop {Priority=} priority - priority of the written data - * @prop {boolean=} express - express - * @prop {IntoZBytes=} attachment - Additional Data sent with the request + * Options for a Put operation + * @prop {Encoding=} encoding - Encoding type + * @prop {CongestionControl=} congestionControl - Congestion control applied when routing the data + * @prop {Priority=} priority - Priority of the written data + * @prop {boolean=} express - Express: if set to `true`, this message will not be batched. This usually has a positive impact on latency but negative impact on throughput. + * @prop {Reliability=} reliability - Reliability to apply to data transport, + * @prop {Locality=} allowedDestination - Allowed destination for the data, + * @prop {IntoZBytes=} attachment - Additional Data to send with the request * @prop {Timestamp=} timestamp - Timestamp of the message */ export interface PutOptions { - encoding?: Encoding, - congestionControl?: CongestionControl, - priority?: Priority, - express?: boolean, - attachment?: IntoZBytes - timestamp?: Timestamp, + encoding?: IntoEncoding, + congestionControl?: CongestionControl, + priority?: Priority, + express?: boolean, + reliability?: Reliability, + allowedDestinaton?: Locality, + attachment?: IntoZBytes + timestamp?: Timestamp, } /** - * Options for a Delete function - * @prop {CongestionControl=} congestion_control - congestion_control applied when routing the data - * @prop {Priority=} priority - priority of the written data - * @prop {boolean=} express - Express + * Options for a Delete operation + * @prop {CongestionControl=} congestion_control - Congestion control applied when routing the data + * @prop {Priority=} priority - Prriority of the written data + * @prop {boolean=} express - Express: if set to `true`, this message will not be batched. This usually has a positive impact on latency but negative impact on throughput. + * @prop {Reliability=} reliability - Reliability to apply to data transport + * @prop {Locality=} allowedDestination - Allowed destination for the data * @prop {IntoZBytes=} attachment - Additional Data sent with the request * @prop {Timestamp=} timestamp - Timestamp of the message */ export interface DeleteOptions { - congestionControl?: CongestionControl, - priority?: Priority, - express?: boolean, - attachment?: IntoZBytes - timestamp?: Timestamp + congestionControl?: CongestionControl, + priority?: Priority, + express?: boolean, + reliability?: Reliability, + allowedDestinaton?: Locality, + attachment?: IntoZBytes + timestamp?: Timestamp } /** - * Options for a Get function - * @prop {ConsolidationMode=} consolidation - consolidation mode - * @prop {CongestionControl=} congestion_control - congestion_control applied when routing the data - * @prop {Priority=} priority - priority of the written data - * @prop {boolean=} express - Express - * @prop {Encoding=} encoding - Encoding type of payload - * @prop {IntoZBytes=} payload - Payload associated with getrequest - * @prop {IntoZBytes=} attachment - Additional Data sent with the request - * @prop {TimeDuration=} timeout - Timeout value for a get request - * @prop {Handler} handler - A reply handler + * Options for a Get operation + * @prop {CongestionControl=} congestion_control - Congestion control applied when routing the data + * @prop {Priority=} priority - Priority of the query + * @prop {boolean=} express - Express: if set to `true`, this query will not be batched. This usually has a positive impact on latency but negative impact on throughput. + * @prop {Locality=} allowedDestination - Allowed destination for the query + * @prop {IntoEncoding=} encoding - Encoding type of payload + * @prop {IntoZBytes=} payload - Payload associated with the query + * @prop {IntoZBytes=} attachment - Additional Data sent with the query + * @prop {TimeDuration=} timeout - Timeout value for a query + * @prop {ConsolidationMode=} consolidation - Consolidation mode + * @prop {QueryTarget=} target - Queryables this query should target + * @prop {ReplyKeyExpr=} acceptReplies - Replies this query accepts + * @prop {Handler=} handler - A reply handler */ export interface GetOptions { - consolidation?: ConsolidationMode, - congestionControl?: CongestionControl, - priority?: Priority, - express?: boolean, - encoding?: Encoding, - payload?: IntoZBytes, - attachment?: IntoZBytes - timeout?: TimeDuration, - target?: QueryTarget, - handler?: Handler, + congestionControl?: CongestionControl, + priority?: Priority, + express?: boolean, + allowedDestinaton?: Locality, + encoding?: IntoEncoding, + payload?: IntoZBytes, + attachment?: IntoZBytes + timeout?: TimeDuration, + target?: QueryTarget, + consolidation?: ConsolidationMode, + acceptReplies?: ReplyKeyExpr, + handler?: Handler, } /** * Options for a Queryable - * @prop complete - Change queryable completeness. - * @prop callback - Callback function for this queryable + * @prop {boolean?} complete - Queryable completness. + * @prop {Locality=} allowedOrigin - Origin of queries, this queryable should reply to + * @prop {Handler=} handler - A query handler */ export interface QueryableOptions { - complete?: boolean, - handler?: Handler + complete?: boolean, + allowedOrigin?: Locality, + handler?: Handler } /** - * Set of options used when declaring a publisher - * @prop {Encoding} encoding - Optional, Type of Encoding data to be sent over - * @prop {CongestionControl} congestion_control - Optional, Type of Congestion control to be used (BLOCK / DROP) - * @prop {Priority} priority - Optional, The Priority of zenoh messages - * @prop {boolean} express - Optional, The Priority of zenoh messages - * @prop {Reliability} reliability - Optional, The Priority of zenoh messages : Note This is unstable in Zenoh + * Options for a Publisher + * @prop {Encoding=} encoding - Default publisher encoding, that will be applied if no encoding is specified when sending individual messages. + * @prop {CongestionControl=} congestionControl - Congestion control to be applied to messages sent with this publisher + * @prop {Priority=} priority - The Priority of messages sent with this publisher + * @prop {boolean=} express - Express setting for messages sent with this publisher. If set to `true`, the messages will not be batched. This usually has a positive impact on latency but negative impact on throughput. + * @prop {Reliability=} reliability - Reliability of messages sent with this publisher + * @prop {Locality=} allowedDestination - Allowed destination for the messages sent with this publisher */ export interface PublisherOptions { - encoding?: Encoding, - congestionControl?: CongestionControl, - priority?: Priority, - express?: boolean, - // Note realiability is unstable in Zenoh - reliability?: Reliability, + encoding?: IntoEncoding, + congestionControl?: CongestionControl, + priority?: Priority, + express?: boolean, + reliability?: Reliability, + allowedDestination?: Locality } /** * Options for a Subscriber - * @prop handler - Handler for this subscriber + * @prop {Locality=} allowedOrigin - Origin of messages this subscriber can receive + * @prop {Handler} handler - Handler for this subscriber */ export interface SubscriberOptions { - handler?: Handler, + allowedOrigin?: Locality, + handler?: Handler, +} + +/** + * Options for a Querier + * @prop {CongestionControl=} congestion_control - Congestion control applied when routing this Querier queries + * @prop {Priority=} priority - Priority of this Querier's queries + * @prop {boolean=} express - Express: If set to `true`, this query will not be batched. This usually has a positive impact on latency but negative impact on throughput. + * @prop {Locality=} allowedDestination - Allowed destination for this Querier queries + * @prop {TimeDuration=} timeout - Timeout value for this Querier queries + * @prop {QueryTarget=} target - Queryables this Querier queries should target + * @prop {ConsolidationMode=} consolidation - Consolidation mode for this Querier queries + * @prop {ReplyKeyExpr=} acceptReplies - Replies this Querier queries accept + */ +export interface QuerierOptions { + congestionControl?: CongestionControl, + priority?: Priority, + express?: boolean, + consolidation?: ConsolidationMode, + target: QueryTarget + timeout?: TimeDuration, + allowedDestination?: Locality + acceptReplies?: ReplyKeyExpr } // ███████ ███████ ███████ ███████ ██ ██████ ███ ██ @@ -161,503 +181,325 @@ export interface SubscriberOptions { * Zenoh Session */ export class Session { - async [Symbol.asyncDispose]() { - await this.close(); - } - - private constructor( - // WebSocket Backend - private remoteSession: RemoteSession - ) {} - - /** - * Creates a new Session instance - * - * @remarks - * Opens A Zenoh Session - * - * @param config - Config for session - * @returns Typescript instance of a Session - * - */ - - static async open(config: Config): Promise { - let remoteSession = await RemoteSession.new(config.locator); - return new Session(remoteSession); - } - - /** - * Closes a session, cleaning up the resource in Zenoh - * - * @returns Nothing - */ - async close() { - this.remoteSession.close(); - } - - isClosed() { - return this.remoteSession.isClosed(); - } - /** - * Puts a value on the session, on a specific key expression KeyExpr - * - * @param {IntoKeyExpr} intoKeyExpr - something that implements intoKeyExpr - * @param {IntoZBytes} intoZBytes - something that implements intoValue - * @param {PutOptions=} putOpts - an interface for the options settings on puts - * @returns void - */ - put( - intoKeyExpr: IntoKeyExpr, - intoZBytes: IntoZBytes, - putOpts?: PutOptions, - ): void { - let keyExpr = new KeyExpr(intoKeyExpr); - let zBytes = new ZBytes(intoZBytes); - - let priority; - let express; - let attachment; - let encoding = putOpts?.encoding?.toString() - let congestionControl = congestionControlToInt(putOpts?.congestionControl); - let timestamp; - - if (putOpts?.timestamp != undefined) { - timestamp = putOpts?.timestamp.getResourceUuid() as string; - } - if (putOpts?.priority != undefined) { - priority = priorityToInt(putOpts?.priority); - } - express = putOpts?.express?.valueOf(); - - if (putOpts?.attachment != undefined) { - attachment = Array.from(new ZBytes(putOpts?.attachment).toBytes()) - } - - this.remoteSession.put( - keyExpr.toString(), - Array.from(zBytes.toBytes()), - encoding, - congestionControl, - priority, - express, - attachment, - timestamp, - ); - } - - /** - * Creates a Key Expression - * - * @returns KeyExpr - */ - declareKeyexpr(intoKeyExpr: IntoKeyExpr): KeyExpr { - return new KeyExpr(intoKeyExpr) - } - - /** - * Returns the Zenoh SessionInfo Object - * - * @returns SessionInfo - */ - async info(): Promise { - let sessionInfoIface: SessionInfoIface = await this.remoteSession.info(); - - let zid = new ZenohId(sessionInfoIface.zid); - let zPeers = sessionInfoIface.z_peers.map(x => new ZenohId(x)); - let zRouters = sessionInfoIface.z_routers.map(x => new ZenohId(x)); - - let sessionInfo = new SessionInfo(zid, zPeers, zRouters); - - return sessionInfo; - } - - /** - * Executes a Delete on a session, for a specific key expression KeyExpr - * - * @param {IntoKeyExpr} intoKeyExpr - something that implements intoKeyExpr - * @param {DeleteOptions} deleteOpts - optional additional parameters to go with a delete function - * - * @returns void - */ - delete( - intoKeyExpr: IntoKeyExpr, - deleteOpts?: DeleteOptions - ): void { - let keyExpr = new KeyExpr(intoKeyExpr); - let congestionControl = congestionControlToInt(deleteOpts?.congestionControl); - let priority = priorityToInt(deleteOpts?.priority); - let express = deleteOpts?.express; - let attachment; - let timestamp; - - if (deleteOpts?.attachment != undefined) { - attachment = Array.from(new ZBytes(deleteOpts?.attachment).toBytes()) + async [Symbol.asyncDispose]() { + await this.close(); } - if (deleteOpts?.timestamp != undefined) { - timestamp = deleteOpts?.timestamp.getResourceUuid() as string; + private constructor( + private inner: SessionInner + ) { } + + /** + * Creates a new Session instance + * + * @remarks + * Opens A Zenoh Session + * + * @param config - Config for session + * @returns Typescript instance of a Session + * + */ + + static async open(config: Config): Promise { + let inner = await SessionInner.open(config.locator); + return new Session(inner); } - this.remoteSession.delete( - keyExpr.toString(), - congestionControl, - priority, - express, - attachment, - timestamp - ); - } - - /** - * Issues a get query on a Zenoh session - * - * @param intoSelector - representing a KeyExpr and Parameters - * - * @returns Receiver - */ - async get( - intoSelector: IntoSelector, - getOptions?: GetOptions - ): Promise | undefined> { - - let selector: Selector; - let keyExpr: KeyExpr; - - if (typeof intoSelector === "string" || intoSelector instanceof String) { - let splitString = intoSelector.split("?") - if (splitString.length == 1) { - keyExpr = new KeyExpr(intoSelector); - selector = new Selector(keyExpr); - } else if (splitString.length == 2 && splitString[0] != undefined && splitString[1] != undefined) { - keyExpr = new KeyExpr(splitString[0]); - let parameters: Parameters = new Parameters(splitString[1]); - selector = new Selector(keyExpr, parameters); - } else { - throw "Error: Invalid Selector, expected format ?"; - } - } else { - selector = new Selector(intoSelector); + /** + * Closes a session, cleaning up the resource in Zenoh + * + * @returns Nothing + */ + async close() { + this.inner.close(); } - let handler = getOptions?.handler ?? new FifoChannel(256); - let [calback, drop, receiver] = intoCbDropReceiver(handler); - - let callbackWS = (replyWS: ReplyWS): void => { - let reply: Reply = replyFromReplyWS(replyWS); - calback(reply); - } - // Optional Parameters - - let consolidation = consolidationModeToInt(getOptions?.consolidation) - let encoding = getOptions?.encoding?.toString(); - let congestionControl = congestionControlToInt(getOptions?.congestionControl); - let priority = priorityToInt(getOptions?.priority); - let express = getOptions?.express; - let target = queryTargetToInt(getOptions?.target); - let attachment; - let payload; - let timeoutMillis: number | undefined = undefined; - - if (getOptions?.timeout !== undefined) { - timeoutMillis = Duration.milliseconds.from(getOptions?.timeout); + isClosed() { + return this.inner.isClosed(); } - if (getOptions?.attachment != undefined) { - attachment = Array.from(new ZBytes(getOptions?.attachment).toBytes()) - } - if (getOptions?.payload != undefined) { - payload = Array.from(new ZBytes(getOptions?.payload).toBytes()) + /** + * Puts a value on the session, on a specific key expression + * + * @param {IntoKeyExpr} intoKeyExpr - key expression to publish to + * @param {IntoZBytes} intoZBytes - payload to publish + * @param {PutOptions=} putOpts - optional additional parameters to pass to delete operation + * @returns void + */ + async put( + intoKeyExpr: IntoKeyExpr, + intoZBytes: IntoZBytes, + putOpts?: PutOptions, + ) { + await this.inner.put( + new Put( + new KeyExpr(intoKeyExpr), + new ZBytes(intoZBytes), + putOpts?.encoding ? Encoding.from(putOpts.encoding) : Encoding.default(), + putOpts?.attachment ? new ZBytes(putOpts.attachment) : undefined, + putOpts?.timestamp, + new Qos( + putOpts?.priority ?? Priority.DEFAULT, + putOpts?.congestionControl ?? CongestionControl.DEFAULT_PUSH, + putOpts?.express ?? false, + putOpts?.reliability ?? Reliability.DEFAULT, + putOpts?.allowedDestinaton ?? Locality.DEFAULT + ) + ) + ); } - await this.remoteSession.get( - selector.keyExpr().toString(), - selector.parameters().toString(), - callbackWS, - drop, - consolidation, - congestionControl, - priority, - express, - target, - encoding, - payload, - attachment, - timeoutMillis - ); - - return receiver; - } - - /** - * Declares a new subscriber - * - * @remarks - * If a Subscriber is created with a callback, it cannot be simultaneously polled for new values - * - * @param {IntoKeyExpr} intoKeyExpr - key expression as a string or KeyExpr instance - * @param {SubscriberOptions} subscriberOpts - Options for the subscriber, including a handler - * - * @returns Subscriber - */ - // Handler size : This is to match the API_DATA_RECEPTION_CHANNEL_SIZE of zenoh internally - async declareSubscriber( - intoKeyExpr: IntoKeyExpr, - subscriberOpts?: SubscriberOptions - ): Promise { - let keyExpr = new KeyExpr(intoKeyExpr); - let remoteSubscriber: RemoteSubscriber; - - let handler = subscriberOpts?.handler ?? new FifoChannel(256); - let [callback, drop, receiver] = intoCbDropReceiver(handler); - - let callbackWS = (sampleWS: SampleWS): void => { - let sample: Sample = sampleFromSampleWS(sampleWS); - callback(sample); + /** + * Creates a Key Expression + * + * @returns KeyExpr + */ + declareKeyexpr(intoKeyExpr: IntoKeyExpr): KeyExpr { + return new KeyExpr(intoKeyExpr) } - remoteSubscriber = await this.remoteSession.declareRemoteSubscriber( - intoKeyExpr.toString(), - callbackWS, - drop - ); - - let subscriber = new Subscriber( - remoteSubscriber, - keyExpr, - receiver, - ); - - return subscriber; - } - - /** - * Obtain a Liveliness struct tied to this Zenoh Session. - * - * @returns Liveliness - */ - liveliness(): Liveliness { - return new Liveliness(this.remoteSession) - } - - /** - * Creates a new Timestamp instance - * - * @returns Timestamp - */ - async newTimestamp(): Promise { - - let tsIface: TimestampIface = await this.remoteSession.newTimestamp(); - - return new Timestamp(tsIface.id, tsIface.string_rep, tsIface.millis_since_epoch); - } - - /** - * Declares a new Queryable - * - * @param {IntoKeyExpr} intoKeyExpr - Queryable key expression - * @param {QueryableOptions} queryableOpts - Optional additional settings for a Queryable [QueryableOptions] - * - * @returns Queryable - */ - async declareQueryable( - intoKeyExpr: IntoKeyExpr, - queryableOpts?: QueryableOptions - ): Promise { - let keyExpr = new KeyExpr(intoKeyExpr); - - let complete = false; - if (queryableOpts?.complete != undefined) { - complete = queryableOpts?.complete; - }; - - let handler = queryableOpts?.handler ?? new FifoChannel(256); - let [callback, drop, receiver] = intoCbDropReceiver(handler); - - let callbackWS = (queryWS: QueryWS): void => { - let query = queryFromQueryWS(queryWS, this.remoteSession); - callback(query); + /** + * Returns the Zenoh SessionInfo Object + * + * @returns SessionInfo + */ + async info(): Promise { + return await this.inner.getSessionInfo(); } - let remoteQueryable = await this.remoteSession.declareRemoteQueryable( - keyExpr.toString(), - complete, - callbackWS, - drop - ); - - let queryable = new Queryable(remoteQueryable, receiver); - return queryable; - } - - /** - * Declares a new Publisher - * - * @remarks - * If a Queryable is created with a callback, it cannot be simultaneously polled for new Query's - * - * @param {IntoKeyExpr} intoKeyExpr - string of key_expression - * @param {PublisherOptions=} publisherOpts - Optional, set of options to be used when declaring a publisher - * @returns Publisher - */ - async declarePublisher( - intoKeyExpr: IntoKeyExpr, - publisherOpts?: PublisherOptions - ): Promise { - let keyExpr: KeyExpr = new KeyExpr(intoKeyExpr); - - let express = publisherOpts?.express; - - let priorityRemote; - let priority = Priority.DATA; - if (publisherOpts?.priority != null) { - priorityRemote = priorityToInt(publisherOpts?.priority); - priority = publisherOpts?.priority; + /** + * Executes a Delete on a session, for a specific key expression KeyExpr + * + * @param {IntoKeyExpr} intoKeyExpr - something that implements intoKeyExpr + * @param {DeleteOptions} deleteOpts - optional additional parameters to pass to delete operation + * + * @returns void + */ + async delete( + intoKeyExpr: IntoKeyExpr, + deleteOpts?: DeleteOptions + ) { + await this.inner.delete( + new Delete( + new KeyExpr(intoKeyExpr), + deleteOpts?.attachment ? new ZBytes(deleteOpts.attachment) : undefined, + deleteOpts?.timestamp, + new Qos( + deleteOpts?.priority ?? Priority.DEFAULT, + deleteOpts?.congestionControl ?? CongestionControl.DEFAULT_PUSH, + deleteOpts?.express ?? false, + deleteOpts?.reliability ?? Reliability.DEFAULT, + deleteOpts?.allowedDestinaton ?? Locality.DEFAULT + ) + ) + ); } - let congestionControlRemote; - let congestionControl = CongestionControl.DROP; - if (publisherOpts?.congestionControl != null) { - congestionControlRemote = congestionControlToInt(publisherOpts?.congestionControl); - congestionControl = publisherOpts?.congestionControl; + /** + * Issues a get query on a Zenoh session + * + * @param intoSelector - representing a KeyExpr and Parameters + * @param {GetOptions=} getOpts - optional additional parameters to pass to get operation + * + * @returns Receiver + */ + async get( + intoSelector: IntoSelector, + getOpts?: GetOptions + ): Promise | undefined> { + let handler = getOpts?.handler ?? new FifoChannel(256); + let [callback, drop, receiver] = intoCbDropReceiver(handler); + let selector = Selector.from(intoSelector); + await this.inner.get( + { + keyexpr: selector.keyExpr(), + parameters: selector.parameters().toString(), + payload: getOpts?.payload ? new ZBytes(getOpts.payload) : undefined, + encoding: getOpts?.encoding ? Encoding.from(getOpts.encoding) : undefined, + attachment: getOpts?.attachment ? new ZBytes(getOpts.attachment) : undefined, + qos: new Qos( + getOpts?.priority ?? Priority.DEFAULT, + getOpts?.congestionControl ?? CongestionControl.DEFAULT_REQUEST, + getOpts?.express ?? false, + Reliability.DEFAULT, + getOpts?.allowedDestinaton ?? Locality.DEFAULT + ), + querySettings: new QuerySettings( + getOpts?.target ?? QueryTarget.DEFAULT, + getOpts?.consolidation ?? ConsolidationMode.DEFAULT, + getOpts?.acceptReplies ?? ReplyKeyExpr.DEFAULT + ), + timeoutMs: getOpts?.timeout ? Duration.milliseconds.from(getOpts.timeout) : DEFAULT_QUERY_TIMEOUT_MS, + }, + { callback, drop } + ); + + return receiver; } - let reliabilityRemote = 0; // Default Reliable - let reliability = Reliability.RELIABLE; - if (publisherOpts?.reliability != null) { - reliabilityRemote = reliabilityToInt(publisherOpts?.reliability); + /** + * Declares a new subscriber + * + * @remarks + * If a Subscriber is created with a callback, it cannot be simultaneously polled for new values + * + * @param {IntoKeyExpr} intoKeyExpr - the key expression to subscribe to + * @param {SubscriberOptions} subscriberOpts - optional additional parameters to pass to subscriber declaration + * + * @returns Subscriber + */ + async declareSubscriber( + intoKeyExpr: IntoKeyExpr, + subscriberOpts?: SubscriberOptions + ): Promise { + const handler = subscriberOpts?.handler ?? new FifoChannel(256); + const keyexpr = new KeyExpr(intoKeyExpr); + let [callback, drop, receiver] = intoCbDropReceiver(handler); + + const subscriberId = await this.inner.declareSubscriber( + { + keyexpr, + allowedOrigin: subscriberOpts?.allowedOrigin ?? Locality.DEFAULT + }, + { callback, drop } + ); + return new Subscriber(this.inner, subscriberId, keyexpr, receiver); } - let encodingRemote = ""; - let encoding = Encoding.default(); - if (publisherOpts?.encoding != null) { - encodingRemote = publisherOpts?.encoding.toString(); - encoding = publisherOpts?.encoding; + /** + * Obtain a Liveliness struct tied to this Zenoh Session. + * + * @returns Liveliness + */ + liveliness(): Liveliness { + return new Liveliness(this.inner) } - let remotePublisher: RemotePublisher = - await this.remoteSession.declareRemotePublisher( - keyExpr.toString(), - encodingRemote, - congestionControlRemote, - priorityRemote, - express, - reliabilityRemote - ); - - let publisher: Publisher = new Publisher( - remotePublisher, - keyExpr, - congestionControl, - priority, - reliability, - encoding - ); - return publisher; - } - - /** - * Declares a Querier - * - * @param {IntoKeyExpr} keyexpr - string of key_expression - * @param {QuerierOptions} publisher_opts - Optional, set of options to be used when declaring a publisher - * @returns Publisher - */ - async declareQuerier( - intoKeyexpr: IntoKeyExpr, - querierOpts: QuerierOptions, - ): Promise { - const keyExpr = new KeyExpr(intoKeyexpr); - - // Optional Parameters - let priorityRemote; - let priority = Priority.DATA; - if (querierOpts?.priority != null) { - priorityRemote = priorityToInt(querierOpts?.priority); - priority = querierOpts?.priority; + /** + * Creates a new Timestamp instance + * + * @returns Timestamp + */ + async newTimestamp(): Promise { + return await this.inner.getTimestamp(); } - let congestionControlRemote; - let congestionControl = CongestionControl.DROP; - if (querierOpts?.congestionControl != null) { - congestionControlRemote = congestionControlToInt(querierOpts?.congestionControl); - congestionControl = querierOpts?.congestionControl; + /** + * Declares a new Queryable + * + * @param {IntoKeyExpr} intoKeyExpr - Queryable key expression + * @param {QueryableOptions=} queryableOpts - Optional additional settings for a Queryable [QueryableOptions] + * + * @returns Queryable + */ + async declareQueryable( + intoKeyExpr: IntoKeyExpr, + queryableOpts?: QueryableOptions + ): Promise { + const keyexpr = new KeyExpr(intoKeyExpr); + const handler = queryableOpts?.handler ?? new FifoChannel(256); + const [callback, drop, receiver] = intoCbDropReceiver(handler); + const queryableId = await this.inner.declareQueryable( + { + keyexpr, + complete: queryableOpts?.complete ?? false, + allowedOrigin: queryableOpts?.allowedOrigin ?? Locality.DEFAULT, + }, + { callback, drop } + ); + + return new Queryable(this.inner, queryableId, keyexpr, receiver); } - let acceptRepliesRemote; - let acceptReplies = ReplyKeyExpr.Any; - if (querierOpts?.acceptReplies != null) { - acceptRepliesRemote = replyKeyExprToInt(querierOpts?.acceptReplies); - acceptReplies = querierOpts?.acceptReplies; + /** + * Declares a new Publisher + * + * @param {IntoKeyExpr} intoKeyExpr - Publisher's key expression + * @param {PublisherOptions=} publisherOpts - Optional additional settings for a Publisher [PublisherOptions] + * @returns Publisher + */ + async declarePublisher( + intoKeyExpr: IntoKeyExpr, + publisherOpts?: PublisherOptions + ): Promise { + let publisherProperties = { + keyexpr: new KeyExpr(intoKeyExpr), + encoding: publisherOpts?.encoding ? Encoding.from(publisherOpts.encoding) : Encoding.default(), + qos: new Qos( + publisherOpts?.priority ?? Priority.DEFAULT, + publisherOpts?.congestionControl ?? CongestionControl.DEFAULT_PUSH, + publisherOpts?.express ?? false, + publisherOpts?.reliability ?? Reliability.DEFAULT, + publisherOpts?.allowedDestination ?? Locality.DEFAULT + ) + }; + const publisherId = await this.inner.declarePublisher(publisherProperties); + return new Publisher(this.inner, publisherId, publisherProperties); } - let consolidation = consolidationModeToInt(querierOpts?.consolidation); - let target = queryTargetToInt(querierOpts?.target); - let allowedDestination = localityToInt(querierOpts?.allowedDestination); - let express = querierOpts?.express; - let timeoutMillis: number | undefined = undefined; - - if (querierOpts?.timeout !== undefined) { - timeoutMillis = Duration.milliseconds.from(querierOpts?.timeout); + /** + * Declares a Querier + * + * @param {IntoKeyExpr} intoKeyexpr - Querier's key expression + * @param {QuerierOptions=} querierOpts - Optional additional settings for a Querier [QuerierOptions] + * @returns Publisher + */ + async declareQuerier( + intoKeyexpr: IntoKeyExpr, + querierOpts?: QuerierOptions, + ): Promise { + const properties: QuerierProperties = { + keyexpr: new KeyExpr(intoKeyexpr), + qos: new Qos( + querierOpts?.priority ?? Priority.DEFAULT, + querierOpts?.congestionControl ?? CongestionControl.DEFAULT_REQUEST, + querierOpts?.express ?? false, + Reliability.DEFAULT, + querierOpts?.allowedDestination ?? Locality.DEFAULT + ), + querySettings: new QuerySettings( + querierOpts?.target ?? QueryTarget.DEFAULT, + querierOpts?.consolidation ?? ConsolidationMode.DEFAULT, + querierOpts?.acceptReplies ?? ReplyKeyExpr.DEFAULT + ), + timeoutMs: querierOpts?.timeout ? Duration.milliseconds.from(querierOpts.timeout) : DEFAULT_QUERY_TIMEOUT_MS, + } + + let querierId = await this.inner.declareQuerier(properties); + return new Querier( + this.inner, + querierId, + properties.keyexpr, + properties.qos.congestionControl, + properties.qos.priority, + properties.querySettings.replyKeyExpr + ); } - - let remoteQuerier = await this.remoteSession.declareRemoteQuerier( - keyExpr.toString(), - consolidation, - congestionControlRemote, - priorityRemote, - express, - target, - allowedDestination, - acceptRepliesRemote, - timeoutMillis, - ); - - return new Querier( - remoteQuerier, - keyExpr, - congestionControl, - priority, - acceptReplies, - ); - } -} - -export enum RecvErr { - Disconnected, - MalformedReply, } /** * Function to open a Zenoh session */ -export function open(config: Config): Promise { - return Session.open(config); +export async function open(config: Config): Promise { + return await Session.open(config); } /** - * Struct to expose Info for your Zenoh Session + * Struct to expose Info for Zenoh Session */ export class SessionInfo { - constructor( - private zid_: ZenohId, - private peers_: ZenohId[], - private routers_: ZenohId[], - ) {} - - zid(): ZenohId { - return this.zid_; - } - routersZid(): ZenohId[] { - return this.routers_; - } - peersZid(): ZenohId[] { - return this.peers_; - } -} - -export class ZenohId { - constructor(private zid: string) {} - - toString(): string { - return this.zid; - } + constructor( + private zid_: ZenohId, + private peers_: ZenohId[], + private routers_: ZenohId[], + ) { } + + zid(): ZenohId { + return this.zid_; + } + routersZid(): ZenohId[] { + return this.routers_; + } + peersZid(): ZenohId[] { + return this.peers_; + } } \ No newline at end of file diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index 46a3f91d..c227a003 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -14,16 +14,20 @@ // Remote API interface import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; -import { DeclarePublisher, DeclareQueryable, DeclareSubscriber, deserializeHeader, InQuery, InRemoteMessageId, InReply, InSample, OutMessageInterface, Ping, PublisherProperties, QueryableProperties, ResponseError, ResponseOk, ResponsePing, serializeHeader, SubscriberProperties, UndeclarePublisher, UndeclareQueryable, UndeclareSubscriber } from "./message"; -import { Query, Reply } from "./query"; -import { Closure } from "./remote_api/closure"; -import { RemoteLink } from "./remote_api/link"; +import { KeyExpr } from "./key_expr"; +import { DeclareLivelinessSubscriber, DeclareLivelinessToken, DeclarePublisher, DeclareQuerier, DeclareQueryable, DeclareSubscriber, Delete, deserializeHeader, Get, GetProperties, GetSessionInfo, InQuery, InRemoteMessageId, InReply, InSample, LivelinessGet, LivelinessGetProperties, LivelinessSubscriberProperties, OutMessageInterface, Ping, PublisherDelete, PublisherProperties, PublisherPut, Put, QuerierGet, QuerierGetProperties, QuerierProperties, QueryableProperties, QueryResponseFinal, ReplyDel, ReplyErr, ReplyOk, ResponseError, ResponseOk, ResponsePing, ResponseSessionInfo, ResponseTimestamp, serializeHeader, SubscriberProperties, UndeclareLivelinessToken, UndeclarePublisher, UndeclareQueryable, UndeclareSubscriber } from "./message"; +import { Query, Reply, ReplyError } from "./query"; +import { Closure } from "./closure"; +import { RemoteLink } from "./link"; import { Sample } from "./sample"; +import { SessionInfo } from "./session"; +import { Timestamp } from "./timestamp"; import { ZBytes } from "./z_bytes"; class IdSource { private static MAX: number = 1 << 31; private current: number; + constructor() { this.current = 0; } @@ -42,9 +46,9 @@ class IdSource { type OnResponseReceivedCallback = (msg: [InRemoteMessageId, ZBytesDeserializer]) => void; -class SessionInner { +export class SessionInner { private static TIMEOUT_ERROR = new Error("Timeout"); - private static LINK_CLOSED_ERROR = new Error("Link is closed"); + private isClosed_: boolean = false; private link: RemoteLink; private id: string = ""; @@ -68,7 +72,13 @@ class SessionInner { private constructor(link: RemoteLink) { this.link = link; - this.link.onmessage((msg: any) => { this.onMessageReceived(msg); }); + this.link.onmessage((msg: any) => { + try { + this.onMessageReceived(msg); + } catch (e) { + console.warn(e); + } + }); } private onMessageReceived(msg: Uint8Array) { @@ -90,7 +100,7 @@ class SessionInner { if (queryable == undefined) { console.warn(`Received query for inexistant queryable ${q.queryableId}`) } else { - queryable.callback(new Query(q.query)); + queryable.callback(new Query(this, q.query)); } break; } @@ -114,6 +124,17 @@ class SessionInner { } break; } + case InRemoteMessageId.QueryResponseFinal: { + const q = QueryResponseFinal.deserialize(deserializer); + let get = this.gets.get(q.queryId); + if (get == undefined) { + console.warn(`Received responseFinal for inexistant get ${q.queryId}`) + } else { + this.gets.delete(q.queryId); + get.drop(); + } + break; + } default: throw new Error(`Received unexpected message type ${messageId}`); } } @@ -237,4 +258,159 @@ class SessionInner { ResponseOk.deserialize ); } + + async declareQuerier(info: QuerierProperties): Promise { + let querierId = this.querierIdCounter.get(); + await this.sendRequest( + new DeclareQuerier(querierId, info), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + return querierId; + } + + async undeclareQuerier(querierId: number) { + await this.sendRequest( + new UndeclareQueryable(querierId), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + } + + async declareLivelinessToken(keyexpr: KeyExpr): Promise { + let tokenId = this.livelinessTokenIdCounter.get(); + await this.sendRequest( + new DeclareLivelinessToken(tokenId, keyexpr), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + return tokenId; + } + + async undeclareLivelinessToken(tokenId: number) { + await this.sendRequest( + new UndeclareLivelinessToken(tokenId), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + } + + async declareLivelinessSubscriber(info: LivelinessSubscriberProperties, closure: Closure): Promise { + let subscriberId = this.subscriberIdCounter.get(); + await this.sendRequest( + new DeclareLivelinessSubscriber(subscriberId, info), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + this.subscribers.set(subscriberId, closure); + return subscriberId; + } + + async undeclareLivelinessSubscriber(subscriberId: number) { + const subscriber = this.subscribers.get(subscriberId); + if (subscriber == undefined) { + new Error (`Unknown subscriber id: ${subscriberId}`) + } else { + this.subscribers.delete(subscriberId); + subscriber.drop(); + } + await this.sendRequest( + new UndeclareSubscriber(subscriberId), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + } + + async getSessionInfo(): Promise { + return await this.sendRequest( + new GetSessionInfo(), + InRemoteMessageId.ResponseSessionInfo, + ResponseSessionInfo.deserialize + ).then( + (value) => value.info + ); + } + + async getTimestamp(): Promise { + return await this.sendRequest( + new GetSessionInfo(), + InRemoteMessageId.ResponseSessionInfo, + ResponseTimestamp.deserialize + ).then( + (value) => value.timestamp + ); + } + + async put(data: Put) { + return await this.sendMessage(data); + } + + async delete(data: Delete) { + return await this.sendMessage(data); + } + + async publisherPut(data: PublisherPut) { + return await this.sendMessage(data); + } + + async publisherDelete(data: PublisherDelete) { + return await this.sendMessage(data); + } + + async get(data: GetProperties, closure: Closure) { + let getId = this.getIdCounter.get(); + this.gets.set(getId, closure); + await this.sendMessage(new Get(getId, data)); + } + + async querierGet(data: QuerierGetProperties, closure: Closure) { + let getId = this.getIdCounter.get(); + this.gets.set(getId, closure); + await this.sendMessage(new QuerierGet(getId, data)); + } + + async livelinessGet(data: LivelinessGetProperties, closure: Closure) { + let getId = this.getIdCounter.get(); + this.gets.set(getId, closure); + await this.sendMessage(new LivelinessGet(getId, data)); + } + + async replyOk(data: ReplyOk) { + await this.sendMessage(data); + } + + async replyDel(data: ReplyDel) { + await this.sendMessage(data); + } + + async replyErr(data: ReplyErr) { + await this.sendMessage(data); + } + + async sendResponseFinal(queryId: number) { + await this.sendMessage(new QueryResponseFinal(queryId)); + } + + async close() { + this.link.close(); + for (let s of this.subscribers) { + s[1].drop(); + } + this.subscribers.clear(); + + for (let g of this.gets) { + g[1].drop(); + } + this.gets.clear(); + + for (let q of this.queryables) { + q[1].drop(); + } + this.queryables.clear(); + this.isClosed_ = true; + } + + isClosed(): boolean { + return this.isClosed_; + } } \ No newline at end of file diff --git a/zenoh-ts/src/z_bytes.ts b/zenoh-ts/src/z_bytes.ts index 443f97f3..bd6cdb02 100644 --- a/zenoh-ts/src/z_bytes.ts +++ b/zenoh-ts/src/z_bytes.ts @@ -16,78 +16,78 @@ * Union Type to convert various primitives and containers into ZBytes */ export type IntoZBytes = - | ZBytes - | Uint8Array - | String - | string; + | ZBytes + | Uint8Array + | String + | string; /** * Class to represent an Array of Bytes received from Zenoh */ export class ZBytes { - private buffer_: Uint8Array; + private buffer_: Uint8Array; - /** - * new function to create a ZBytes - * - * @returns ZBytes - */ - constructor(bytes: IntoZBytes) { - if (bytes instanceof ZBytes) { - this.buffer_ = bytes.buffer_; - } else if (bytes instanceof String || typeof bytes === "string") { - const encoder = new TextEncoder(); - const encoded = encoder.encode(bytes.toString()); - this.buffer_ = encoded; - } else { - this.buffer_ = Uint8Array.from(bytes); + /** + * new function to create a ZBytes + * + * @returns ZBytes + */ + constructor(bytes: IntoZBytes) { + if (bytes instanceof ZBytes) { + this.buffer_ = bytes.buffer_; + } else if (bytes instanceof String || typeof bytes === "string") { + const encoder = new TextEncoder(); + const encoded = encoder.encode(bytes.toString()); + this.buffer_ = encoded; + } else { + this.buffer_ = Uint8Array.from(bytes); + } } - } - /** - * returns the length of the ZBytes buffer - * - * @returns number - */ - public len(): number { - return this.buffer_.length; - } + /** + * returns the length of the ZBytes buffer + * + * @returns number + */ + public len(): number { + return this.buffer_.length; + } - /** - * returns if the ZBytes Buffer is empty - * - * @returns boolean - */ - public isEmpty(): boolean { - return this.buffer_.length == 0; - } + /** + * returns if the ZBytes Buffer is empty + * + * @returns boolean + */ + public isEmpty(): boolean { + return this.buffer_.length == 0; + } - /** - * returns an empty ZBytes buffer - * - * @returns ZBytes - */ - public empty(): ZBytes { - return new ZBytes(new Uint8Array()); - } + /** + * returns an empty ZBytes buffer + * + * @returns ZBytes + */ + public empty(): ZBytes { + return new ZBytes(new Uint8Array()); + } - /** - * returns the underlying Uint8Array buffer - * - * @returns Uint8Array - */ - public toBytes(): Uint8Array { - return this.buffer_ - } + /** + * returns the underlying Uint8Array buffer + * + * @returns Uint8Array + */ + public toBytes(): Uint8Array { + return this.buffer_ + } - /** - * decodes the underlying Uint8Array buffer as UTF-8 string - * - * @returns string - */ - public toString(): string { - let decoder = new TextDecoder(); - return decoder.decode(this.buffer_) - } + /** + * decodes the underlying Uint8Array buffer as UTF-8 string + * + * @returns string + */ + public toString(): string { + let decoder = new TextDecoder(); + return decoder.decode(this.buffer_) + } } \ No newline at end of file From e6078a81dc736073f9932be1775a753bef2f9742 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 26 May 2025 17:47:47 +0200 Subject: [PATCH 08/23] fix build issues --- zenoh-ts/examples/deno/src/z_get.ts | 10 ++-- .../examples/deno/src/z_get_liveliness.ts | 1 - zenoh-ts/examples/deno/src/z_querier.ts | 12 ++-- zenoh-ts/src/enums.ts | 20 +++---- zenoh-ts/src/index.ts | 4 +- zenoh-ts/src/message.ts | 58 +++++-------------- zenoh-ts/src/query.ts | 4 +- zenoh-ts/src/sample.ts | 10 ++-- zenoh-ts/src/session_inner.ts | 20 +++---- zenoh-ts/src/timestamp.ts | 2 +- zenoh-ts/tests/src/z_api_queryable_get.ts | 12 ++-- 11 files changed, 60 insertions(+), 93 deletions(-) diff --git a/zenoh-ts/examples/deno/src/z_get.ts b/zenoh-ts/examples/deno/src/z_get.ts index ea54a841..29b0fe84 100644 --- a/zenoh-ts/examples/deno/src/z_get.ts +++ b/zenoh-ts/examples/deno/src/z_get.ts @@ -12,7 +12,7 @@ // ZettaScale Zenoh Team, // -import { ReplyError, Config, RecvErr, Sample, Session, QueryTarget, ChannelReceiver, Reply } from "@eclipse-zenoh/zenoh-ts"; +import { ReplyError, Config, Sample, Session, QueryTarget, ChannelReceiver, Reply } from "@eclipse-zenoh/zenoh-ts"; import { Duration, Milliseconds } from 'typed-duration' const { milliseconds } = Duration import { BaseParseArgs } from "./parse_args.ts"; @@ -77,13 +77,13 @@ class ParseArgs extends BaseParseArgs { public getQueryTarget(): QueryTarget { switch (this.target) { case "BEST_MATCHING": - return QueryTarget.BestMatching; + return QueryTarget.BEST_MATCHING; case "ALL": - return QueryTarget.All; + return QueryTarget.ALL; case "ALL_COMPLETE": - return QueryTarget.AllComplete; + return QueryTarget.ALL_COMPLETE; default: - return QueryTarget.BestMatching; + return QueryTarget.BEST_MATCHING; } } diff --git a/zenoh-ts/examples/deno/src/z_get_liveliness.ts b/zenoh-ts/examples/deno/src/z_get_liveliness.ts index 9b5f2236..f321d63f 100644 --- a/zenoh-ts/examples/deno/src/z_get_liveliness.ts +++ b/zenoh-ts/examples/deno/src/z_get_liveliness.ts @@ -14,7 +14,6 @@ import { Sample, Config, Session, KeyExpr, - RecvErr, ReplyError, ChannelReceiver, Reply diff --git a/zenoh-ts/examples/deno/src/z_querier.ts b/zenoh-ts/examples/deno/src/z_querier.ts index 432c3868..b434c2ff 100644 --- a/zenoh-ts/examples/deno/src/z_querier.ts +++ b/zenoh-ts/examples/deno/src/z_querier.ts @@ -12,7 +12,7 @@ // ZettaScale Zenoh Team, // -import { ReplyError, Config, RecvErr, Sample, Session, QueryTarget, Selector, ChannelReceiver, Reply, } from "@eclipse-zenoh/zenoh-ts"; +import { ReplyError, Config, Sample, Session, QueryTarget, Selector, ChannelReceiver, Reply, } from "@eclipse-zenoh/zenoh-ts"; import { Duration, Milliseconds } from 'typed-duration' import { BaseParseArgs } from "./parse_args.ts"; @@ -30,7 +30,7 @@ export async function main() { for (let i = 0; i < 1000; i++) { await sleep(1000); const payload = `[${i}] ${args.payload}`; - const receiver = await querier.get(args.getSelector().parameters(), { payload: payload }) as ChannelReceiver; + const receiver = await querier.get({ parameters: args.getSelector().parameters(), payload: payload }) as ChannelReceiver; for await (const reply of receiver) { const resp = reply.result(); @@ -78,13 +78,13 @@ class ParseArgs extends BaseParseArgs { public getQueryTarget(): QueryTarget { switch (this.target) { case "BEST_MATCHING": - return QueryTarget.BestMatching; + return QueryTarget.BEST_MATCHING; case "ALL": - return QueryTarget.All; + return QueryTarget.ALL; case "ALL_COMPLETE": - return QueryTarget.AllComplete; + return QueryTarget.ALL_COMPLETE; default: - return QueryTarget.BestMatching; + return QueryTarget.BEST_MATCHING; } } diff --git a/zenoh-ts/src/enums.ts b/zenoh-ts/src/enums.ts index 79d9a36b..2f39a48a 100644 --- a/zenoh-ts/src/enums.ts +++ b/zenoh-ts/src/enums.ts @@ -13,7 +13,7 @@ // // Message priority. -export const enum Priority { +export enum Priority { REAL_TIME = 1, INTERACTIVE_HIGH = 2, INTERACTIVE_LOW = 3, @@ -25,7 +25,7 @@ export const enum Priority { } // Congestion control strategy. -export const enum CongestionControl { +export enum CongestionControl { // When transmitting a message in a node with a full queue, the node may drop the message. DROP = 0, // When transmitting a message in a node with a full queue, the node will wait for queue to @@ -39,14 +39,14 @@ export const enum CongestionControl { // The publisher reliability. // Currently `reliability` does not trigger any data retransmission on the wire. // It is rather used as a marker on the wire and it may be used to select the best link available (e.g. TCP for reliable data and UDP for best effort data). -export const enum Reliability { +export enum Reliability { BEST_EFFORT = 0, RELIABLE = 1, DEFAULT = RELIABLE } // The locality of samples to be received by subscribers or targeted by publishers. -export const enum Locality { +export enum Locality { SESSION_LOCAL = 0, REMOTE = 1, ANY = 2, @@ -54,13 +54,13 @@ export const enum Locality { } -export const enum SampleKind { - Put = 0, - Delete = 1 +export enum SampleKind { + PUT = 0, + DELETE = 1 } // The `zenoh.queryable.Queryables that should be target of a `zenoh.Session.get()`. -export const enum QueryTarget { +export enum QueryTarget { // Let Zenoh find the BestMatching queryable capabale of serving the query. BEST_MATCHING = 0, // Deliver the query to all queryables matching the query's key expression. @@ -71,7 +71,7 @@ export const enum QueryTarget { } // The kind of consolidation to apply to a query. -export const enum ConsolidationMode { +export enum ConsolidationMode { // Apply automatic consolidation based on queryable's preferences AUTO = 0, // No consolidation applied: multiple samples may be received for the same key-timestamp. @@ -90,7 +90,7 @@ export const enum ConsolidationMode { } // The kind of accepted query replies. -export const enum ReplyKeyExpr { +export enum ReplyKeyExpr { // Accept replies whose key expressions may not match the query key expression. ANY = 0, // // Accept replies whose key expressions match the query key expression. diff --git a/zenoh-ts/src/index.ts b/zenoh-ts/src/index.ts index 030efc56..a1ca30ae 100644 --- a/zenoh-ts/src/index.ts +++ b/zenoh-ts/src/index.ts @@ -15,7 +15,7 @@ // API Layer Files import { KeyExpr, IntoKeyExpr } from "./key_expr.js"; import { ZBytes, IntoZBytes } from "./z_bytes.js"; -import { CongestionControl, ConsolidationMode, Locality, Priority, Reliability, SampleKind } from "./enums.js"; +import { CongestionControl, ConsolidationMode, Locality, Priority, QueryTarget, Reliability, SampleKind } from "./enums.js"; import { Sample } from "./sample.js"; import { Timestamp } from "./timestamp.js"; import { ZenohId } from "./zid.js"; @@ -35,7 +35,7 @@ import { Duration } from 'typed-duration' // Exports export { KeyExpr, IntoKeyExpr }; export { ZBytes, IntoZBytes }; -export { CongestionControl, ConsolidationMode, Locality, Priority, Reliability, Sample, SampleKind }; +export { CongestionControl, ConsolidationMode, Locality, Priority, QueryTarget, Reliability, Sample, SampleKind }; export { Publisher, Subscriber}; export { IntoSelector, Parameters, IntoParameters, Query, Queryable, Reply, ReplyError, Selector }; export { Session, DeleteOptions, PutOptions, GetOptions, QuerierOptions, QueryableOptions, PublisherOptions, SessionInfo}; diff --git a/zenoh-ts/src/message.ts b/zenoh-ts/src/message.ts index 4c6d52b6..35553254 100644 --- a/zenoh-ts/src/message.ts +++ b/zenoh-ts/src/message.ts @@ -12,24 +12,24 @@ // ZettaScale Zenoh Team, // -import { ZBytesDeserializer, ZBytesSerializer, ZD } from "./ext" -import { Encoding, EncodingPredefined } from "./encoding.js"; +import { ZBytesDeserializer, ZBytesSerializer, ZD } from "./ext/index.js" +import { Encoding } from "./encoding.js"; import { KeyExpr } from "./key_expr.js"; import { Locality, Reliability, CongestionControl, Priority, SampleKind, ConsolidationMode, ReplyKeyExpr, QueryTarget } from "./enums.js"; -import { Timestamp } from "./timestamp"; -import { ZenohId } from "./zid"; -import { Sample } from "./sample"; -import { Parameters, QueryInner, Reply, ReplyError } from "./query"; -import { ZBytes } from "./z_bytes"; -import { SessionInfo } from "./session"; +import { Timestamp } from "./timestamp.js"; +import { ZenohId } from "./zid.js"; +import { Sample } from "./sample.js"; +import { Parameters, QueryInner, Reply, ReplyError } from "./query.js"; +import { ZBytes } from "./z_bytes.js"; +import { SessionInfo } from "./session.js"; function sampleKindFromUint8(val: number): SampleKind { switch (val) { - case SampleKind.Put: return SampleKind.Put; - case SampleKind.Delete: return SampleKind.Delete; + case SampleKind.PUT: return SampleKind.PUT; + case SampleKind.DELETE: return SampleKind.DELETE; default: { console.warn(`Unsupported SampleKind value ${val}`); - return SampleKind.Put; + return SampleKind.PUT; } } } @@ -109,31 +109,6 @@ function qosFromUint8(val: number): Qos { return new Qos(priorityFromUint8(p), congestionControlFromUint8(c), e != 0, reliabilityFromUint8(r), localityFromUint8(l)); } -function queryTargetFromUint8(val: number): QueryTarget { - switch (val) { - case QueryTarget.BEST_MATCHING: return QueryTarget.BEST_MATCHING; - case QueryTarget.ALL: return QueryTarget.ALL; - case QueryTarget.ALL_COMPLETE: return QueryTarget.ALL_COMPLETE; - default: { - console.warn(`Unsupported QueryTarget value ${val}`); - return QueryTarget.DEFAULT; - } - } -} - -function consolidationModeFromUint8(val: number): ConsolidationMode { - switch (val) { - case ConsolidationMode.AUTO: return ConsolidationMode.AUTO; - case ConsolidationMode.NONE: return ConsolidationMode.NONE; - case ConsolidationMode.MONOTONIC: return ConsolidationMode.MONOTONIC; - case ConsolidationMode.LATEST: return ConsolidationMode.LATEST; - default: { - console.warn(`Unsupported ConsolidationMode value ${val}`); - return ConsolidationMode.DEFAULT; - } - } -} - function replyKeyExprFromUint8(val: number): ReplyKeyExpr { switch (val) { case ReplyKeyExpr.ANY: return ReplyKeyExpr.ANY; @@ -197,13 +172,6 @@ function querySettingsToUint8(qs: QuerySettings): number { return qs.target | (qs.consolidation << 2) | (qs.replyKeyExpr << 4); } -function querySettingsFromUint8(val: number): QuerySettings { - let t = val & 0b11; - let c = (val >> 2) & 0b11; - let r = (val >> 4) & 0b1; - return new QuerySettings(queryTargetFromUint8(t), consolidationModeFromUint8(c), replyKeyExprFromUint8(r)); -} - function deserializeZenohId(deserializer: ZBytesDeserializer): ZenohId { return new ZenohId(deserializer.deserializeUint8Array()); } @@ -295,7 +263,7 @@ function deserializeQueryInner(deserializer: ZBytesDeserializer): QueryInner { return new QueryInner(queryId, keyexpr, params, payload, encoding, attachment, replyKeyExpr); } -export const enum OutRemoteMessageId { +export enum OutRemoteMessageId { DeclarePublisher = 0, UndeclarePublisher, DeclareSubscriber, @@ -749,7 +717,7 @@ export class Ping { } -export const enum InRemoteMessageId { +export enum InRemoteMessageId { ResponsePing = 0, ResponseOk, ResponseError, diff --git a/zenoh-ts/src/query.ts b/zenoh-ts/src/query.ts index 8cf23f9a..d79304d5 100644 --- a/zenoh-ts/src/query.ts +++ b/zenoh-ts/src/query.ts @@ -609,8 +609,8 @@ export class Selector { * New Function to create a selector from Selector / KeyExpr and Parameters * @returns Selector */ - constructor(keyexpr: KeyExpr, parameters?: IntoParameters) { - this.keyExpr_ = keyexpr; + constructor(keyexpr: IntoKeyExpr, parameters?: IntoParameters) { + this.keyExpr_ = new KeyExpr(keyexpr); if (parameters == undefined) { this.parameters_ = new Parameters("") } else { diff --git a/zenoh-ts/src/sample.ts b/zenoh-ts/src/sample.ts index 6fdea2a3..d72a604e 100644 --- a/zenoh-ts/src/sample.ts +++ b/zenoh-ts/src/sample.ts @@ -12,11 +12,11 @@ // ZettaScale Zenoh Team, // -import { KeyExpr } from "./key_expr"; -import { ZBytes } from "./z_bytes"; -import { Encoding } from "./encoding"; -import { CongestionControl, Priority, SampleKind } from "./enums"; -import { Timestamp } from "./timestamp"; +import { KeyExpr } from "./key_expr.js"; +import { ZBytes } from "./z_bytes.js"; +import { Encoding } from "./encoding.js"; +import { CongestionControl, Priority, SampleKind } from "./enums.js"; +import { Timestamp } from "./timestamp.js"; export class Sample { /** diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index c227a003..65f05a62 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -13,16 +13,16 @@ // // Remote API interface -import { ZBytesDeserializer, ZBytesSerializer } from "./ext"; -import { KeyExpr } from "./key_expr"; -import { DeclareLivelinessSubscriber, DeclareLivelinessToken, DeclarePublisher, DeclareQuerier, DeclareQueryable, DeclareSubscriber, Delete, deserializeHeader, Get, GetProperties, GetSessionInfo, InQuery, InRemoteMessageId, InReply, InSample, LivelinessGet, LivelinessGetProperties, LivelinessSubscriberProperties, OutMessageInterface, Ping, PublisherDelete, PublisherProperties, PublisherPut, Put, QuerierGet, QuerierGetProperties, QuerierProperties, QueryableProperties, QueryResponseFinal, ReplyDel, ReplyErr, ReplyOk, ResponseError, ResponseOk, ResponsePing, ResponseSessionInfo, ResponseTimestamp, serializeHeader, SubscriberProperties, UndeclareLivelinessToken, UndeclarePublisher, UndeclareQueryable, UndeclareSubscriber } from "./message"; -import { Query, Reply, ReplyError } from "./query"; -import { Closure } from "./closure"; -import { RemoteLink } from "./link"; -import { Sample } from "./sample"; -import { SessionInfo } from "./session"; -import { Timestamp } from "./timestamp"; -import { ZBytes } from "./z_bytes"; +import { ZBytesDeserializer, ZBytesSerializer } from "./ext/index.js"; +import { KeyExpr } from "./key_expr.js"; +import { DeclareLivelinessSubscriber, DeclareLivelinessToken, DeclarePublisher, DeclareQuerier, DeclareQueryable, DeclareSubscriber, Delete, deserializeHeader, Get, GetProperties, GetSessionInfo, InQuery, InRemoteMessageId, InReply, InSample, LivelinessGet, LivelinessGetProperties, LivelinessSubscriberProperties, OutMessageInterface, Ping, PublisherDelete, PublisherProperties, PublisherPut, Put, QuerierGet, QuerierGetProperties, QuerierProperties, QueryableProperties, QueryResponseFinal, ReplyDel, ReplyErr, ReplyOk, ResponseError, ResponseOk, ResponsePing, ResponseSessionInfo, ResponseTimestamp, serializeHeader, SubscriberProperties, UndeclareLivelinessToken, UndeclarePublisher, UndeclareQueryable, UndeclareSubscriber } from "./message.js"; +import { Query, Reply } from "./query.js"; +import { Closure } from "./closure.js"; +import { RemoteLink } from "./link.js"; +import { Sample } from "./sample.js"; +import { SessionInfo } from "./session.js"; +import { Timestamp } from "./timestamp.js"; +import { ZBytes } from "./z_bytes.js"; class IdSource { private static MAX: number = 1 << 31; diff --git a/zenoh-ts/src/timestamp.ts b/zenoh-ts/src/timestamp.ts index b2dd744c..843661aa 100644 --- a/zenoh-ts/src/timestamp.ts +++ b/zenoh-ts/src/timestamp.ts @@ -1,4 +1,4 @@ -import { ZenohId } from "./zid"; +import { ZenohId } from "./zid.js"; export class Timestamp { constructor(private readonly zid: ZenohId, private readonly ntp64: bigint) {} diff --git a/zenoh-ts/tests/src/z_api_queryable_get.ts b/zenoh-ts/tests/src/z_api_queryable_get.ts index e7b1cde5..a1096ee0 100644 --- a/zenoh-ts/tests/src/z_api_queryable_get.ts +++ b/zenoh-ts/tests/src/z_api_queryable_get.ts @@ -192,11 +192,11 @@ Deno.test("API - Querier Get with Channel", async () => { await sleep(1000); querier = await session2.declareQuerier(selector, { - target: QueryTarget.BestMatching + target: QueryTarget.BEST_MATCHING }); // First query with ok parameters - receiver1 = await querier.get(new Parameters("ok"), { payload: "1" }); + receiver1 = await querier.get({ parameters: new Parameters("ok"), payload: "1" }); if (!receiver1) { throw new Error("Failed to get receiver"); } @@ -222,7 +222,7 @@ Deno.test("API - Querier Get with Channel", async () => { } // Second query using the same querier with error parameters - receiver2 = await querier.get(new Parameters("err"), { payload: "2" }); + receiver2 = await querier.get({ parameters: new Parameters("err"), payload: "2" }); if (!receiver2) { throw new Error("Failed to get receiver"); } @@ -283,7 +283,7 @@ Deno.test("API - Querier Get with Callback", async () => { await sleep(1000); querier = await session2.declareQuerier(selector, { - target: QueryTarget.BestMatching + target: QueryTarget.BEST_MATCHING }); // First query with ok parameters @@ -292,7 +292,7 @@ Deno.test("API - Querier Get with Callback", async () => { replies.push(reply); }; - const receiver1 = await querier.get(new Parameters("ok"), { payload: "1", handler }); + const receiver1 = await querier.get({ parameters: new Parameters("ok"), payload: "1", handler }); assertEquals(receiver1, undefined, "Receiver should be undefined when handler is provided"); @@ -318,7 +318,7 @@ Deno.test("API - Querier Get with Callback", async () => { } // Second query using the same querier with error parameters - const receiver2 = await querier.get(new Parameters("err"), { payload: "2", handler }); + const receiver2 = await querier.get({ parameters: new Parameters("err"), payload: "2", handler }); assertEquals(receiver2, undefined, "Receiver should be undefined when handler is provided"); const query2 = await queryable.receiver()?.receive(); From c98de7847ac107a45517b56bdfa8e6268e41d09a Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 26 May 2025 21:54:48 +0200 Subject: [PATCH 09/23] fixes --- zenoh-plugin-remote-api/src/interface/mod.rs | 10 +++-- zenoh-plugin-remote-api/src/lib.rs | 3 +- zenoh-ts/examples/deno/src/z_pub.ts | 5 +-- zenoh-ts/src/message.ts | 47 +++++++++----------- zenoh-ts/src/session_inner.ts | 2 +- 5 files changed, 31 insertions(+), 36 deletions(-) diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index cfc3506c..d524731b 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -223,9 +223,9 @@ impl QuerySettings { fn locality_from_u8(l: u8) -> Result { match l { - 0 => Ok(Locality::Any), + 0 => Ok(Locality::SessionLocal), 1 => Ok(Locality::Remote), - 2 => Ok(Locality::SessionLocal), + 2 => Ok(Locality::Any), v => bail!("Unsupported locality value {}", v), } } @@ -926,9 +926,13 @@ macro_rules! remote_message { let enum_t: $enum_name = (t & 0b01111111u8).try_into()?; match enum_t { $($enum_name::$val => { + let sequence_id = match requires_ack { + true => Some(deserializer.deserialize::()?), + false => None + }; let header = Header { content_id: enum_t, - sequence_id: requires_ack.then_some(deserializer.deserialize::()?) + sequence_id }; Ok((header, $name::$val($val::from_wire(&mut deserializer).map_err(|e| FromWireError::BodyError((header, e.into())))?))) },)* diff --git a/zenoh-plugin-remote-api/src/lib.rs b/zenoh-plugin-remote-api/src/lib.rs index 25182d14..d7d03127 100644 --- a/zenoh-plugin-remote-api/src/lib.rs +++ b/zenoh-plugin-remote-api/src/lib.rs @@ -655,6 +655,5 @@ async fn handle_message( tracing::error!("RemoteAPI: message format is not `Binary`"); None } - }; - None + } } diff --git a/zenoh-ts/examples/deno/src/z_pub.ts b/zenoh-ts/examples/deno/src/z_pub.ts index d7698f0a..c0e56142 100644 --- a/zenoh-ts/examples/deno/src/z_pub.ts +++ b/zenoh-ts/examples/deno/src/z_pub.ts @@ -35,10 +35,9 @@ export async function main() { for (let idx = 0; idx < Number.MAX_VALUE; idx++) { const buf = `[${idx}] ${args.payload}`; - - console.warn("Block statement execution no : " + idx); console.warn(`Putting Data ('${keyExpr}': '${buf}')...`); - await publisher.put(buf, { encoding: Encoding.TEXT_PLAIN, attachment: args.attach }); + let attachment = args.attach.length == 0 ? undefined : args.attach + await publisher.put(buf, { encoding: Encoding.TEXT_PLAIN, attachment }); await sleep(1000); } } diff --git a/zenoh-ts/src/message.ts b/zenoh-ts/src/message.ts index 35553254..2cc5f040 100644 --- a/zenoh-ts/src/message.ts +++ b/zenoh-ts/src/message.ts @@ -123,12 +123,7 @@ function replyKeyExprFromUint8(val: number): ReplyKeyExpr { function serializeEncoding(e: Encoding, serializer: ZBytesSerializer) { let [id, schema] = e.toIdSchema(); serializer.serializeNumberUint16(id); - if (schema == undefined) { - serializer.serializeBoolean(false); - } else { - serializer.serializeBoolean(true); - serializer.serializeString(schema); - } + serializer.serializeString(schema ?? ""); } function serializeOptEncoding(e: Encoding | undefined, serializer: ZBytesSerializer) { @@ -142,10 +137,8 @@ function serializeOptEncoding(e: Encoding | undefined, serializer: ZBytesSeriali function deserializeEncoding(deserializer: ZBytesDeserializer): Encoding { let id = deserializer.deserializeNumberUint16(); - let schema: string | undefined; - if (deserializer.deserializeBoolean()) { - schema = deserializer.deserializeString(); - } else { + let schema: string | undefined = deserializer.deserializeString(); + if (schema.length == 0) { schema = undefined; } return new Encoding(id, schema); @@ -306,7 +299,7 @@ export class DeclarePublisher { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); serializer.serializeString(this.properties.keyexpr.toString()); serializeEncoding(this.properties.encoding, serializer); serializer.serializeNumberUint8(qosToUint8(this.properties.qos)); @@ -320,7 +313,7 @@ export class UndeclarePublisher { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); } } @@ -337,7 +330,7 @@ export class DeclareSubscriber { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); serializer.serializeString(this.properties.keyexpr.toString()); serializer.serializeNumberUint8(this.properties.allowedOrigin); } @@ -350,7 +343,7 @@ export class UndeclareSubscriber { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); } } @@ -367,7 +360,7 @@ export class DeclareQueryable { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); serializer.serializeString(this.properties.keyexpr.toString()); serializer.serializeBoolean(this.properties.complete); serializer.serializeNumberUint8(this.properties.allowedOrigin); @@ -381,7 +374,7 @@ export class UndeclareQueryable { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); } } @@ -400,7 +393,7 @@ export class DeclareQuerier { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); serializer.serializeString(this.properties.keyexpr.toString()); serializer.serializeNumberUint8(qosToUint8(this.properties.qos)); serializer.serializeNumberUint8(querySettingsToUint8(this.properties.querySettings)); @@ -415,7 +408,7 @@ export class UndeclareQuerier { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); } } @@ -427,7 +420,7 @@ export class DeclareLivelinessToken { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); serializer.serializeString(this.keyexpr.toString()); } } @@ -439,7 +432,7 @@ export class UndeclareLivelinessToken { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); } } @@ -456,7 +449,7 @@ export class DeclareLivelinessSubscriber { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); serializer.serializeString(this.properties.keyexpr.toString()); serializer.serializeBoolean(this.properties.history); } @@ -469,7 +462,7 @@ export class UndeclareLivelinessSubscriber { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.id); + serializer.serializeNumberUint32(this.id); } } @@ -646,7 +639,7 @@ export class ReplyOk { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.queryId); + serializer.serializeNumberUint32(this.queryId); serializer.serializeString(this.keyexpr.toString()); serializer.serializeUint8Array(this.payload.toBytes()); serializeEncoding(this.encoding, serializer); @@ -667,7 +660,7 @@ export class ReplyDel { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.queryId); + serializer.serializeNumberUint32(this.queryId); serializer.serializeString(this.keyexpr.toString()); serializeOptZBytes(this.attachment, serializer); serializeOptTimestamp(this.timestamp, serializer); @@ -684,7 +677,7 @@ export class ReplyErr { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.queryId); + serializer.serializeNumberUint32(this.queryId); serializer.serializeUint8Array(this.payload.toBytes()); serializeEncoding(this.encoding, serializer); } @@ -699,11 +692,11 @@ export class QueryResponseFinal { ) {} public serializeWithZSerializer(serializer: ZBytesSerializer) { - serializer.serializeNumberUint8(this.queryId); + serializer.serializeNumberUint32(this.queryId); } public static deserialize(deserializer: ZBytesDeserializer): QueryResponseFinal { - let queryId = deserializer.deserializeNumberUint8(); + let queryId = deserializer.deserializeNumberUint32(); return new QueryResponseFinal(queryId); } } diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index 65f05a62..5639c53d 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -152,7 +152,7 @@ export class SessionInner { const msgId = this.messageIdCounter.get(); serializeHeader([msg.outMessageId, msgId], serializer); - serializer.serializeNumberUint8(msgId); + msg.serializeWithZSerializer(serializer); const p = new Promise((resolve: OnResponseReceivedCallback, _) => { this.pendingMessageResponses.set(msgId, resolve); }); From 8092e5ab6947d5bcc080d66a5a62403d20b2d81c Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 26 May 2025 22:08:56 +0200 Subject: [PATCH 10/23] improve serialization performance --- zenoh-ts/src/ext/serialization.ts | 35 ++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/zenoh-ts/src/ext/serialization.ts b/zenoh-ts/src/ext/serialization.ts index 2970fa85..7ee10ed3 100644 --- a/zenoh-ts/src/ext/serialization.ts +++ b/zenoh-ts/src/ext/serialization.ts @@ -117,21 +117,21 @@ function isSerializeable(s: any): s is ZSerializeable { * Provides functionality for tuple-like serialization. */ export class ZBytesSerializer { - private buffer_: Uint8Array + private buffer: Uint8Array[]; + private len: number; /** * new function to create a ZBytesSerializer. * * @returns ZBytesSerializer */ constructor() { - this.buffer_ = new Uint8Array(); + this.buffer = new Array; + this.len = 0; } private append(buf: Uint8Array) { - let b = new Uint8Array(this.buffer_.length + buf.length) - b.set(this.buffer_) - b.set(buf, this.buffer_.length) - this.buffer_ = b + this.buffer.push(buf); + this.len += buf.length; } /** @@ -383,7 +383,7 @@ export class ZBytesSerializer { * Serializes boolean. */ public serializeBoolean(val: Boolean) { - const b:Uint8Array = new Uint8Array(1) + const b: Uint8Array = new Uint8Array(1) b[0] = val === true ? 1 : 0 this.append(b) } @@ -499,9 +499,24 @@ export class ZBytesSerializer { * @returns ZBytes */ public finish(): ZBytes { - let out = new ZBytes(this.buffer_); - this.buffer_ = new Uint8Array() - return out + let out: ZBytes; + if (this.buffer.length == 0) { + out = new ZBytes(new Uint8Array(0)); + } else if (this.buffer.length == 1) { + out = new ZBytes(this.buffer[0] as Uint8Array); + } else { + let b = new Uint8Array(this.len); + let offset = 0; + for (let a of this.buffer) { + b.set(a, offset); + offset += a.length; + } + out = new ZBytes(b); + } + + this.buffer = new Array(); + this.len = 0; + return out; } } From 040a1e747c02b13f3ce2bb9bc77a1e051820d898 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 27 May 2025 12:39:08 +0200 Subject: [PATCH 11/23] improve serialization performance --- zenoh-ts/examples/deno/src/z_pub_thr.ts | 9 + zenoh-ts/examples/deno/src/z_sub_thr.ts | 49 +++-- zenoh-ts/src/ext/serialization.ts | 226 +++++++++++++++--------- zenoh-ts/src/session_inner.ts | 2 +- 4 files changed, 177 insertions(+), 109 deletions(-) diff --git a/zenoh-ts/examples/deno/src/z_pub_thr.ts b/zenoh-ts/examples/deno/src/z_pub_thr.ts index 6767d432..4eef123b 100644 --- a/zenoh-ts/examples/deno/src/z_pub_thr.ts +++ b/zenoh-ts/examples/deno/src/z_pub_thr.ts @@ -36,8 +36,17 @@ export async function main() { payload[i] = i; } + let startTime = performance.now(); + let n = 0; while (true) { await publisher.put(payload); + n++; + + if (n % 100000 == 0) { + const endTime = performance.now(); + console.log(100000 / (endTime - startTime) * 1000, " msg/s") + startTime = performance.now(); + } } await session.close(); } diff --git a/zenoh-ts/examples/deno/src/z_sub_thr.ts b/zenoh-ts/examples/deno/src/z_sub_thr.ts index 2a3d0121..d9d56dde 100644 --- a/zenoh-ts/examples/deno/src/z_sub_thr.ts +++ b/zenoh-ts/examples/deno/src/z_sub_thr.ts @@ -16,44 +16,39 @@ import { Config, Session, Sample, KeyExpr } from "@eclipse-zenoh/zenoh-ts"; import { BaseParseArgs } from "./parse_args.ts"; // Throughput test class Stats { - // eslint-disable-next-line @typescript-eslint/naming-convention - round_count: number; - // eslint-disable-next-line @typescript-eslint/naming-convention - round_size: number; - // eslint-disable-next-line @typescript-eslint/naming-convention - finished_rounds: number; - // eslint-disable-next-line @typescript-eslint/naming-convention - round_start: number; - // eslint-disable-next-line @typescript-eslint/naming-convention - global_start: number; + roundCount: number; + roundSize: number; + finishedRounds: number; + roundStart: number; + globalStart: number; constructor(roundSize: number) { - this.round_count = 0; - this.round_size = roundSize; - this.finished_rounds = 0; - this.round_start = Date.now(); - this.global_start = 0; + this.roundCount = 0; + this.roundSize = roundSize; + this.finishedRounds = 0; + this.roundStart = performance.now(); + this.globalStart = 0; } increment() { - if (this.round_count == 0) { - this.round_start = Date.now(); - if (this.global_start == 0) { - this.global_start = this.round_start; + if (this.roundCount == 0) { + this.roundStart = performance.now(); + if (this.globalStart == 0) { + this.globalStart = this.roundStart; } - this.round_count += 1; - } else if (this.round_count < this.round_size) { - this.round_count += 1; + this.roundCount += 1; + } else if (this.roundCount < this.roundSize) { + this.roundCount += 1; } else { this.printRound(); - this.finished_rounds += 1; - this.round_count = 0; + this.finishedRounds += 1; + this.roundCount = 0; } } printRound() { - const elapsedMs = Date.now() - this.round_start; - const throughput = (this.round_size) / (elapsedMs / 1000); + const elapsedMs = performance.now() - this.roundStart; + const throughput = (this.roundSize) / (elapsedMs / 1000); console.warn(throughput, " msg/s"); } } @@ -73,7 +68,7 @@ export async function main() { { handler: subscriberCallback } ); - while (stats.finished_rounds < args.samples) { + while (stats.finishedRounds < args.samples) { await sleep(500); } diff --git a/zenoh-ts/src/ext/serialization.ts b/zenoh-ts/src/ext/serialization.ts index 7ee10ed3..5a2d5821 100644 --- a/zenoh-ts/src/ext/serialization.ts +++ b/zenoh-ts/src/ext/serialization.ts @@ -117,20 +117,41 @@ function isSerializeable(s: any): s is ZSerializeable { * Provides functionality for tuple-like serialization. */ export class ZBytesSerializer { - private buffer: Uint8Array[]; + private static readonly DEFAULT_BUFFER_SIZE = 256; + private buffer: Uint8Array; + private bufferLen: number; + private data: Uint8Array[]; private len: number; + + private resetBuffer(size: number) { + if (size < ZBytesSerializer.DEFAULT_BUFFER_SIZE) { + size = ZBytesSerializer.DEFAULT_BUFFER_SIZE; + } + this.buffer = new Uint8Array(size); + this.bufferLen = 0; + } + + private ensureBuffer(size: number) { + if (this.bufferLen + size >= this.buffer.length) { + this.resetBuffer(size); + } + this.bufferLen += size; + return this.buffer.subarray(this.bufferLen - size, this.bufferLen); + } /** * new function to create a ZBytesSerializer. * * @returns ZBytesSerializer */ constructor() { - this.buffer = new Array; + this.data = new Array; this.len = 0; + this.buffer = new Uint8Array(ZBytesSerializer.DEFAULT_BUFFER_SIZE); + this.bufferLen = 0; } private append(buf: Uint8Array) { - this.buffer.push(buf); + this.data.push(buf); this.len += buf.length; } @@ -138,7 +159,11 @@ export class ZBytesSerializer { * Serializes length of the sequence. Can be used when defining serialization for custom containers. */ public writeSequenceLength(len: number) { - this.append(leb.encodeULEB128(len)) + let a = this.ensureBuffer(10); + let sz = leb.encodeULEB128Into(a, len); + a = a.subarray(0, sz); + this.bufferLen -= (10 - sz); + this.append(a); } /** @@ -267,8 +292,8 @@ export class ZBytesSerializer { * Serializes bigint as 64 bit signed integer. */ public serializeBigintInt64(val: bigint) { - let data = new Uint8Array(8); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(8); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setBigInt64(0, val, true); this.append(data) } @@ -277,8 +302,8 @@ export class ZBytesSerializer { * Serializes bigint as 64 bit unsigned integer. */ public serializeBigintUint64(val: bigint) { - let data = new Uint8Array(8); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(8); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setBigUint64(0, val, true); this.append(data) } @@ -287,8 +312,8 @@ export class ZBytesSerializer { * Serializes number as 64 bit floating point number. */ public serializeNumberFloat64(val: number) { - let data = new Uint8Array(8); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(8); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setFloat64(0, val, true); this.append(data) } @@ -297,8 +322,8 @@ export class ZBytesSerializer { * Serializes number as 32 bit floating point number. */ public serializeNumberFloat32(val: number) { - let data = new Uint8Array(4); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(4); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setFloat32(0, val, true); this.append(data) } @@ -323,8 +348,8 @@ export class ZBytesSerializer { * Serializes number as 32 bit integer. */ public serializeNumberInt32(val: number) { - let data = new Uint8Array(4); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(4); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setInt32(0, val, true); this.append(data) } @@ -333,8 +358,8 @@ export class ZBytesSerializer { * Serializes number as 32 bit unsigned integer. */ public serializeNumberUint32(val: number) { - let data = new Uint8Array(4); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(4); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setUint32(0, val, true); this.append(data) } @@ -343,8 +368,8 @@ export class ZBytesSerializer { * Serializes number as 16 bit integer. */ public serializeNumberInt16(val: number) { - let data = new Uint8Array(2); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(2); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setInt16(0, val, true); this.append(data) } @@ -353,8 +378,8 @@ export class ZBytesSerializer { * Serializes number as 16 bit unsigned integer. */ public serializeNumberUint16(val: number) { - let data = new Uint8Array(2); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(2); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setUint16(0, val, true); this.append(data) } @@ -363,8 +388,8 @@ export class ZBytesSerializer { * Serializes number as 8 bit integer. */ public serializeNumberInt8(val: number) { - let data = new Uint8Array(1); - let view = new DataView(data.buffer); + let data = this.ensureBuffer(1); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); view.setInt8(0, val); this.append(data) } @@ -373,19 +398,16 @@ export class ZBytesSerializer { * Serializes number as 8 bit unsigned integer. */ public serializeNumberUint8(val: number) { - let data = new Uint8Array(1); - let view = new DataView(data.buffer); - view.setUint8(0, val); - this.append(data) + let data = this.ensureBuffer(1); + data[0] = val; + this.append(data); } /** * Serializes boolean. */ public serializeBoolean(val: Boolean) { - const b: Uint8Array = new Uint8Array(1) - b[0] = val === true ? 1 : 0 - this.append(b) + this.serializeNumberUint8(val === true ? 1 : 0); } /** @@ -499,24 +521,32 @@ export class ZBytesSerializer { * @returns ZBytes */ public finish(): ZBytes { - let out: ZBytes; - if (this.buffer.length == 0) { - out = new ZBytes(new Uint8Array(0)); - } else if (this.buffer.length == 1) { - out = new ZBytes(this.buffer[0] as Uint8Array); + const out = new ZBytes(this.toBytes()); + this.data = new Array(); + this.len = 0; + this.bufferLen = 0; + return new ZBytes(out); + } + + /** + * Extracts currently serialized bytes. + * + * @returns ZBytes + */ + public toBytes(): Uint8Array { + if (this.data.length == 0) { + return new Uint8Array(0); + } else if (this.data.length == 1) { + return this.data[0] as Uint8Array; } else { let b = new Uint8Array(this.len); let offset = 0; - for (let a of this.buffer) { + for (let a of this.data) { b.set(a, offset); offset += a.length; } - out = new ZBytes(b); + return b; } - - this.buffer = new Array(); - this.len = 0; - return out; } } @@ -1038,6 +1068,15 @@ export class ZBytesDeserializer { return s } + private readByte(): number { + if (this.idx_ >= this.buffer_.length) { + throw new Error(`Array index is out of bounds: ${this.idx_ + 1} / ${this.buffer_.length}`); + } + const b = this.buffer_[this.idx_] as number; + this.idx_ += 1; + return b; + } + /** * Reads length of the sequence previously written by {@link ZBytesSerializer.writeSequenceLength} and advances the reading position. * @returns Number of sequence elements. @@ -1065,7 +1104,7 @@ export class ZBytesDeserializer { */ public deserializeUint8Array(): Uint8Array { let len = this.readSequenceLength(); - return this.readSlice(len).slice() + return this.readSlice(len) } /** @@ -1074,7 +1113,11 @@ export class ZBytesDeserializer { public deserializeUint16Array(): Uint16Array { let len = this.readSequenceLength(); if (isLittleEndian) { - return new Uint16Array(this.readSlice(len * 2).slice().buffer) + let s = this.readSlice(len * 2); + if (s.byteOffset % 2 != 0) { + s = s.slice(); + } + return new Uint16Array(s.buffer, s.byteOffset, len); } else { let out = new Uint16Array(len) for (let i = 0; i < len; i++) { @@ -1090,7 +1133,11 @@ export class ZBytesDeserializer { public deserializeUint32Array(): Uint32Array { let len = this.readSequenceLength(); if (isLittleEndian) { - return new Uint32Array(this.readSlice(len * 4).slice().buffer) + let s = this.readSlice(len * 4); + if (s.byteOffset % 4 != 0) { + s = s.slice(); + } + return new Uint32Array(s.buffer, s.byteOffset, len); } else { let out = new Uint32Array(len) for (let i = 0; i < len; i++) { @@ -1106,7 +1153,11 @@ export class ZBytesDeserializer { public deserializeBiguint64Array(): BigUint64Array { let len = this.readSequenceLength(); if (isLittleEndian) { - return new BigUint64Array(this.readSlice(len * 8).slice().buffer) + let s = this.readSlice(len * 8); + if (s.byteOffset % 8 != 0) { + s = s.slice(); + } + return new BigUint64Array(s.buffer, s.byteOffset, len); } else { let out = new BigUint64Array(len) for (let i = 0; i < len; i++) { @@ -1121,7 +1172,8 @@ export class ZBytesDeserializer { */ public deserializeInt8Array(): Int8Array { let len = this.readSequenceLength(); - return new Int8Array(this.readSlice(len).slice().buffer) + const s = this.readSlice(len); + return new Int8Array(s.buffer, s.byteOffset, len) } /** @@ -1130,7 +1182,11 @@ export class ZBytesDeserializer { public deserializeInt16Array(): Int16Array { let len = this.readSequenceLength(); if (isLittleEndian) { - return new Int16Array(this.readSlice(len * 2).slice().buffer) + let s = this.readSlice(len * 2); + if (s.byteOffset % 2 != 0) { + s = s.slice(); + } + return new Int16Array(s.buffer, s.byteOffset, len) } else { let out = new Int16Array(len) for (let i = 0; i < len; i++) { @@ -1146,13 +1202,17 @@ export class ZBytesDeserializer { public deserializeInt32Array(): Int32Array { let len = this.readSequenceLength(); if (isLittleEndian) { - return new Int32Array(this.readSlice(len * 4).slice().buffer) + let s = this.readSlice(len * 4); + if (s.byteOffset % 4 != 0) { + s = s.slice(); + } + return new Int32Array(s.buffer, s.byteOffset, len); } else { - let out = new Int32Array(len) + let out = new Int32Array(len); for (let i = 0; i < len; i++) { - out[i] = this.deserializeNumberInt32() + out[i] = this.deserializeNumberInt32(); } - return out + return out; } } @@ -1162,7 +1222,11 @@ export class ZBytesDeserializer { public deserializeBigint64Array(): BigInt64Array { let len = this.readSequenceLength(); if (isLittleEndian) { - return new BigInt64Array(this.readSlice(len * 8).slice().buffer) + let s = this.readSlice(len * 8); + if (s.byteOffset % 8 != 0) { + s = s.slice(); + } + return new BigInt64Array(s.buffer, s.byteOffset, len); } else { let out = new BigInt64Array(len) for (let i = 0; i < len; i++) { @@ -1178,7 +1242,11 @@ export class ZBytesDeserializer { public deserializeFloat32Array(): Float32Array { let len = this.readSequenceLength(); if (isLittleEndian) { - return new Float32Array(this.readSlice(len * 4).slice().buffer) + let s = this.readSlice(len * 4); + if (s.byteOffset % 4 != 0) { + s = s.slice(); + } + return new Float32Array(s.buffer, s.byteOffset, len); } else { let out = new Float32Array(len) for (let i = 0; i < len; i++) { @@ -1194,7 +1262,11 @@ export class ZBytesDeserializer { public deserializeFloat64Array(): Float64Array { let len = this.readSequenceLength(); if (isLittleEndian) { - return new Float64Array(this.readSlice(len * 8).slice().buffer) + let s = this.readSlice(len * 8); + if (s.byteOffset % 8 != 0) { + s = s.slice(); + } + return new Float64Array(s.buffer, s.byteOffset, len); } else { let out = new Float64Array(len) for (let i = 0; i < len; i++) { @@ -1204,14 +1276,12 @@ export class ZBytesDeserializer { } } - - /** * Deserializes next portion of data (serialized as 64 bit signed integer) as bigint and advances the reading position. */ public deserializeBigintInt64(): bigint { - let data = this.readSlice(8).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(8); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getBigInt64(0, true); } @@ -1219,8 +1289,8 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 64 bit unsigned integer) as bigint and advances the reading position. */ public deserializeBigintUint64(): bigint { - let data = this.readSlice(8).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(8); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getBigUint64(0, true); } @@ -1228,8 +1298,8 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 64 bit floating point number) as number and advances the reading position. */ public deserializeNumberFloat64(): number { - let data = this.readSlice(8).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(8); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getFloat64(0, true); } @@ -1237,8 +1307,8 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 32 bit floating point number) as number and advances the reading position. */ public deserializeNumberFloat32(): number { - let data = this.readSlice(4).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(4); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getFloat32(0, true); } @@ -1270,8 +1340,8 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 32 bit signed integer) as number and advances the reading position. */ public deserializeNumberInt32(): number { - let data = this.readSlice(4).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(4); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getInt32(0, true); } @@ -1279,8 +1349,8 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 32 bit unsigned integer) as number and advances the reading position. */ public deserializeNumberUint32(): number { - let data = this.readSlice(4).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(4); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getUint32(0, true); } @@ -1288,8 +1358,8 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 16 bit signed integer) as number and advances the reading position. */ public deserializeNumberInt16(): number { - let data = this.readSlice(2).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(2); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getInt16(0, true); } @@ -1297,8 +1367,8 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 16 bit unsigned integer) as number and advances the reading position. */ public deserializeNumberUint16(): number { - let data = this.readSlice(2).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(2); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getUint16(0, true); } @@ -1306,8 +1376,8 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 8 bit signed integer) as number and advances the reading position. */ public deserializeNumberInt8(): number { - let data = this.readSlice(1).slice(); - let view = new DataView(data.buffer); + let data = this.readSlice(1); + let view = new DataView(data.buffer, data.byteOffset, data.byteLength); return view.getInt8(0); } @@ -1315,20 +1385,14 @@ export class ZBytesDeserializer { * Deserializes next portion of data (serialized as 8 bit unsigned integer) as number and advances the reading position. */ public deserializeNumberUint8(): number { - let data = this.readSlice(1).slice(); - let view = new DataView(data.buffer); - return view.getUint8(0); + return this.readByte(); } /** * Deserializes next portion of data as a boolean and advances the reading position. */ public deserializeBoolean(): boolean { - if (this.idx_ >= this.buffer_.length) { - throw new Error(`Array index is out of bounds: ${this.idx_} / ${this.buffer_.length}`); - } - const res = this.buffer_[this.idx_] - this.idx_ += 1 + const res = this.readByte(); if (res == 1) { return true; } else if (res == 0) { diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index 5639c53d..9a08de68 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -144,7 +144,7 @@ export class SessionInner { let serializer = new ZBytesSerializer(); serializer.serializeNumberUint8(msg.outMessageId); msg.serializeWithZSerializer(serializer); - return await this.link.send(serializer.finish().toBytes()); + return await this.link.send(serializer.toBytes()); } private async sendRequest(msg: OutMessageInterface, expectedResponseId: InRemoteMessageId, deserialize: (deserializer: ZBytesDeserializer) => T): Promise { From 87d104ece680d92fc614aeff28b9085875599389 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 27 May 2025 15:48:26 +0200 Subject: [PATCH 12/23] fixes --- zenoh-ts/examples/deno/src/z_info.ts | 12 ++--- zenoh-ts/examples/deno/src/z_querier.ts | 3 +- zenoh-ts/examples/deno/src/z_queryable.ts | 8 ++-- zenoh-ts/examples/deno/src/z_sub.ts | 5 +- zenoh-ts/src/message.ts | 2 +- zenoh-ts/src/query.ts | 10 ++-- zenoh-ts/src/session_inner.ts | 57 ++++++++++++++++------- zenoh-ts/src/zid.ts | 3 +- 8 files changed, 63 insertions(+), 37 deletions(-) diff --git a/zenoh-ts/examples/deno/src/z_info.ts b/zenoh-ts/examples/deno/src/z_info.ts index 88ec9840..83b896cc 100644 --- a/zenoh-ts/examples/deno/src/z_info.ts +++ b/zenoh-ts/examples/deno/src/z_info.ts @@ -23,17 +23,11 @@ export async function main() { console.log!("Get Info..."); const info: SessionInfo = await session.info(); - console.log!("zid: {}", info.zid()); + console.log!("zid: ", info.zid().toString()); - console.log!( - "routers zid: {:?}", - info.routersZid() - ); + console.log!(`routers zid: ${info.routersZid()}`); - console.log!( - "peers zid: {:?}", - info.peersZid() - ); + console.log!(`peers zid: ${info.peersZid()}`); } diff --git a/zenoh-ts/examples/deno/src/z_querier.ts b/zenoh-ts/examples/deno/src/z_querier.ts index b434c2ff..2e3e2527 100644 --- a/zenoh-ts/examples/deno/src/z_querier.ts +++ b/zenoh-ts/examples/deno/src/z_querier.ts @@ -30,13 +30,14 @@ export async function main() { for (let i = 0; i < 1000; i++) { await sleep(1000); const payload = `[${i}] ${args.payload}`; + console.log!(`Querying '${args.getSelector().toString()}' with payload: '${payload}'...`); const receiver = await querier.get({ parameters: args.getSelector().parameters(), payload: payload }) as ChannelReceiver; for await (const reply of receiver) { const resp = reply.result(); if (resp instanceof Sample) { const sample: Sample = resp; - console.warn(">> Received ('", sample.keyexpr(), ":", sample.payload().toString(), "')"); + console.warn(">> Received ('", sample.keyexpr().toString(), ":", sample.payload().toString(), "')"); } else { const replyError: ReplyError = resp; console.warn(">> Received (ERROR: '{", replyError.payload().toString(), "}')"); diff --git a/zenoh-ts/examples/deno/src/z_queryable.ts b/zenoh-ts/examples/deno/src/z_queryable.ts index 453bb8b4..8f82489b 100644 --- a/zenoh-ts/examples/deno/src/z_queryable.ts +++ b/zenoh-ts/examples/deno/src/z_queryable.ts @@ -43,6 +43,7 @@ export async function main() { // `>> [Queryable ] Responding ${key_expr.toString()} with payload '${response}'`, // ); // query.reply(key_expr, response); + // query.finalize(); // } // let queryable_cb: Queryable = await session.declare_queryable(key_expr, { @@ -61,20 +62,21 @@ export async function main() { }); for await (const query of queryable.receiver() as ChannelReceiver) { + await using scopedQuery = query; const zbytes: ZBytes | undefined = query.payload(); if (zbytes == undefined) { - console.warn!(`>> [Queryable ] Received Query ${query.selector().toString()}`); + console.warn!(`>> [Queryable ] Received Query ${scopedQuery.selector().toString()}`); } else { console.warn!( - `>> [Queryable ] Received Query ${query.selector().toString()} with payload '${zbytes.toString()}'`, + `>> [Queryable ] Received Query ${scopedQuery.selector().toString()} with payload '${zbytes.toString()}'`, ); } console.warn( `>> [Queryable ] Responding ${keyExpr.toString()} with payload '${response}'`, ); - await query.reply(keyExpr, response); + await scopedQuery.reply(keyExpr, response); } } diff --git a/zenoh-ts/examples/deno/src/z_sub.ts b/zenoh-ts/examples/deno/src/z_sub.ts index 96bb2e61..f6af7853 100644 --- a/zenoh-ts/examples/deno/src/z_sub.ts +++ b/zenoh-ts/examples/deno/src/z_sub.ts @@ -13,7 +13,8 @@ // import { - Config, Subscriber, Session, KeyExpr, RingChannel, ChannelReceiver, Sample + Config, Subscriber, Session, KeyExpr, RingChannel, ChannelReceiver, Sample, + SampleKind } from "@eclipse-zenoh/zenoh-ts"; import { BaseParseArgs } from "./parse_args.ts"; @@ -30,7 +31,7 @@ export async function main() { for await (const sample of pollSubscriber.receiver() as ChannelReceiver) { console.warn!( ">> [Subscriber] Received " + - sample.kind() + " ('" + + SampleKind[sample.kind()] + " ('" + sample.keyexpr() + "': '" + sample.payload().toString() + "')", ); diff --git a/zenoh-ts/src/message.ts b/zenoh-ts/src/message.ts index 2cc5f040..2c4bdd93 100644 --- a/zenoh-ts/src/message.ts +++ b/zenoh-ts/src/message.ts @@ -245,7 +245,7 @@ function deserializeReply(deserializer: ZBytesDeserializer): Reply { } function deserializeQueryInner(deserializer: ZBytesDeserializer): QueryInner { - let queryId = deserializer.deserializeNumberUint8(); + let queryId = deserializer.deserializeNumberUint32(); let keyexpr = new KeyExpr(deserializer.deserializeString()); let params = new Parameters(deserializer.deserializeString()); let payload = deserializeOptZBytes(deserializer); diff --git a/zenoh-ts/src/query.ts b/zenoh-ts/src/query.ts index d79304d5..691d07e6 100644 --- a/zenoh-ts/src/query.ts +++ b/zenoh-ts/src/query.ts @@ -143,7 +143,10 @@ export class QueryInner { } /** - * Query Class to handle + * A Zenoh Query + * @remarks Once all replies have been sent, it is necessary to call Query.finalize + * to signal, that no more replies will be provided by the given queryable, otherwise this + * will result in query timeout error on the querier side. */ export class Query { /** @@ -265,8 +268,7 @@ export class Query { } /** - * Finalize query, signaling that all replies have been sent. - * This function MUST be called once all replies to the query have been sent. + * Finalizes query, signaling that all replies have been sent. * @returns void */ async finalize() { @@ -574,7 +576,7 @@ export class Selector { toString(): string { if (this.parameters_ != undefined) { - return this.keyExpr_.toString() + "?" + this.parameters_?.toString() + return this.keyExpr_.toString() + "?" + this.parameters_.toString() } else { return this.keyExpr_.toString() } diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index 9a08de68..b79a7ec5 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -15,7 +15,7 @@ import { ZBytesDeserializer, ZBytesSerializer } from "./ext/index.js"; import { KeyExpr } from "./key_expr.js"; -import { DeclareLivelinessSubscriber, DeclareLivelinessToken, DeclarePublisher, DeclareQuerier, DeclareQueryable, DeclareSubscriber, Delete, deserializeHeader, Get, GetProperties, GetSessionInfo, InQuery, InRemoteMessageId, InReply, InSample, LivelinessGet, LivelinessGetProperties, LivelinessSubscriberProperties, OutMessageInterface, Ping, PublisherDelete, PublisherProperties, PublisherPut, Put, QuerierGet, QuerierGetProperties, QuerierProperties, QueryableProperties, QueryResponseFinal, ReplyDel, ReplyErr, ReplyOk, ResponseError, ResponseOk, ResponsePing, ResponseSessionInfo, ResponseTimestamp, serializeHeader, SubscriberProperties, UndeclareLivelinessToken, UndeclarePublisher, UndeclareQueryable, UndeclareSubscriber } from "./message.js"; +import { DeclareLivelinessSubscriber, DeclareLivelinessToken, DeclarePublisher, DeclareQuerier, DeclareQueryable, DeclareSubscriber, Delete, deserializeHeader, Get, GetProperties, GetSessionInfo, InQuery, InRemoteMessageId, InReply, InSample, LivelinessGet, LivelinessGetProperties, LivelinessSubscriberProperties, OutMessageInterface, Ping, PublisherDelete, PublisherProperties, PublisherPut, Put, QuerierGet, QuerierGetProperties, QuerierProperties, QueryableProperties, QueryResponseFinal, ReplyDel, ReplyErr, ReplyOk, ResponseError, ResponseOk, ResponsePing, ResponseSessionInfo, ResponseTimestamp, serializeHeader, SubscriberProperties, UndeclareLivelinessSubscriber, UndeclareLivelinessToken, UndeclarePublisher, UndeclareQuerier, UndeclareQueryable, UndeclareSubscriber } from "./message.js"; import { Query, Reply } from "./query.js"; import { Closure } from "./closure.js"; import { RemoteLink } from "./link.js"; @@ -209,12 +209,17 @@ export class SessionInner { async declareSubscriber(info: SubscriberProperties, closure: Closure): Promise { let subscriberId = this.subscriberIdCounter.get(); - await this.sendRequest( - new DeclareSubscriber(subscriberId, info), - InRemoteMessageId.ResponseOk, - ResponseOk.deserialize - ); this.subscribers.set(subscriberId, closure); + try { + await this.sendRequest( + new DeclareSubscriber(subscriberId, info), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + } catch (error) { + this.subscribers.delete(subscriberId); + throw error; + } return subscriberId; } @@ -271,7 +276,7 @@ export class SessionInner { async undeclareQuerier(querierId: number) { await this.sendRequest( - new UndeclareQueryable(querierId), + new UndeclareQuerier(querierId), InRemoteMessageId.ResponseOk, ResponseOk.deserialize ); @@ -297,12 +302,17 @@ export class SessionInner { async declareLivelinessSubscriber(info: LivelinessSubscriberProperties, closure: Closure): Promise { let subscriberId = this.subscriberIdCounter.get(); - await this.sendRequest( - new DeclareLivelinessSubscriber(subscriberId, info), - InRemoteMessageId.ResponseOk, - ResponseOk.deserialize - ); this.subscribers.set(subscriberId, closure); + try { + await this.sendRequest( + new DeclareLivelinessSubscriber(subscriberId, info), + InRemoteMessageId.ResponseOk, + ResponseOk.deserialize + ); + } catch (error) { + this.subscribers.delete(subscriberId); + throw error; + } return subscriberId; } @@ -315,7 +325,7 @@ export class SessionInner { subscriber.drop(); } await this.sendRequest( - new UndeclareSubscriber(subscriberId), + new UndeclareLivelinessSubscriber(subscriberId), InRemoteMessageId.ResponseOk, ResponseOk.deserialize ); @@ -360,19 +370,34 @@ export class SessionInner { async get(data: GetProperties, closure: Closure) { let getId = this.getIdCounter.get(); this.gets.set(getId, closure); - await this.sendMessage(new Get(getId, data)); + try { + await this.sendMessage(new Get(getId, data)); + } catch (error) { + this.gets.delete(getId); + throw error; + } } async querierGet(data: QuerierGetProperties, closure: Closure) { let getId = this.getIdCounter.get(); this.gets.set(getId, closure); - await this.sendMessage(new QuerierGet(getId, data)); + try { + await this.sendMessage(new QuerierGet(getId, data)); + } catch (error) { + this.gets.delete(getId); + throw error; + } } async livelinessGet(data: LivelinessGetProperties, closure: Closure) { let getId = this.getIdCounter.get(); this.gets.set(getId, closure); - await this.sendMessage(new LivelinessGet(getId, data)); + try { + await this.sendMessage(new LivelinessGet(getId, data)); + } catch (error) { + this.gets.delete(getId); + throw error; + } } async replyOk(data: ReplyOk) { diff --git a/zenoh-ts/src/zid.ts b/zenoh-ts/src/zid.ts index c16eda35..65ece664 100644 --- a/zenoh-ts/src/zid.ts +++ b/zenoh-ts/src/zid.ts @@ -22,7 +22,8 @@ export class ZenohId { toString() { let out: string = ""; - for (let b of this.zid) { + for (let i = this.zid.length - 1; i >= 0; --i) { + let b = this.zid[i] as number; out += ZenohId.KEY[b >> 4]; out += ZenohId.KEY[b & 15]; } From 2d46137e5b92626b8d84e05beb064fba8e7d7975 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 27 May 2025 16:27:12 +0200 Subject: [PATCH 13/23] test fixes --- zenoh-ts/src/encoding.ts | 16 ++++- zenoh-ts/tests/src/z_api_queryable_get.ts | 77 ++++++++++++++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/zenoh-ts/src/encoding.ts b/zenoh-ts/src/encoding.ts index e80ca6e7..e083bc18 100644 --- a/zenoh-ts/src/encoding.ts +++ b/zenoh-ts/src/encoding.ts @@ -105,7 +105,16 @@ export class Encoding { constructor(private id: EncodingPredefined, private schema?: string) {} withSchema(input: string): Encoding { - return new Encoding(this.id, input); + if (this.id != EncodingPredefined.CUSTOM || this.schema == undefined) { + return new Encoding(this.id, input); + } else { + const idx = this.schema.indexOf(Encoding.SEP); + if (idx == -1) { + return new Encoding(this.id, this.schema + ";" + input); + } else { + return new Encoding(this.id, this.schema.substring(0, idx) + ";" + input); + } + } } static default(): Encoding { @@ -136,10 +145,13 @@ export class Encoding { if (idx == -1) { key = input; } else { - key = input.substring(0, idx + 1); + key = input.substring(0, idx); schema = input.substring(idx + 1); } const id = Encoding.ENCODING_TO_ID.get(key) ?? EncodingPredefined.CUSTOM; + if (id == EncodingPredefined.CUSTOM) { + schema = schema ? key + ";" + schema : key; + } return new Encoding(id, schema); } diff --git a/zenoh-ts/tests/src/z_api_queryable_get.ts b/zenoh-ts/tests/src/z_api_queryable_get.ts index a1096ee0..85712740 100644 --- a/zenoh-ts/tests/src/z_api_queryable_get.ts +++ b/zenoh-ts/tests/src/z_api_queryable_get.ts @@ -12,7 +12,7 @@ // ZettaScale Zenoh Team, // -import { Config, Session, Query, Reply, KeyExpr, Selector, ReplyError, Parameters, Sample, QueryTarget, Queryable, ChannelReceiver, Querier } from "@eclipse-zenoh/zenoh-ts"; +import { Config, Session, Query, Reply, KeyExpr, Selector, ReplyError, Parameters, Sample, QueryTarget, Queryable, ChannelReceiver, Querier, ConsolidationMode } from "@eclipse-zenoh/zenoh-ts"; import { assertEquals } from "https://deno.land/std@0.192.0/testing/asserts.ts"; function sleep(ms: number) { @@ -42,6 +42,7 @@ Deno.test("API - Session Get with Callback", async () => { } else { query.replyErr("err"); } + query.finalize(); }, }); @@ -128,6 +129,7 @@ Deno.test("API - Session Get with Channel", async () => { assertEquals(query.parameters().toString(), "ok", "Parameters mismatch"); assertEquals(query.payload()?.toString(), "1", "Payload mismatch"); query.reply(query.keyExpr(), "1"); + query.finalize(); let reply = await receiver1.receive(); const result = reply.result(); @@ -149,6 +151,7 @@ Deno.test("API - Session Get with Channel", async () => { assertEquals(query2.parameters().toString(), "err", "Parameters mismatch"); assertEquals(query2.payload()?.toString(), "3", "Payload mismatch"); query2.replyErr("err"); + query2.finalize(); reply = await receiver2.receive(); const result2 = reply.result(); @@ -210,6 +213,7 @@ Deno.test("API - Querier Get with Channel", async () => { assertEquals(query.parameters().toString(), "ok", "Parameters mismatch"); assertEquals(query.payload()?.toString(), "1", "Payload mismatch"); query.reply(query.keyExpr(), "1"); + query.finalize(); // sleep to ensure the reply is processed await sleep(100); @@ -236,6 +240,7 @@ Deno.test("API - Querier Get with Channel", async () => { assertEquals(query2.parameters().toString(), "err", "Parameters mismatch"); assertEquals(query2.payload()?.toString(), "2", "Payload mismatch"); query2.replyErr("err"); + query2.finalize(); // sleep to ensure the reply is processed await sleep(100); @@ -307,6 +312,7 @@ Deno.test("API - Querier Get with Callback", async () => { assertEquals(query.parameters().toString(), "ok", "Parameters mismatch"); assertEquals(query.payload()?.toString(), "1", "Payload mismatch"); query.reply(query.keyExpr(), "1"); + query.finalize(); // sleep to ensure the reply is sent await sleep(200); @@ -330,6 +336,7 @@ Deno.test("API - Querier Get with Callback", async () => { assertEquals(query2.parameters().toString(), "err", "Parameters mismatch"); assertEquals(query2.payload()?.toString(), "2", "Payload mismatch"); query2.replyErr("err"); + query2.finalize(); // sleep to ensure the reply is processed await sleep(100); @@ -355,4 +362,72 @@ Deno.test("API - Querier Get with Callback", async () => { } await sleep(100); } +}); + +Deno.test("API - Session Get with multiple responses", async () => { + const ke = new KeyExpr("zenoh/test/*"); + const selector = new KeyExpr("zenoh/test/1"); + + let session1: Session | undefined; + let session2: Session | undefined; + let queryable: Queryable | undefined; + let receiver1: ChannelReceiver | undefined; + let receiver2: ChannelReceiver | undefined; + + try { + session1 = await Session.open(new Config("ws/127.0.0.1:10000")); + session2 = await Session.open(new Config("ws/127.0.0.1:10000")); + + queryable = await session1.declareQueryable(ke, { complete: true }); + + receiver1 = await session2.get(new Selector(selector, "ok"), { payload: "1", consolidation: ConsolidationMode.NONE }); + if (!receiver1) { + throw new Error("Failed to get receiver"); + } + + const query = await queryable.receiver()?.receive(); + if (!query) { + throw new Error("Failed to get query"); + } + + assertEquals(query.keyExpr().toString(), selector.toString(), "Key mismatch"); + assertEquals(query.parameters().toString(), "ok", "Parameters mismatch"); + assertEquals(query.payload()?.toString(), "1", "Payload mismatch"); + query.reply(query.keyExpr(), "1"); + query.reply(query.keyExpr(), "2"); + query.replyErr("3"); + query.finalize(); + + let replies = new Array(); + for await (const reply of receiver1) { + replies.push(reply); + } + assertEquals(replies.length, 3, "Should receive 3 replies"); + + let result = replies[0].result(); + assertEquals(result instanceof ReplyError, false, "Reply should be OK"); + assertEquals(result.payload().toString(), "1", "Reply payload mismatch"); + + result = replies[1].result(); + assertEquals(result instanceof ReplyError, false, "Reply should be OK"); + assertEquals(result.payload().toString(), "2", "Reply payload mismatch"); + + result = replies[2].result(); + assertEquals(result instanceof ReplyError, true, "Reply should be ERROR"); + assertEquals(result.payload().toString(), "3", "Reply payload mismatch"); + + + } finally { + // Cleanup in reverse order of creation + if (queryable) { + await queryable.undeclare(); + } + if (session2) { + await session2.close(); + } + if (session1) { + await session1.close(); + } + await sleep(100); + } }); \ No newline at end of file From 9fb3a377f980e7008a4c55b87b5f795d56fe1fd7 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 27 May 2025 16:31:32 +0200 Subject: [PATCH 14/23] fix browser example --- zenoh-ts/examples/browser/chat/src/chat_session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/zenoh-ts/examples/browser/chat/src/chat_session.ts b/zenoh-ts/examples/browser/chat/src/chat_session.ts index bd7ebb83..6529b268 100644 --- a/zenoh-ts/examples/browser/chat/src/chat_session.ts +++ b/zenoh-ts/examples/browser/chat/src/chat_session.ts @@ -131,6 +131,7 @@ export class ChatSession { query.reply(keyexpr, response, { attachment: this.user.username }); + query.finalize(); }, complete: true }); From 2c33e02757579ec4c5171ff74d1b191971c364ca Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 27 May 2025 16:43:03 +0200 Subject: [PATCH 15/23] lint fixes --- zenoh-plugin-remote-api/src/interface/mod.rs | 27 ++++++++++---------- zenoh-plugin-remote-api/src/lib.rs | 1 - zenoh-plugin-remote-api/src/remote_state.rs | 10 ++++---- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index d524731b..950094d0 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -23,7 +23,6 @@ use zenoh::{ query::{ConsolidationMode, QueryTarget, ReplyKeyExpr}, sample::{Locality, SampleKind}, }; - use zenoh_ext::{Deserialize, Serialize, ZDeserializeError, ZDeserializer, ZSerializer}; use zenoh_result::bail; @@ -273,7 +272,7 @@ fn encoding_to_id_schema(encoding: &Encoding) -> (u16, &[u8]) { } fn opt_encoding_from_id_schema(id_schema: Option<(u16, String)>) -> Option { - id_schema.map(|x| encoding_from_id_schema(x)) + id_schema.map(encoding_from_id_schema) } fn opt_timestamp_from_ntp_id( @@ -608,13 +607,13 @@ impl QuerierGet { fn serialize_sample(serializer: &mut ZSerializer, sample: &zenoh::sample::Sample) { serializer.serialize(sample.key_expr().as_str()); - serializer.serialize(&sample.payload().to_bytes()); + serializer.serialize(sample.payload().to_bytes()); serializer.serialize(sample_kind_to_u8(sample.kind())); - serializer.serialize(encoding_to_id_schema(&sample.encoding())); + serializer.serialize(encoding_to_id_schema(sample.encoding())); serialize_option(serializer, &sample.attachment().map(|a| a.to_bytes())); serialize_option( serializer, - &sample.timestamp().map(|t| timestamp_to_ntp_id(t)), + &sample.timestamp().map(timestamp_to_ntp_id), ); let qos = Qos::new( sample.priority(), @@ -633,7 +632,7 @@ pub(crate) struct Sample { impl Sample { pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { - serializer.serialize(&self.subscriber_id); + serializer.serialize(self.subscriber_id); serialize_sample(serializer, &self.sample); } } @@ -650,7 +649,7 @@ fn serialize_query(serializer: &mut ZSerializer, query: &zenoh::query::Query) { serialize_option(serializer, &query.payload().map(|p| p.to_bytes())); serialize_option( serializer, - &query.encoding().map(|e| encoding_to_id_schema(e)), + &query.encoding().map(encoding_to_id_schema), ); serialize_option(serializer, &query.attachment().map(|a| a.to_bytes())); serializer.serialize(reply_keyexpr_to_u8( @@ -662,8 +661,8 @@ fn serialize_query(serializer: &mut ZSerializer, query: &zenoh::query::Query) { impl Query { pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { - serializer.serialize(&self.queryable_id); - serializer.serialize(&self.query_id); + serializer.serialize(self.queryable_id); + serializer.serialize(self.query_id); serialize_query(serializer, &self.query); } } @@ -681,15 +680,15 @@ fn serialize_reply(serializer: &mut ZSerializer, reply: &zenoh::query::Reply) { } Err(e) => { serializer.serialize(false); - serializer.serialize(&e.payload().to_bytes()); - serializer.serialize(encoding_to_id_schema(&e.encoding())); + serializer.serialize(e.payload().to_bytes()); + serializer.serialize(encoding_to_id_schema(e.encoding())); } } } impl Reply { pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { - serializer.serialize(&self.query_id); + serializer.serialize(self.query_id); serialize_reply(serializer, &self.reply); } } @@ -760,7 +759,7 @@ pub(crate) struct QueryResponseFinal { impl QueryResponseFinal { pub(crate) fn to_wire(&self, serializer: &mut ZSerializer) { - serializer.serialize(&self.query_id); + serializer.serialize(self.query_id); } pub(crate) fn from_wire( @@ -957,7 +956,7 @@ macro_rules! remote_message { let mut t: $typ = $enum_name::$val.into(); match sequence_id { Some(id) => { - t = t | 0b10000000u8; + t |= 0b10000000u8; serializer.serialize(t); serializer.serialize(id); }, diff --git a/zenoh-plugin-remote-api/src/lib.rs b/zenoh-plugin-remote-api/src/lib.rs index d7d03127..d99c6259 100644 --- a/zenoh-plugin-remote-api/src/lib.rs +++ b/zenoh-plugin-remote-api/src/lib.rs @@ -48,7 +48,6 @@ use tokio_rustls::{ TlsAcceptor, }; use tokio_tungstenite::tungstenite::protocol::Message; -use tracing; use uuid::Uuid; use zenoh::{ bytes::{Encoding, ZBytes}, diff --git a/zenoh-plugin-remote-api/src/remote_state.rs b/zenoh-plugin-remote-api/src/remote_state.rs index 5b9c488e..a5dc1109 100644 --- a/zenoh-plugin-remote-api/src/remote_state.rs +++ b/zenoh-plugin-remote-api/src/remote_state.rs @@ -211,7 +211,7 @@ impl RemoteState { .callback(move |s| { let msg = interface::Sample { subscriber_id: declare_subscriber.id, - sample: s.into(), + sample: s, }; let _ = tx.send((OutRemoteMessage::Sample(msg), None)); }) @@ -439,7 +439,7 @@ impl RemoteState { callback: move |r: zenoh::query::Reply| { let msg = interface::Reply { query_id, - reply: r.into(), + reply: r, }; let _ = tx1.send((OutRemoteMessage::Reply(msg), None)); }, @@ -451,7 +451,7 @@ impl RemoteState { } async fn get(&self, get: Get) -> Result<(), zenoh_result::Error> { - let selector: Selector = match get.parameters.len() > 0 { + let selector: Selector = match !get.parameters.is_empty() { true => (get.keyexpr, get.parameters).into(), false => get.keyexpr.into(), }; @@ -492,7 +492,7 @@ impl RemoteState { if let Some(encoding) = querier_get.encoding { gb = gb.encoding(encoding); } - if querier_get.parameters.len() > 0 { + if !querier_get.parameters.is_empty() { gb = gb.parameters(querier_get.parameters); } @@ -642,7 +642,7 @@ impl RemoteState { .callback(move |s| { let msg = interface::Sample { subscriber_id: declare_liveliness_subscriber.id, - sample: s.into(), + sample: s, }; let _ = tx.send((OutRemoteMessage::Sample(msg), None)); }) From 08bd73c0926868a62ffe551a11a233cc5b39c1dd Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 27 May 2025 16:47:44 +0200 Subject: [PATCH 16/23] add test for 0 query replies --- zenoh-ts/tests/src/z_api_queryable_get.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/zenoh-ts/tests/src/z_api_queryable_get.ts b/zenoh-ts/tests/src/z_api_queryable_get.ts index 85712740..9f48091c 100644 --- a/zenoh-ts/tests/src/z_api_queryable_get.ts +++ b/zenoh-ts/tests/src/z_api_queryable_get.ts @@ -398,7 +398,7 @@ Deno.test("API - Session Get with multiple responses", async () => { query.replyErr("3"); query.finalize(); - let replies = new Array(); + let replies = new Array(); for await (const reply of receiver1) { replies.push(reply); } @@ -417,6 +417,23 @@ Deno.test("API - Session Get with multiple responses", async () => { assertEquals(result.payload().toString(), "3", "Reply payload mismatch"); + receiver2 = await session2.get(new Selector(selector, "ok"), { payload: "1", consolidation: ConsolidationMode.NONE }); + if (!receiver2) { + throw new Error("Failed to get receiver"); + } + + const query2 = await queryable.receiver()?.receive(); + if (!query2) { + throw new Error("Failed to get query"); + } + query2.finalize(); + + let replies2 = new Array(); + for await (const reply of receiver2) { + replies2.push(reply); + } + assertEquals(replies2.length, 0, "Should receive 0 replies"); + } finally { // Cleanup in reverse order of creation if (queryable) { From 020e4c018ad4355660bd2b1d83d5a41460c7b150 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 27 May 2025 16:50:55 +0200 Subject: [PATCH 17/23] fmt --- zenoh-plugin-remote-api/src/interface/mod.rs | 10 ++-------- zenoh-plugin-remote-api/src/remote_state.rs | 11 ++++------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/zenoh-plugin-remote-api/src/interface/mod.rs b/zenoh-plugin-remote-api/src/interface/mod.rs index 950094d0..a12255dd 100644 --- a/zenoh-plugin-remote-api/src/interface/mod.rs +++ b/zenoh-plugin-remote-api/src/interface/mod.rs @@ -611,10 +611,7 @@ fn serialize_sample(serializer: &mut ZSerializer, sample: &zenoh::sample::Sample serializer.serialize(sample_kind_to_u8(sample.kind())); serializer.serialize(encoding_to_id_schema(sample.encoding())); serialize_option(serializer, &sample.attachment().map(|a| a.to_bytes())); - serialize_option( - serializer, - &sample.timestamp().map(timestamp_to_ntp_id), - ); + serialize_option(serializer, &sample.timestamp().map(timestamp_to_ntp_id)); let qos = Qos::new( sample.priority(), sample.congestion_control(), @@ -647,10 +644,7 @@ fn serialize_query(serializer: &mut ZSerializer, query: &zenoh::query::Query) { serializer.serialize(query.key_expr().as_str()); serializer.serialize(query.parameters().as_str()); serialize_option(serializer, &query.payload().map(|p| p.to_bytes())); - serialize_option( - serializer, - &query.encoding().map(encoding_to_id_schema), - ); + serialize_option(serializer, &query.encoding().map(encoding_to_id_schema)); serialize_option(serializer, &query.attachment().map(|a| a.to_bytes())); serializer.serialize(reply_keyexpr_to_u8( query diff --git a/zenoh-plugin-remote-api/src/remote_state.rs b/zenoh-plugin-remote-api/src/remote_state.rs index a5dc1109..6717d5f5 100644 --- a/zenoh-plugin-remote-api/src/remote_state.rs +++ b/zenoh-plugin-remote-api/src/remote_state.rs @@ -436,11 +436,8 @@ impl RemoteState { let tx1 = self.tx.clone(); let tx2 = self.tx.clone(); CallbackDrop { - callback: move |r: zenoh::query::Reply| { - let msg = interface::Reply { - query_id, - reply: r, - }; + callback: move |reply: zenoh::query::Reply| { + let msg = interface::Reply { query_id, reply }; let _ = tx1.send((OutRemoteMessage::Reply(msg), None)); }, drop: move || { @@ -639,10 +636,10 @@ impl RemoteState { .liveliness() .declare_subscriber(declare_liveliness_subscriber.keyexpr) .history(declare_liveliness_subscriber.history) - .callback(move |s| { + .callback(move |sample| { let msg = interface::Sample { subscriber_id: declare_liveliness_subscriber.id, - sample: s, + sample, }; let _ = tx.send((OutRemoteMessage::Sample(msg), None)); }) From cab1f82c42318dbfb9763c4d24d0753fab95fa8d Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 28 May 2025 11:58:03 +0200 Subject: [PATCH 18/23] remove unnecessary ZBytes constructor call; allow to initialize deserializer from Uint8Array; --- zenoh-ts/src/ext/serialization.ts | 12 ++++++++---- zenoh-ts/src/session_inner.ts | 3 +-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/zenoh-ts/src/ext/serialization.ts b/zenoh-ts/src/ext/serialization.ts index 5a2d5821..6997ace6 100644 --- a/zenoh-ts/src/ext/serialization.ts +++ b/zenoh-ts/src/ext/serialization.ts @@ -525,7 +525,7 @@ export class ZBytesSerializer { this.data = new Array(); this.len = 0; this.bufferLen = 0; - return new ZBytes(out); + return out; } /** @@ -1051,11 +1051,15 @@ export class ZBytesDeserializer { private idx_: number /** * new function to create a ZBytesDeserializer - * @param p payload to deserialize. + * @param data payload to deserialize. * @returns ZBytesSerializer */ - constructor(zbytes: ZBytes) { - this.buffer_ = zbytes.toBytes() + constructor(data: ZBytes | Uint8Array) { + if (data instanceof ZBytes) { + this.buffer_ = data.toBytes() + } else { + this.buffer_ = data; + } this.idx_ = 0 } diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index b79a7ec5..3bbccf79 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -22,7 +22,6 @@ import { RemoteLink } from "./link.js"; import { Sample } from "./sample.js"; import { SessionInfo } from "./session.js"; import { Timestamp } from "./timestamp.js"; -import { ZBytes } from "./z_bytes.js"; class IdSource { private static MAX: number = 1 << 31; @@ -82,7 +81,7 @@ export class SessionInner { } private onMessageReceived(msg: Uint8Array) { - let deserializer = new ZBytesDeserializer(new ZBytes(msg)); + let deserializer = new ZBytesDeserializer(msg); let [messageId, sequenceId] = deserializeHeader(deserializer); if (sequenceId != undefined) { // received response to one of the messages let res = this.pendingMessageResponses.get(sequenceId); From d9dbcf5e02c92382b606f6d4d701eacdcec6898d Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 28 May 2025 12:00:06 +0200 Subject: [PATCH 19/23] fix AdminSapceClient::register_subscriber --- zenoh-plugin-remote-api/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zenoh-plugin-remote-api/src/lib.rs b/zenoh-plugin-remote-api/src/lib.rs index d99c6259..da280ccd 100644 --- a/zenoh-plugin-remote-api/src/lib.rs +++ b/zenoh-plugin-remote-api/src/lib.rs @@ -275,7 +275,7 @@ impl AdminSpaceClient { } pub(crate) fn register_subscriber(&mut self, id: u32, key_expr: &str) { - self.publishers.insert(id, key_expr.to_string()); + self.subscribers.insert(id, key_expr.to_string()); } pub(crate) fn register_queryable(&mut self, id: u32, key_expr: &str) { From c11afef72f8220cbb4d97aa9e9f62f0030b1de53 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 28 May 2025 12:33:41 +0200 Subject: [PATCH 20/23] do not use encoder/decoder for 0-length strings during serialziation/deserialization --- zenoh-ts/src/ext/serialization.ts | 20 ++++++++++++++------ zenoh-ts/src/session_inner.ts | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/zenoh-ts/src/ext/serialization.ts b/zenoh-ts/src/ext/serialization.ts index 6997ace6..1dd14562 100644 --- a/zenoh-ts/src/ext/serialization.ts +++ b/zenoh-ts/src/ext/serialization.ts @@ -170,10 +170,14 @@ export class ZBytesSerializer { * Serializes a utf-8 encoded string. */ public serializeString(val: string) { - const encoder = new TextEncoder(); - const encoded = encoder.encode(val); - this.writeSequenceLength(encoded.length) - this.append(encoded) + if (val.length == 0) { + this.writeSequenceLength(0); + } else { + const encoder = new TextEncoder(); + const encoded = encoder.encode(val); + this.writeSequenceLength(encoded.length) + this.append(encoded) + } } /** @@ -1099,8 +1103,12 @@ export class ZBytesDeserializer { */ public deserializeString(): string { let len = this.readSequenceLength() - const decoder = new TextDecoder() - return decoder.decode(this.readSlice(len)) + if (len == 0) { + return ""; + } else { + const decoder = new TextDecoder() + return decoder.decode(this.readSlice(len)) + } } /** diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index 3bbccf79..8df3f666 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -76,7 +76,7 @@ export class SessionInner { this.onMessageReceived(msg); } catch (e) { console.warn(e); - } + } }); } From 43a50345556ddaa9d9af16d18815c3d9b20aec89 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 28 May 2025 15:24:39 +0200 Subject: [PATCH 21/23] improve read/writeSequenceLenght performance for small sequences --- zenoh-ts/examples/deno/src/z_pub_thr.ts | 4 ++-- zenoh-ts/src/ext/serialization.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/zenoh-ts/examples/deno/src/z_pub_thr.ts b/zenoh-ts/examples/deno/src/z_pub_thr.ts index 4eef123b..78551ff7 100644 --- a/zenoh-ts/examples/deno/src/z_pub_thr.ts +++ b/zenoh-ts/examples/deno/src/z_pub_thr.ts @@ -42,9 +42,9 @@ export async function main() { await publisher.put(payload); n++; - if (n % 100000 == 0) { + if (n % 1000000 == 0) { const endTime = performance.now(); - console.log(100000 / (endTime - startTime) * 1000, " msg/s") + console.log(1000000 / (endTime - startTime) * 1000, " msg/s") startTime = performance.now(); } } diff --git a/zenoh-ts/src/ext/serialization.ts b/zenoh-ts/src/ext/serialization.ts index 1dd14562..d1e23f29 100644 --- a/zenoh-ts/src/ext/serialization.ts +++ b/zenoh-ts/src/ext/serialization.ts @@ -118,6 +118,7 @@ function isSerializeable(s: any): s is ZSerializeable { */ export class ZBytesSerializer { private static readonly DEFAULT_BUFFER_SIZE = 256; + private static readonly MAX_SEQUENCE_LENGTH_TO_FIT_IN_SINGLE_BYTE = 127; private buffer: Uint8Array; private bufferLen: number; private data: Uint8Array[]; @@ -159,6 +160,10 @@ export class ZBytesSerializer { * Serializes length of the sequence. Can be used when defining serialization for custom containers. */ public writeSequenceLength(len: number) { + if (len <= ZBytesSerializer.MAX_SEQUENCE_LENGTH_TO_FIT_IN_SINGLE_BYTE) { + this.serializeNumberUint8(len); + return; + } let a = this.ensureBuffer(10); let sz = leb.encodeULEB128Into(a, len); a = a.subarray(0, sz); @@ -1051,6 +1056,7 @@ export namespace ZS{ } export class ZBytesDeserializer { + private static readonly LEB128_CONTINUATION_MASK = 0b10000000; private buffer_: Uint8Array; private idx_: number /** @@ -1085,11 +1091,21 @@ export class ZBytesDeserializer { return b; } + private peekByte(): number | undefined { + return this.buffer_[this.idx_] + } + /** * Reads length of the sequence previously written by {@link ZBytesSerializer.writeSequenceLength} and advances the reading position. * @returns Number of sequence elements. */ public readSequenceLength(): number { + const b = this.peekByte(); + if (b != undefined && (b & ZBytesDeserializer.LEB128_CONTINUATION_MASK) == 0) { + this.idx_ += 1; + return b; + } + let [res, bytesRead] = leb.decodeULEB128(this.buffer_, this.idx_) this.idx_ += bytesRead if (res > Number.MAX_SAFE_INTEGER) { From 3040ef2b3a2ca9f9555a8a4ee947bf059d288de5 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 28 May 2025 18:14:24 +0200 Subject: [PATCH 22/23] add messageResponseTimeoutMs to the Config --- zenoh-ts/src/config.ts | 6 ++++-- zenoh-ts/src/session.ts | 2 +- zenoh-ts/src/session_inner.ts | 14 ++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/zenoh-ts/src/config.ts b/zenoh-ts/src/config.ts index e1c873f0..887ef8bd 100644 --- a/zenoh-ts/src/config.ts +++ b/zenoh-ts/src/config.ts @@ -24,11 +24,13 @@ export class Config { /** * Construct a new config, containing a locator - * @param {string} locator - A string that respects the Locator to connect to. Currently this can be only the address of zenohd websocket plugin. + * @param {string} locator - A string that respects the Locator to connect to. Currently this can be only the address of zenohd remote-api plugin websocket. * It accepts either * - zenoh canon form: `/
[?]` where can be `ws` and `wss` only, e.g. `ws/127.0.0.1:10000` * - common url form, e.g. `ws://127.0.0.1:10000` + * @param {number} messageResponseTimeoutMs - timeout value in milliseconds for receiving a response from zenoh-plugin-remote-api. + * Defaults to 500 ms. * @returns {Config} configuration instance */ - constructor(public locator: string) {} + constructor(public locator: string, public messageResponseTimeoutMs: number = 500) {} } diff --git a/zenoh-ts/src/session.ts b/zenoh-ts/src/session.ts index bd81b272..8f89f2f3 100644 --- a/zenoh-ts/src/session.ts +++ b/zenoh-ts/src/session.ts @@ -201,7 +201,7 @@ export class Session { */ static async open(config: Config): Promise { - let inner = await SessionInner.open(config.locator); + let inner = await SessionInner.open(config.locator, config.messageResponseTimeoutMs); return new Session(inner); } diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index 8df3f666..134d9a85 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -46,7 +46,6 @@ type OnResponseReceivedCallback = (msg: [InRemoteMessageId, ZBytesDeserializer]) export class SessionInner { - private static TIMEOUT_ERROR = new Error("Timeout"); private isClosed_: boolean = false; private link: RemoteLink; @@ -63,14 +62,13 @@ export class SessionInner { private subscribers: Map> = new Map>(); private queryables: Map> = new Map>(); private gets: Map> = new Map>(); - - private messageResponseTimeoutMs: number = 100; - private pendingMessageResponses: Map = new Map(); + private readonly messageResponseTimeoutMs: number; - private constructor(link: RemoteLink) { + private constructor(link: RemoteLink, messageResponseTimeoutMs: number) { this.link = link; + this.messageResponseTimeoutMs = messageResponseTimeoutMs; this.link.onmessage((msg: any) => { try { this.onMessageReceived(msg); @@ -157,7 +155,7 @@ export class SessionInner { }); const timeout = new Promise<[InRemoteMessageId, ZBytesDeserializer]>((_, reject) => - setTimeout(() => reject(SessionInner.TIMEOUT_ERROR), this.messageResponseTimeoutMs), + setTimeout(() => reject(new Error("Request timeout")), this.messageResponseTimeoutMs), ); await this.link.send(serializer.finish().toBytes()); @@ -180,9 +178,9 @@ export class SessionInner { return await this.sendRequest(new Ping, InRemoteMessageId.ResponsePing, ResponsePing.deserialize); } - static async open(locator: string): Promise { + static async open(locator: string, messageResponseTimeoutMs: number): Promise { let link = await RemoteLink.new(locator); - let session = new SessionInner(link); + let session = new SessionInner(link, messageResponseTimeoutMs); session.id = (await session.ping()).uuid; // verify connection console.log(`Successfully opened session with id: ${session.id}`); return session; From 21b28c2a4629851490f2b290d566841c55c29467 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 28 May 2025 20:55:11 +0200 Subject: [PATCH 23/23] clear error timeout upon remote api request reception --- zenoh-ts/src/session_inner.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/zenoh-ts/src/session_inner.ts b/zenoh-ts/src/session_inner.ts index 134d9a85..d3e9fc04 100644 --- a/zenoh-ts/src/session_inner.ts +++ b/zenoh-ts/src/session_inner.ts @@ -150,16 +150,16 @@ export class SessionInner { serializeHeader([msg.outMessageId, msgId], serializer); msg.serializeWithZSerializer(serializer); - const p = new Promise((resolve: OnResponseReceivedCallback, _) => { - this.pendingMessageResponses.set(msgId, resolve); + const p = new Promise((resolve: OnResponseReceivedCallback, reject) => { + let t = setTimeout(() => reject(), this.messageResponseTimeoutMs); + this.pendingMessageResponses.set(msgId, (arg: [InRemoteMessageId, ZBytesDeserializer]) => { + clearTimeout(t); + resolve(arg); + }); }); - - const timeout = new Promise<[InRemoteMessageId, ZBytesDeserializer]>((_, reject) => - setTimeout(() => reject(new Error("Request timeout")), this.messageResponseTimeoutMs), - ); await this.link.send(serializer.finish().toBytes()); - return await Promise.race([p, timeout]).then((r: [InRemoteMessageId, ZBytesDeserializer]) => { + return await p.then((r: [InRemoteMessageId, ZBytesDeserializer]) => { switch (r[0]) { case expectedResponseId: return deserialize(r[1]); case InRemoteMessageId.ResponseError: { @@ -168,9 +168,9 @@ export class SessionInner { } default: throw new Error(`Unexpected InRemoteMessageId ${r[0]}`); }; - }, (e: Error) => { + }, () => { this.pendingMessageResponses.delete(msgId); - throw e; + throw new Error("Remote api request timeout"); }); }