From 2b033a4c6375062ad4119b5a39e797c0b6942725 Mon Sep 17 00:00:00 2001 From: Vadim Obradovich Date: Thu, 28 Nov 2024 17:40:11 +0100 Subject: [PATCH] feat(net): implement Event listener (v2) (#691) --- net/rs/client-gen/src/ctor_generators.rs | 21 +-- net/rs/client-gen/src/events_generator.rs | 68 +++---- net/rs/client-gen/src/helpers.rs | 17 -- net/rs/client-gen/src/service_generators.rs | 14 +- .../snapshots/generator__basic_works.snap | 10 +- .../snapshots/generator__events_works.snap | 25 ++- .../tests/snapshots/generator__full.snap | 33 +++- .../generator__multiple_services.snap | 18 +- .../snapshots/generator__nonzero_works.snap | 8 +- .../snapshots/generator__rmrk_works.snap | 24 ++- .../ActionExtensions.cs | 1 - .../Core/EventListener.cs | 20 ++ .../Core/IRemoting.cs | 7 + .../Core/IRemotingListener.cs | 14 -- .../IRemotingListener.cs | 15 -- .../Core/BlockStreamEventListener.cs | 28 +++ .../Core/RemotingReplyViaNodeClient.cs | 15 +- .../Core/RemotingViaNodeClient.cs | 8 + .../Sails.Remoting/EventListenerExtensions.cs | 25 +++ net/src/Sails.Remoting/RemotingAction.cs | 38 ++-- .../Sails.Remoting/ServiceEventListener.cs | 72 +++++++ .../BlocksStreamExtensions.cs | 49 +++++ .../Model/gprimitives/ActorIdExtensions.cs | 11 +- net/src/Substrate.Gear.Client/GlobalUsings.cs | 3 + ...rTests.Generate_DemoIdl#Demo.g.verified.cs | 176 ++++++------------ .../Core/RemotingViaNodeClientTests.cs | 40 +++- 26 files changed, 465 insertions(+), 295 deletions(-) create mode 100644 net/src/Sails.Remoting.Abstractions/Core/EventListener.cs delete mode 100644 net/src/Sails.Remoting.Abstractions/Core/IRemotingListener.cs delete mode 100644 net/src/Sails.Remoting.Abstractions/IRemotingListener.cs create mode 100644 net/src/Sails.Remoting/Core/BlockStreamEventListener.cs create mode 100644 net/src/Sails.Remoting/EventListenerExtensions.cs create mode 100644 net/src/Sails.Remoting/ServiceEventListener.cs create mode 100644 net/src/Substrate.Gear.Client/BlocksStreamExtensions.cs diff --git a/net/rs/client-gen/src/ctor_generators.rs b/net/rs/client-gen/src/ctor_generators.rs index 27feaf11..33d74303 100644 --- a/net/rs/client-gen/src/ctor_generators.rs +++ b/net/rs/client-gen/src/ctor_generators.rs @@ -1,5 +1,4 @@ use crate::{helpers::*, type_decl_generators::*}; -use convert_case::{Case, Casing}; use csharp::Tokens; use genco::prelude::*; use sails_idl_parser::{ast::visitor, ast::visitor::Visitor, ast::*}; @@ -22,20 +21,21 @@ impl<'a> CtorFactoryGenerator<'a> { } pub(crate) fn finalize(self) -> Tokens { - let class_name = format!("{}Factory", self.service_name); + let class_name = &format!("{}Factory", self.service_name); let remoting = &csharp::import("global::Sails.Remoting.Abstractions.Core", "IRemoting"); quote! { - public interface I$(&class_name)$['\r'] + public interface I$class_name$['\r'] { $(self.interface_tokens) } $['\n'] - public sealed partial class $(&class_name) : I$(&class_name)$['\r'] - {$['\n'] + public sealed partial class $class_name : I$class_name$['\r'] + { + $['\n'] private readonly $remoting remoting; $['\n'] - public $(&class_name)($remoting remoting)$['\r'] + public $class_name($remoting remoting)$['\r'] { this.remoting = remoting; } @@ -53,13 +53,12 @@ impl<'a> Visitor<'a> for CtorFactoryGenerator<'a> { } fn visit_ctor_func(&mut self, func: &'a CtorFunc) { - let func_name_pascal = &func.name().to_case(Case::Pascal); + let func_name = func.name(); self.interface_tokens.push(); self.interface_tokens.append(summary_comment(func.docs())); self.interface_tokens.push(); - let route_bytes = &path_bytes(func.name()).0; let args = &encoded_fn_args_comma_prefixed(func.params()); let args_with_type = &self.type_generator.fn_params_with_types(func.params()); let void_type = primitive_type_to_dotnet(PrimitiveType::Null); @@ -68,14 +67,14 @@ impl<'a> Visitor<'a> for CtorFactoryGenerator<'a> { let action = &csharp::import("global::Sails.Remoting", "RemotingAction"); quote_in! { self.interface_tokens => - $activation $func_name_pascal($args_with_type);$['\r'] + $activation $func_name($args_with_type);$['\r'] }; quote_in! { self.class_tokens => $(inheritdoc()) - public $activation $func_name_pascal($args_with_type) + public $activation $func_name($args_with_type) { - return new $action<$(void_type)>(this.remoting, [$route_bytes]$args); + return new $action<$(void_type)>(this.remoting, nameof($func_name), string.Empty $args); } $['\n'] }; diff --git a/net/rs/client-gen/src/events_generator.rs b/net/rs/client-gen/src/events_generator.rs index c8fd1180..ab37243a 100644 --- a/net/rs/client-gen/src/events_generator.rs +++ b/net/rs/client-gen/src/events_generator.rs @@ -9,7 +9,7 @@ pub(crate) struct EventsGenerator<'a> { type_generator: TypeDeclGenerator<'a>, enum_tokens: Tokens, class_tokens: Tokens, - listener_tokens: Tokens, + event_routes_tokens: Tokens, } impl<'a> EventsGenerator<'a> { @@ -19,23 +19,21 @@ impl<'a> EventsGenerator<'a> { type_generator, enum_tokens: Tokens::new(), class_tokens: Tokens::new(), - listener_tokens: Tokens::new(), + event_routes_tokens: Tokens::new(), } } pub(crate) fn finalize(self) -> Tokens { - let name = &self.service_name.to_case(Case::Pascal); + let name = self.service_name; let enum_name = &format!("{}Events", name); let class_name = &format!("Enum{}Events", name); let listener_name = &format!("{}Listener", name); - let system_buffer = &csharp::import("global::System", "Buffer"); - let core_listener = &csharp::import( - "global::Sails.Remoting.Abstractions.Core", - "IRemotingListener", - ); - let service_listener = - &csharp::import("global::Sails.Remoting.Abstractions", "IRemotingListener"); + let remoting = &csharp::import("global::Sails.Remoting.Abstractions.Core", "IRemoting"); + let task = &csharp::import("global::System.Threading.Tasks", "Task"); + let cancellation_token = &csharp::import("global::System.Threading", "CancellationToken"); + let listener = &csharp::import("global::Sails.Remoting.Abstractions.Core", "EventListener"); + let actor_id_type = primitive_type_to_dotnet(PrimitiveType::ActorId); quote! { public enum $enum_name @@ -51,46 +49,27 @@ impl<'a> EventsGenerator<'a> { } } $['\n'] - public sealed partial class $listener_name : $service_listener<$class_name> + public sealed partial class $listener_name { - private static readonly byte[][] EventRoutes = + $['\n'] + private const string ROUTE = $(quoted(name)); + $['\n'] + private static readonly string[] EventRoutes = [ - $(self.listener_tokens) + $(self.event_routes_tokens) ]; $['\n'] - private readonly $core_listener remoting; + private readonly $remoting remoting; $['\n'] - public $listener_name($core_listener remoting) + public $listener_name($remoting remoting) { this.remoting = remoting; } $['\n'] - public async global::System.Collections.Generic.IAsyncEnumerable<$class_name> ListenAsync([global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) - { - await foreach (var bytes in this.remoting.ListenAsync(cancellationToken)) - { - byte idx = 0; - foreach (var route in EventRoutes) - { - if (route.Length > bytes.Length) - { - continue; - } - if (route.AsSpan().SequenceEqual(bytes.AsSpan()[..route.Length])) - { - var bytesLength = bytes.Length - route.Length + 1; - var data = new byte[bytesLength]; - data[0] = idx; - $system_buffer.BlockCopy(bytes, route.Length, data, 1, bytes.Length - route.Length); - - var p = 0; - $class_name ev = new(); - ev.Decode(bytes, ref p); - yield return ev; - } - idx++; - } - } + public async $task<$listener<($actor_id_type, $class_name)>> ListenAsync($cancellation_token cancellationToken = default) + {$['\r'] + var listener = await this.remoting.ListenAsync(cancellationToken);$['\r'] + return listener.ToServiceEventListener<$class_name>(ROUTE, EventRoutes);$['\r'] } } $['\n'] @@ -105,12 +84,9 @@ impl<'a> Visitor<'a> for EventsGenerator<'a> { fn visit_service_event(&mut self, event: &'a ServiceEvent) { let name = &self.service_name.to_case(Case::Pascal); - let service_route_bytes = path_bytes(self.service_name).0; - let event_route_bytes = path_bytes(event.name()).0; - let route_bytes = [service_route_bytes, event_route_bytes].join(", "); - quote_in! { self.listener_tokens => - [$(&route_bytes)], + quote_in! { self.event_routes_tokens => + $(quoted(event.name())), }; quote_in! { self.enum_tokens => diff --git a/net/rs/client-gen/src/helpers.rs b/net/rs/client-gen/src/helpers.rs index 0ccdb60c..e6ef84cc 100644 --- a/net/rs/client-gen/src/helpers.rs +++ b/net/rs/client-gen/src/helpers.rs @@ -3,25 +3,8 @@ use genco::{ lang::{csharp::Tokens, Csharp}, tokens::{FormatInto, ItemStr}, }; -use parity_scale_codec::Encode; use sails_idl_parser::ast::FuncParam; -pub(crate) fn path_bytes(path: &str) -> (String, usize) { - if path.is_empty() { - (String::new(), 0) - } else { - let service_path_bytes = path.encode(); - let service_path_encoded_length = service_path_bytes.len(); - let service_path_bytes = service_path_bytes - .into_iter() - .map(|x| x.to_string()) - .collect::>() - .join(", "); - - (service_path_bytes, service_path_encoded_length) - } -} - pub(crate) fn encoded_fn_args_comma_prefixed(params: &[FuncParam]) -> String { params .iter() diff --git a/net/rs/client-gen/src/service_generators.rs b/net/rs/client-gen/src/service_generators.rs index 9adaeee9..72224f76 100644 --- a/net/rs/client-gen/src/service_generators.rs +++ b/net/rs/client-gen/src/service_generators.rs @@ -1,5 +1,4 @@ use crate::{helpers::*, type_decl_generators::*}; -use convert_case::{Case, Casing}; use csharp::Tokens; use genco::prelude::*; use sails_idl_parser::{ast::visitor, ast::visitor::Visitor, ast::*}; @@ -23,7 +22,7 @@ impl<'a> ServiceClientGenerator<'a> { } pub(crate) fn finalize(self) -> Tokens { - let name = &self.service_name.to_case(Case::Pascal); + let name = &self.service_name; let remoting = &csharp::import("global::Sails.Remoting.Abstractions.Core", "IRemoting"); quote! { @@ -34,6 +33,9 @@ impl<'a> ServiceClientGenerator<'a> { $['\n'] public sealed partial class $name : I$name$['\r'] { + $['\n'] + private const string ROUTE = nameof($name); + $['\n'] private readonly $remoting remoting; $['\n'] public $name($remoting remoting) @@ -55,11 +57,7 @@ impl<'a> Visitor<'a> for ServiceClientGenerator<'a> { } fn visit_service_func(&mut self, func: &'a ServiceFunc) { - let func_name_pascal = &func.name().to_case(Case::Pascal); - - let service_route_bytes = path_bytes(self.service_name.as_str()).0; - let func_route_bytes = path_bytes(func.name()).0; - let route_bytes = [service_route_bytes, func_route_bytes].join(", "); + let func_name_pascal = func.name(); let args = &encoded_fn_args_comma_prefixed(func.params()); let args_with_type = &self.type_generator.fn_params_with_types(func.params()); @@ -78,7 +76,7 @@ impl<'a> Visitor<'a> for ServiceClientGenerator<'a> { $(inheritdoc()) public $return_type<$func_return_type> $func_name_pascal($args_with_type) { - return new $action<$func_return_type>(this.remoting, [$(&route_bytes)]$args); + return new $action<$func_return_type>(this.remoting, ROUTE, nameof($func_name_pascal) $args); } }; } diff --git a/net/rs/client-gen/tests/snapshots/generator__basic_works.snap b/net/rs/client-gen/tests/snapshots/generator__basic_works.snap index 7974674d..5f63cd56 100644 --- a/net/rs/client-gen/tests/snapshots/generator__basic_works.snap +++ b/net/rs/client-gen/tests/snapshots/generator__basic_works.snap @@ -21,14 +21,18 @@ ICall DoThat(global::Substrat } public sealed partial class Basic : IBasic - { private readonly IRemoting remoting; + { + + private const string ROUTE = nameof(Basic); + + private readonly IRemoting remoting; public Basic(IRemoting remoting) { this.remoting = remoting; } /// - public ICall DoThis(global::Substrate.NetApi.Model.Types.Primitive.U32 p1, MyParam p2) { return new RemotingAction(this.remoting, [20, 66, 97, 115, 105, 99, 24, 68, 111, 84, 104, 105, 115], p1, p2); } + public ICall DoThis(global::Substrate.NetApi.Model.Types.Primitive.U32 p1, MyParam p2) { return new RemotingAction(this.remoting, ROUTE, nameof(DoThis) , p1, p2); } /// - public ICall DoThat(global::Substrate.NetApi.Model.Types.Base.BaseTuple p1) { return new RemotingAction(this.remoting, [20, 66, 97, 115, 105, 99, 24, 68, 111, 84, 104, 97, 116], p1); } } + public ICall DoThat(global::Substrate.NetApi.Model.Types.Base.BaseTuple p1) { return new RemotingAction(this.remoting, ROUTE, nameof(DoThat) , p1); } } public sealed partial class MyParam : global::Substrate.NetApi.Model.Types.Base.BaseType { public global::Substrate.NetApi.Model.Types.Primitive.U32 F1 { get; init; } = new(); diff --git a/net/rs/client-gen/tests/snapshots/generator__events_works.snap b/net/rs/client-gen/tests/snapshots/generator__events_works.snap index 277b30b4..8723b011 100644 --- a/net/rs/client-gen/tests/snapshots/generator__events_works.snap +++ b/net/rs/client-gen/tests/snapshots/generator__events_works.snap @@ -8,6 +8,8 @@ using global::Sails.Remoting.Abstractions; using global::Sails.Remoting.Abstractions.Core; using global::System; using global::System.Collections.Generic; +using global::System.Threading; +using global::System.Threading.Tasks; #nullable enable @@ -20,12 +22,16 @@ public interface IServiceWithEvents } public sealed partial class ServiceWithEvents : IServiceWithEvents - { private readonly IRemoting remoting; + { + + private const string ROUTE = nameof(ServiceWithEvents); + + private readonly IRemoting remoting; public ServiceWithEvents(IRemoting remoting) { this.remoting = remoting; } /// - public ICall DoThis(global::Substrate.Gear.Client.NetApi.Model.Types.Primitive.NonZeroU256 p1, MyParam p2) { return new RemotingAction(this.remoting, [68, 83, 101, 114, 118, 105, 99, 101, 87, 105, 116, 104, 69, 118, 101, 110, 116, 115, 24, 68, 111, 84, 104, 105, 115], p1, p2); } } + public ICall DoThis(global::Substrate.Gear.Client.NetApi.Model.Types.Primitive.NonZeroU256 p1, MyParam p2) { return new RemotingAction(this.remoting, ROUTE, nameof(DoThis) , p1, p2); } } public enum ServiceWithEventsEvents { One, Two, @@ -39,13 +45,20 @@ this.AddTypeDecoder(ServiceWithEventsEvents.Three); this.AddTypeDecoder(ServiceWithEventsEvents.Reset); } } - public sealed partial class ServiceWithEventsListener : IRemotingListener { private static readonly byte[][] EventRoutes = [ [68, 83, 101, 114, 118, 105, 99, 101, 87, 105, 116, 104, 69, 118, 101, 110, 116, 115, 12, 79, 110, 101],[68, 83, 101, 114, 118, 105, 99, 101, 87, 105, 116, 104, 69, 118, 101, 110, 116, 115, 12, 84, 119, 111],[68, 83, 101, 114, 118, 105, 99, 101, 87, 105, 116, 104, 69, 118, 101, 110, 116, 115, 20, 84, 104, 114, 101, 101],[68, 83, 101, 114, 118, 105, 99, 101, 87, 105, 116, 104, 69, 118, 101, 110, 116, 115, 20, 82, 101, 115, 101, 116], ]; + public sealed partial class ServiceWithEventsListener { + + private const string ROUTE = "ServiceWithEvents"; - private readonly global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting; + private static readonly string[] EventRoutes = [ "One","Two","Three","Reset", ]; - public ServiceWithEventsListener(global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting) { this.remoting = remoting; } + private readonly IRemoting remoting; - public async global::System.Collections.Generic.IAsyncEnumerable ListenAsync([global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) { await foreach (var bytes in this.remoting.ListenAsync(cancellationToken)) { byte idx = 0; foreach (var route in EventRoutes) { if (route.Length > bytes.Length) { continue; } if (route.AsSpan().SequenceEqual(bytes.AsSpan()[..route.Length])) { var bytesLength = bytes.Length - route.Length + 1; var data = new byte[bytesLength]; data[0] = idx; Buffer.BlockCopy(bytes, route.Length, data, 1, bytes.Length - route.Length); var p = 0; EnumServiceWithEventsEvents ev = new(); ev.Decode(bytes, ref p); yield return ev; } idx++; } } } } + public ServiceWithEventsListener(IRemoting remoting) { this.remoting = remoting; } + + public async Task> ListenAsync(CancellationToken cancellationToken = default) { + var listener = await this.remoting.ListenAsync(cancellationToken); + return listener.ToServiceEventListener(ROUTE, EventRoutes); + } } public sealed partial class MyParam : global::Substrate.NetApi.Model.Types.Base.BaseType { public global::Substrate.Gear.Client.NetApi.Model.Types.Primitive.NonZeroU256 F1 { get; init; } = new(); diff --git a/net/rs/client-gen/tests/snapshots/generator__full.snap b/net/rs/client-gen/tests/snapshots/generator__full.snap index 2833129d..29074a9e 100644 --- a/net/rs/client-gen/tests/snapshots/generator__full.snap +++ b/net/rs/client-gen/tests/snapshots/generator__full.snap @@ -8,6 +8,8 @@ using global::Sails.Remoting.Abstractions; using global::Sails.Remoting.Abstractions.Core; using global::System; using global::System.Collections.Generic; +using global::System.Threading; +using global::System.Threading.Tasks; #nullable enable @@ -32,7 +34,7 @@ IActivation New(global::Substrate.NetApi.Model.Types.Primitive.U32 a); { this.remoting = remoting; } /// - public IActivation New(global::Substrate.NetApi.Model.Types.Primitive.U32 a) { return new RemotingAction(this.remoting, [12, 78, 101, 119], a); } + public IActivation New(global::Substrate.NetApi.Model.Types.Primitive.U32 a) { return new RemotingAction(this.remoting, nameof(New), string.Empty , a); } } @@ -44,18 +46,22 @@ IQuery - public ICall> DoThis(global::Substrate.NetApi.Model.Types.Primitive.U32 p1, global::Substrate.NetApi.Model.Types.Primitive.Str p2, global::Substrate.NetApi.Model.Types.Base.BaseTuple, global::Substrate.NetApi.Model.Types.Primitive.U8> p3, ThisThatSvcAppTupleStruct p4) { return new RemotingAction>(this.remoting, [28, 83, 101, 114, 118, 105, 99, 101, 24, 68, 111, 84, 104, 105, 115], p1, p2, p3, p4); } + public ICall> DoThis(global::Substrate.NetApi.Model.Types.Primitive.U32 p1, global::Substrate.NetApi.Model.Types.Primitive.Str p2, global::Substrate.NetApi.Model.Types.Base.BaseTuple, global::Substrate.NetApi.Model.Types.Primitive.U8> p3, ThisThatSvcAppTupleStruct p4) { return new RemotingAction>(this.remoting, ROUTE, nameof(DoThis) , p1, p2, p3, p4); } /// - public ICall, global::Substrate.NetApi.Model.Types.Primitive.Str>> DoThat(ThisThatSvcAppDoThatParam param) { return new RemotingAction, global::Substrate.NetApi.Model.Types.Primitive.Str>>(this.remoting, [28, 83, 101, 114, 118, 105, 99, 101, 24, 68, 111, 84, 104, 97, 116], param); } + public ICall, global::Substrate.NetApi.Model.Types.Primitive.Str>> DoThat(ThisThatSvcAppDoThatParam param) { return new RemotingAction, global::Substrate.NetApi.Model.Types.Primitive.Str>>(this.remoting, ROUTE, nameof(DoThat) , param); } /// - public IQuery This(global::Substrate.NetApi.Model.Types.Base.BaseVec v1) { return new RemotingAction(this.remoting, [28, 83, 101, 114, 118, 105, 99, 101, 16, 84, 104, 105, 115], v1); } + public IQuery This(global::Substrate.NetApi.Model.Types.Base.BaseVec v1) { return new RemotingAction(this.remoting, ROUTE, nameof(This) , v1); } /// - public IQuery> That(global::Substrate.NetApi.Model.Types.Base.BaseVoid v1) { return new RemotingAction>(this.remoting, [28, 83, 101, 114, 118, 105, 99, 101, 16, 84, 104, 97, 116], v1); } } + public IQuery> That(global::Substrate.NetApi.Model.Types.Base.BaseVoid v1) { return new RemotingAction>(this.remoting, ROUTE, nameof(That) , v1); } } public enum ServiceEvents { /// @@ -72,13 +78,20 @@ public enum ServiceEvents { this.AddTypeDecoder(ServiceEvents.ThatDone); } } - public sealed partial class ServiceListener : IRemotingListener { private static readonly byte[][] EventRoutes = [ [28, 83, 101, 114, 118, 105, 99, 101, 32, 84, 104, 105, 115, 68, 111, 110, 101],[28, 83, 101, 114, 118, 105, 99, 101, 32, 84, 104, 97, 116, 68, 111, 110, 101], ]; + public sealed partial class ServiceListener { - private readonly global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting; + private const string ROUTE = "Service"; - public ServiceListener(global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting) { this.remoting = remoting; } + private static readonly string[] EventRoutes = [ "ThisDone","ThatDone", ]; - public async global::System.Collections.Generic.IAsyncEnumerable ListenAsync([global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) { await foreach (var bytes in this.remoting.ListenAsync(cancellationToken)) { byte idx = 0; foreach (var route in EventRoutes) { if (route.Length > bytes.Length) { continue; } if (route.AsSpan().SequenceEqual(bytes.AsSpan()[..route.Length])) { var bytesLength = bytes.Length - route.Length + 1; var data = new byte[bytesLength]; data[0] = idx; Buffer.BlockCopy(bytes, route.Length, data, 1, bytes.Length - route.Length); var p = 0; EnumServiceEvents ev = new(); ev.Decode(bytes, ref p); yield return ev; } idx++; } } } } + private readonly IRemoting remoting; + + public ServiceListener(IRemoting remoting) { this.remoting = remoting; } + + public async Task> ListenAsync(CancellationToken cancellationToken = default) { + var listener = await this.remoting.ListenAsync(cancellationToken); + return listener.ToServiceEventListener(ROUTE, EventRoutes); + } } /// /// ThisThatSvcAppTupleStruct docs diff --git a/net/rs/client-gen/tests/snapshots/generator__multiple_services.snap b/net/rs/client-gen/tests/snapshots/generator__multiple_services.snap index 42051a25..dd2e78df 100644 --- a/net/rs/client-gen/tests/snapshots/generator__multiple_services.snap +++ b/net/rs/client-gen/tests/snapshots/generator__multiple_services.snap @@ -19,23 +19,31 @@ ICall DoThat(global::Substrat } public sealed partial class Multiple : IMultiple - { private readonly IRemoting remoting; + { + + private const string ROUTE = nameof(Multiple); + + private readonly IRemoting remoting; public Multiple(IRemoting remoting) { this.remoting = remoting; } /// - public ICall DoThis(global::Substrate.NetApi.Model.Types.Primitive.U32 p1, MyParam p2) { return new RemotingAction(this.remoting, [32, 77, 117, 108, 116, 105, 112, 108, 101, 24, 68, 111, 84, 104, 105, 115], p1, p2); } + public ICall DoThis(global::Substrate.NetApi.Model.Types.Primitive.U32 p1, MyParam p2) { return new RemotingAction(this.remoting, ROUTE, nameof(DoThis) , p1, p2); } /// - public ICall DoThat(global::Substrate.NetApi.Model.Types.Base.BaseTuple p1) { return new RemotingAction(this.remoting, [32, 77, 117, 108, 116, 105, 112, 108, 101, 24, 68, 111, 84, 104, 97, 116], p1); } } + public ICall DoThat(global::Substrate.NetApi.Model.Types.Base.BaseTuple p1) { return new RemotingAction(this.remoting, ROUTE, nameof(DoThat) , p1); } } public interface INamed { IQuery That(global::Substrate.NetApi.Model.Types.Primitive.U32 p1); } public sealed partial class Named : INamed - { private readonly IRemoting remoting; + { + + private const string ROUTE = nameof(Named); + + private readonly IRemoting remoting; public Named(IRemoting remoting) { this.remoting = remoting; } /// - public IQuery That(global::Substrate.NetApi.Model.Types.Primitive.U32 p1) { return new RemotingAction(this.remoting, [20, 78, 97, 109, 101, 100, 16, 84, 104, 97, 116], p1); } } + public IQuery That(global::Substrate.NetApi.Model.Types.Primitive.U32 p1) { return new RemotingAction(this.remoting, ROUTE, nameof(That) , p1); } } diff --git a/net/rs/client-gen/tests/snapshots/generator__nonzero_works.snap b/net/rs/client-gen/tests/snapshots/generator__nonzero_works.snap index a9b2f3ab..9e2030f7 100644 --- a/net/rs/client-gen/tests/snapshots/generator__nonzero_works.snap +++ b/net/rs/client-gen/tests/snapshots/generator__nonzero_works.snap @@ -20,12 +20,16 @@ public interface INonZeroParams } public sealed partial class NonZeroParams : INonZeroParams - { private readonly IRemoting remoting; + { + + private const string ROUTE = nameof(NonZeroParams); + + private readonly IRemoting remoting; public NonZeroParams(IRemoting remoting) { this.remoting = remoting; } /// - public ICall DoThis(global::Substrate.Gear.Client.NetApi.Model.Types.Primitive.NonZeroU256 p1, MyParam p2) { return new RemotingAction(this.remoting, [52, 78, 111, 110, 90, 101, 114, 111, 80, 97, 114, 97, 109, 115, 24, 68, 111, 84, 104, 105, 115], p1, p2); } } + public ICall DoThis(global::Substrate.Gear.Client.NetApi.Model.Types.Primitive.NonZeroU256 p1, MyParam p2) { return new RemotingAction(this.remoting, ROUTE, nameof(DoThis) , p1, p2); } } public sealed partial class MyParam : global::Substrate.NetApi.Model.Types.Base.BaseType { public global::Substrate.Gear.Client.NetApi.Model.Types.Primitive.NonZeroU256 F1 { get; init; } = new(); diff --git a/net/rs/client-gen/tests/snapshots/generator__rmrk_works.snap b/net/rs/client-gen/tests/snapshots/generator__rmrk_works.snap index 6d04d28b..88bd1d72 100644 --- a/net/rs/client-gen/tests/snapshots/generator__rmrk_works.snap +++ b/net/rs/client-gen/tests/snapshots/generator__rmrk_works.snap @@ -29,7 +29,7 @@ IActivation New(); { this.remoting = remoting; } /// - public IActivation New() { return new RemotingAction(this.remoting, [12, 78, 101, 119]); } + public IActivation New() { return new RemotingAction(this.remoting, nameof(New), string.Empty ); } } @@ -45,26 +45,30 @@ IQuery> Part(global::Sub } public sealed partial class RmrkCatalog : IRmrkCatalog - { private readonly IRemoting remoting; + { + + private const string ROUTE = nameof(RmrkCatalog); + + private readonly IRemoting remoting; public RmrkCatalog(IRemoting remoting) { this.remoting = remoting; } /// - public ICall>, Error>> AddEquippables(global::Substrate.NetApi.Model.Types.Primitive.U32 partId, global::Substrate.NetApi.Model.Types.Base.BaseVec collectionIds) { return new RemotingAction>, Error>>(this.remoting, [44, 82, 109, 114, 107, 67, 97, 116, 97, 108, 111, 103, 56, 65, 100, 100, 69, 113, 117, 105, 112, 112, 97, 98, 108, 101, 115], partId, collectionIds); } + public ICall>, Error>> AddEquippables(global::Substrate.NetApi.Model.Types.Primitive.U32 partId, global::Substrate.NetApi.Model.Types.Base.BaseVec collectionIds) { return new RemotingAction>, Error>>(this.remoting, ROUTE, nameof(AddEquippables) , partId, collectionIds); } /// - public ICall, Error>> AddParts(global::Substrate.Gear.Client.NetApi.Model.Types.Base.BaseDictionary parts) { return new RemotingAction, Error>>(this.remoting, [44, 82, 109, 114, 107, 67, 97, 116, 97, 108, 111, 103, 32, 65, 100, 100, 80, 97, 114, 116, 115], parts); } + public ICall, Error>> AddParts(global::Substrate.Gear.Client.NetApi.Model.Types.Base.BaseDictionary parts) { return new RemotingAction, Error>>(this.remoting, ROUTE, nameof(AddParts) , parts); } /// - public ICall, Error>> RemoveEquippable(global::Substrate.NetApi.Model.Types.Primitive.U32 partId, global::Substrate.Gear.Api.Generated.Model.gprimitives.ActorId collectionId) { return new RemotingAction, Error>>(this.remoting, [44, 82, 109, 114, 107, 67, 97, 116, 97, 108, 111, 103, 64, 82, 101, 109, 111, 118, 101, 69, 113, 117, 105, 112, 112, 97, 98, 108, 101], partId, collectionId); } + public ICall, Error>> RemoveEquippable(global::Substrate.NetApi.Model.Types.Primitive.U32 partId, global::Substrate.Gear.Api.Generated.Model.gprimitives.ActorId collectionId) { return new RemotingAction, Error>>(this.remoting, ROUTE, nameof(RemoveEquippable) , partId, collectionId); } /// - public ICall, Error>> RemoveParts(global::Substrate.NetApi.Model.Types.Base.BaseVec partIds) { return new RemotingAction, Error>>(this.remoting, [44, 82, 109, 114, 107, 67, 97, 116, 97, 108, 111, 103, 44, 82, 101, 109, 111, 118, 101, 80, 97, 114, 116, 115], partIds); } + public ICall, Error>> RemoveParts(global::Substrate.NetApi.Model.Types.Base.BaseVec partIds) { return new RemotingAction, Error>>(this.remoting, ROUTE, nameof(RemoveParts) , partIds); } /// - public ICall> ResetEquippables(global::Substrate.NetApi.Model.Types.Primitive.U32 partId) { return new RemotingAction>(this.remoting, [44, 82, 109, 114, 107, 67, 97, 116, 97, 108, 111, 103, 64, 82, 101, 115, 101, 116, 69, 113, 117, 105, 112, 112, 97, 98, 108, 101, 115], partId); } + public ICall> ResetEquippables(global::Substrate.NetApi.Model.Types.Primitive.U32 partId) { return new RemotingAction>(this.remoting, ROUTE, nameof(ResetEquippables) , partId); } /// - public ICall> SetEquippablesToAll(global::Substrate.NetApi.Model.Types.Primitive.U32 partId) { return new RemotingAction>(this.remoting, [44, 82, 109, 114, 107, 67, 97, 116, 97, 108, 111, 103, 76, 83, 101, 116, 69, 113, 117, 105, 112, 112, 97, 98, 108, 101, 115, 84, 111, 65, 108, 108], partId); } + public ICall> SetEquippablesToAll(global::Substrate.NetApi.Model.Types.Primitive.U32 partId) { return new RemotingAction>(this.remoting, ROUTE, nameof(SetEquippablesToAll) , partId); } /// - public IQuery> Equippable(global::Substrate.NetApi.Model.Types.Primitive.U32 partId, global::Substrate.Gear.Api.Generated.Model.gprimitives.ActorId collectionId) { return new RemotingAction>(this.remoting, [44, 82, 109, 114, 107, 67, 97, 116, 97, 108, 111, 103, 40, 69, 113, 117, 105, 112, 112, 97, 98, 108, 101], partId, collectionId); } + public IQuery> Equippable(global::Substrate.NetApi.Model.Types.Primitive.U32 partId, global::Substrate.Gear.Api.Generated.Model.gprimitives.ActorId collectionId) { return new RemotingAction>(this.remoting, ROUTE, nameof(Equippable) , partId, collectionId); } /// - public IQuery> Part(global::Substrate.NetApi.Model.Types.Primitive.U32 partId) { return new RemotingAction>(this.remoting, [44, 82, 109, 114, 107, 67, 97, 116, 97, 108, 111, 103, 16, 80, 97, 114, 116], partId); } } + public IQuery> Part(global::Substrate.NetApi.Model.Types.Primitive.U32 partId) { return new RemotingAction>(this.remoting, ROUTE, nameof(Part) , partId); } } public enum Error { PartIdCantBeZero, BadConfig, diff --git a/net/src/Sails.Remoting.Abstractions/ActionExtensions.cs b/net/src/Sails.Remoting.Abstractions/ActionExtensions.cs index 62949b18..55071ba6 100644 --- a/net/src/Sails.Remoting.Abstractions/ActionExtensions.cs +++ b/net/src/Sails.Remoting.Abstractions/ActionExtensions.cs @@ -26,7 +26,6 @@ public static async Task SendReceiveAsync( return await reply.ReceiveAsync(cancellationToken).ConfigureAwait(false); } - /// /// Sends a message to a program for execution and receive reply /// diff --git a/net/src/Sails.Remoting.Abstractions/Core/EventListener.cs b/net/src/Sails.Remoting.Abstractions/Core/EventListener.cs new file mode 100644 index 00000000..6622421f --- /dev/null +++ b/net/src/Sails.Remoting.Abstractions/Core/EventListener.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Sails.Remoting.Abstractions.Core; + +public abstract class EventListener : IAsyncDisposable +{ + public async ValueTask DisposeAsync() + { + await this.DisposeCoreAsync().ConfigureAwait(false); + + GC.SuppressFinalize(false); + } + + protected abstract ValueTask DisposeCoreAsync(); + + public abstract IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken); +} diff --git a/net/src/Sails.Remoting.Abstractions/Core/IRemoting.cs b/net/src/Sails.Remoting.Abstractions/Core/IRemoting.cs index 223cfd19..06c49007 100644 --- a/net/src/Sails.Remoting.Abstractions/Core/IRemoting.cs +++ b/net/src/Sails.Remoting.Abstractions/Core/IRemoting.cs @@ -56,4 +56,11 @@ Task QueryAsync( GasUnit? gasLimit, ValueUnit value, CancellationToken cancellationToken); + + /// + /// Asynchronously subscribe to Gear events. + /// + /// + /// + Task> ListenAsync(CancellationToken cancellationToken); } diff --git a/net/src/Sails.Remoting.Abstractions/Core/IRemotingListener.cs b/net/src/Sails.Remoting.Abstractions/Core/IRemotingListener.cs deleted file mode 100644 index f5d72ebe..00000000 --- a/net/src/Sails.Remoting.Abstractions/Core/IRemotingListener.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using System.Threading; - -namespace Sails.Remoting.Abstractions.Core; - -public interface IRemotingListener -{ - /// - /// Listen to Gear events - /// - /// - /// - IAsyncEnumerable ListenAsync(CancellationToken cancellationToken); -} diff --git a/net/src/Sails.Remoting.Abstractions/IRemotingListener.cs b/net/src/Sails.Remoting.Abstractions/IRemotingListener.cs deleted file mode 100644 index 9f319f43..00000000 --- a/net/src/Sails.Remoting.Abstractions/IRemotingListener.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using Substrate.NetApi.Model.Types; - -namespace Sails.Remoting.Abstractions; - -public interface IRemotingListener where T : IType, new() -{ - /// - /// Listen to Service events - /// - /// - /// - IAsyncEnumerable ListenAsync(CancellationToken cancellationToken); -} diff --git a/net/src/Sails.Remoting/Core/BlockStreamEventListener.cs b/net/src/Sails.Remoting/Core/BlockStreamEventListener.cs new file mode 100644 index 00000000..8fa293bd --- /dev/null +++ b/net/src/Sails.Remoting/Core/BlockStreamEventListener.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Sails.Remoting.Abstractions.Core; +using Substrate.Gear.Api.Generated; +using Substrate.Gear.Api.Generated.Model.gprimitives; +using Substrate.Gear.Client; + +namespace Sails.Remoting.Core; + +internal sealed class BlockStreamEventListener : EventListener<(ActorId Source, byte[] Bytes)> +{ + private readonly SubstrateClientExt nodeClient; + private readonly BlocksStream blocksStream; + + internal BlockStreamEventListener(SubstrateClientExt nodeClient, BlocksStream blocksStream) + { + this.nodeClient = nodeClient; + this.blocksStream = blocksStream; + } + + public override IAsyncEnumerable<(ActorId Source, byte[] Bytes)> ReadAllAsync(CancellationToken cancellationToken) + => this.blocksStream.ReadAllHeadersAsync(cancellationToken) + .SelectGearEvents(this.nodeClient, cancellationToken) + .SelectServiceEvents(); + + protected override ValueTask DisposeCoreAsync() => this.blocksStream.DisposeAsync(); +} diff --git a/net/src/Sails.Remoting/Core/RemotingReplyViaNodeClient.cs b/net/src/Sails.Remoting/Core/RemotingReplyViaNodeClient.cs index d0109dbe..47e0ccdd 100644 --- a/net/src/Sails.Remoting/Core/RemotingReplyViaNodeClient.cs +++ b/net/src/Sails.Remoting/Core/RemotingReplyViaNodeClient.cs @@ -4,13 +4,10 @@ using System.Threading.Tasks; using EnsureThat; using Sails.Remoting.Abstractions.Core; -using StreamJsonRpc; using Substrate.Gear.Api.Generated; using Substrate.Gear.Api.Generated.Model.gear_core.message.user; using Substrate.Gear.Api.Generated.Model.gprimitives; -using Substrate.Gear.Api.Generated.Model.vara_runtime; using Substrate.Gear.Client; -using Substrate.Gear.Client.NetApi.Model.Rpc; using Substrate.Gear.Client.NetApi.Model.Types.Base; namespace Sails.Remoting.Core; @@ -89,17 +86,7 @@ public override async Task ReadAsync(CancellationToken cancellationToken) Ensure.Any.IsNotNull(this.blocksStream, nameof(this.blocksStream)); this.replyMessage = await this.blocksStream.ReadAllHeadersAsync(cancellationToken) - .SelectAwait( - async blockHeader => - await this.nodeClient.ListBlockEventsAsync(blockHeader.GetBlockHash(), cancellationToken) - .ConfigureAwait(false)) - .SelectMany( - eventRecords => eventRecords.AsAsyncEnumerable()) - .Select( - eventRecord => eventRecord.Event.ToBaseEnumRust()) - .SelectIfMatches( - RuntimeEvent.Gear, - (EnumGearEvent gearEvent) => gearEvent.ToBaseEnumRust()) + .SelectGearEvents(this.nodeClient, cancellationToken) .SelectIfMatches( GearEvent.UserMessageSent, (UserMessageSentEventData data) => (UserMessage)data.Value[0]) diff --git a/net/src/Sails.Remoting/Core/RemotingViaNodeClient.cs b/net/src/Sails.Remoting/Core/RemotingViaNodeClient.cs index 2ba1279e..478813d3 100644 --- a/net/src/Sails.Remoting/Core/RemotingViaNodeClient.cs +++ b/net/src/Sails.Remoting/Core/RemotingViaNodeClient.cs @@ -170,6 +170,14 @@ public async Task QueryAsync( return replyInfo.EncodedPayload; } + public async Task> ListenAsync(CancellationToken cancellationToken) + { + var nodeClient = await this.nodeClientProvider.GetNodeClientAsync(cancellationToken).ConfigureAwait(false); + var blocksStream = await nodeClient.GetNewBlocksStreamAsync(cancellationToken).ConfigureAwait(false); + + return new BlockStreamEventListener(nodeClient, blocksStream); + } + private static MessageQueuedEventData SelectMessageQueuedEventData(IEnumerable> runtimeEvents) => runtimeEvents .SelectIfMatches( diff --git a/net/src/Sails.Remoting/EventListenerExtensions.cs b/net/src/Sails.Remoting/EventListenerExtensions.cs new file mode 100644 index 00000000..a2579945 --- /dev/null +++ b/net/src/Sails.Remoting/EventListenerExtensions.cs @@ -0,0 +1,25 @@ +using EnsureThat; +using Sails.Remoting.Abstractions.Core; +using Substrate.Gear.Api.Generated.Model.gprimitives; +using Substrate.NetApi.Model.Types; + +namespace Sails.Remoting; + +public static class EventListenerExtensions +{ + /// + /// Projects Gear event to Typed Service Event + /// + public static EventListener<(ActorId Source, T Event)> ToServiceEventListener( + this EventListener<(ActorId Source, byte[] Payload)> source, + string serviceRoute, + string[] eventRoutes) + where T : IType, new() + { + EnsureArg.IsNotNull(source, nameof(source)); + EnsureArg.IsNotNull(serviceRoute, nameof(serviceRoute)); + EnsureArg.IsNotNull(eventRoutes, nameof(eventRoutes)); + + return new ServiceEventListener(source, serviceRoute, eventRoutes); + } +} diff --git a/net/src/Sails.Remoting/RemotingAction.cs b/net/src/Sails.Remoting/RemotingAction.cs index 20dd4a68..85d1d505 100644 --- a/net/src/Sails.Remoting/RemotingAction.cs +++ b/net/src/Sails.Remoting/RemotingAction.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using EnsureThat; @@ -8,10 +7,12 @@ using Sails.Remoting.Abstractions.Core; using Substrate.Gear.Api.Generated.Model.gprimitives; using Substrate.NetApi.Model.Types; +using Substrate.NetApi.Model.Types.Primitive; namespace Sails.Remoting; -public sealed class RemotingAction(IRemoting remoting, byte[] route, params IType[] args) : IActivation, IQuery, ICall +public sealed class RemotingAction(IRemoting remoting, string programRoute, string actionRoute, params IType[] args) + : IActivation, IQuery, ICall where T : IType, new() { private GasUnit? gasLimit; @@ -26,7 +27,7 @@ public async Task> ActivateAsync( EnsureArg.IsNotNull(codeId, nameof(codeId)); EnsureArg.IsNotNull(salt, nameof(salt)); - var encodedPayload = this.EncodePayload(); + var encodedPayload = this.EncodePayload(programRoute); var remotingReply = await remoting.ActivateAsync( codeId, @@ -38,7 +39,8 @@ public async Task> ActivateAsync( return new DelegatingReply<(ActorId ProgramId, byte[] EncodedReply), ActorId>(remotingReply, res => { - EnsureRoute(res.EncodedReply, route); + var p = 0; + EnsureRoute(res.EncodedReply, ref p, programRoute); return res.ProgramId; }); } @@ -48,7 +50,7 @@ public async Task> MessageAsync(ActorId programId, CancellationToken c { EnsureArg.IsNotNull(programId, nameof(programId)); - var encodedPayload = this.EncodePayload(); + var encodedPayload = this.EncodePayload(programRoute, actionRoute); var remotingReply = await remoting.MessageAsync( programId, @@ -65,7 +67,7 @@ public async Task QueryAsync(ActorId programId, CancellationToken cancellatio { EnsureArg.IsNotNull(programId, nameof(programId)); - var encodedPayload = this.EncodePayload(); + var encodedPayload = this.EncodePayload(programRoute, actionRoute); var replyBytes = await remoting.QueryAsync( programId, @@ -95,10 +97,13 @@ public RemotingAction WithValue(ValueUnit value) return this; } - private byte[] EncodePayload() + private byte[] EncodePayload(params string[] routes) { var byteList = new List(); - byteList.AddRange(route); + foreach (var route in routes) + { + byteList.AddRange(new Str(route).Encode()); + } foreach (var arg in args) { byteList.AddRange(arg.Encode()); @@ -108,19 +113,24 @@ private byte[] EncodePayload() private T DecodePayload(byte[] bytes) { - EnsureRoute(bytes, route); - var p = route.Length; + var p = 0; + EnsureRoute(bytes, ref p, programRoute, actionRoute); T value = new(); value.Decode(bytes, ref p); return value; } - private static void EnsureRoute(byte[] bytes, byte[] route) + private static void EnsureRoute(byte[] bytes, ref int p, params string[] routes) { - if (bytes.Length < route.Length || !route.AsSpan().SequenceEqual(bytes.AsSpan()[..route.Length])) + foreach (var route in routes) { - // TODO: custom invalid route exception - throw new ArgumentException(); + var str = new Str(); + str.Decode(bytes, ref p); + if (str != route) + { + // TODO: custom invalid route exception + throw new ArgumentException(); + } } } diff --git a/net/src/Sails.Remoting/ServiceEventListener.cs b/net/src/Sails.Remoting/ServiceEventListener.cs new file mode 100644 index 00000000..de77ce00 --- /dev/null +++ b/net/src/Sails.Remoting/ServiceEventListener.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Sails.Remoting.Abstractions.Core; +using Substrate.Gear.Api.Generated.Model.gprimitives; +using Substrate.NetApi.Model.Types; +using Substrate.NetApi.Model.Types.Primitive; + +namespace Sails.Remoting; + +internal sealed class ServiceEventListener : EventListener<(ActorId Source, T Event)> + where T : IType, new() +{ + private readonly EventListener<(ActorId Source, byte[] Payload)> source; + private readonly byte[] serviceRoute; + private readonly byte[][] eventRoutes; + + internal ServiceEventListener( + EventListener<(ActorId Source, byte[] Payload)> source, + string serviceRoute, + string[] eventRoutes) + { + this.source = source; + this.serviceRoute = new Str(serviceRoute).Encode(); + this.eventRoutes = eventRoutes.Select(r => new Str(r).Encode()).ToArray(); + } + + public override IAsyncEnumerable<(ActorId Source, T Event)> ReadAllAsync(CancellationToken cancellationToken) + => this.source + .ReadAllAsync(cancellationToken) + .Select(this.Decode) + .Where(x => x != null) + .Select(x => x!.Value); + + protected override ValueTask DisposeCoreAsync() => this.source.DisposeAsync(); + + private (ActorId Source, T Event)? Decode((ActorId, byte[]) tuple) + { + var (source, bytes) = tuple; + var serviceLength = this.serviceRoute.Length; + if (bytes.Length < serviceLength || !this.serviceRoute.AsSpan().SequenceEqual(bytes.AsSpan(0, serviceLength))) + { + return null; + } + var offset = serviceLength; + byte idx = 0; + foreach (var route in this.eventRoutes) + { + if (bytes.Length < route.Length + offset) + { + continue; + } + if (route.AsSpan().SequenceEqual(bytes.AsSpan(offset, route.Length))) + { + offset += route.Length; + var bytesLength = bytes.Length - offset + 1; + var data = new byte[bytesLength]; + data[0] = idx; + Buffer.BlockCopy(bytes, offset, data, 1, bytesLength - 1); + + var p = 0; + T ev = new(); + ev.Decode(data, ref p); + return (source, ev); + } + idx++; + } + return null; + } +} diff --git a/net/src/Substrate.Gear.Client/BlocksStreamExtensions.cs b/net/src/Substrate.Gear.Client/BlocksStreamExtensions.cs new file mode 100644 index 00000000..11b6ffb4 --- /dev/null +++ b/net/src/Substrate.Gear.Client/BlocksStreamExtensions.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Substrate.Gear.Api.Generated; +using Substrate.Gear.Api.Generated.Model.gear_core.message.user; +using Substrate.Gear.Api.Generated.Model.gprimitives; +using Substrate.Gear.Api.Generated.Model.vara_runtime; +using Substrate.Gear.Client.NetApi.Model.Rpc; +using Substrate.Gear.Client.NetApi.Model.Types.Base; +using Substrate.NetApi.Model.Rpc; +using Substrate.NetApi.Model.Types.Base; + +namespace Substrate.Gear.Client; + +public static class BlocksStreamExtensions +{ + [SuppressMessage( + "Style", + "VSTHRD200:Use \"Async\" suffix for async methods", + Justification = "To be consistent with system provided extensions")] + public static IAsyncEnumerable> SelectGearEvents( + this IAsyncEnumerable
headers, + SubstrateClientExt nodeClient, + CancellationToken cancellationToken = default) + => headers + .SelectAwait(async blockHeader => await nodeClient + .ListBlockEventsAsync(blockHeader.GetBlockHash(), cancellationToken).ConfigureAwait(false)) + .SelectMany(eventRecords => eventRecords.ToAsyncEnumerable()) + .Select(eventRecord => eventRecord.Event.ToBaseEnumRust()) + .SelectIfMatches( + RuntimeEvent.Gear, + (EnumGearEvent gearEvent) => gearEvent.ToBaseEnumRust() + ); + + [SuppressMessage( + "Style", + "VSTHRD200:Use \"Async\" suffix for async methods", + Justification = "To be consistent with system provided extensions")] + public static IAsyncEnumerable<(ActorId Source, byte[] Payload)> SelectServiceEvents( + this IAsyncEnumerable> gearEvents) + => gearEvents + .SelectIfMatches( + GearEvent.UserMessageSent, + (UserMessageSentEventData data) => (UserMessage)data.Value[0]) + .Where(userMessage => userMessage.Destination + .IsEqualTo(GearApi.Model.gprimitives.ActorIdExtensions.Zero)) + .Select(userMessage => (userMessage.Source, userMessage.Payload.Value.Value.Select(@byte => @byte.Value).ToArray())); +} diff --git a/net/src/Substrate.Gear.Client/GearApi/Model/gprimitives/ActorIdExtensions.cs b/net/src/Substrate.Gear.Client/GearApi/Model/gprimitives/ActorIdExtensions.cs index a4ca07fa..0d086375 100644 --- a/net/src/Substrate.Gear.Client/GearApi/Model/gprimitives/ActorIdExtensions.cs +++ b/net/src/Substrate.Gear.Client/GearApi/Model/gprimitives/ActorIdExtensions.cs @@ -1,12 +1,21 @@ using System.Linq; using EnsureThat; +using Substrate.Gear.Api.Generated.Model.gprimitives; using Substrate.NetApi; namespace Substrate.Gear.Client.GearApi.Model.gprimitives; public static class ActorIdExtensions { - public static string ToHexString(this Api.Generated.Model.gprimitives.ActorId actorId) + public static ActorId Zero { get; } = new(); + + static ActorIdExtensions() + { + var p = 0; + Zero.Decode(new byte[32], ref p); + } + + public static string ToHexString(this ActorId actorId) { EnsureArg.IsNotNull(actorId, nameof(actorId)); EnsureArg.IsNotNull(actorId.Value, "actorId.Value"); diff --git a/net/src/Substrate.Gear.Client/GlobalUsings.cs b/net/src/Substrate.Gear.Client/GlobalUsings.cs index be6ab16c..a29aa50d 100644 --- a/net/src/Substrate.Gear.Client/GlobalUsings.cs +++ b/net/src/Substrate.Gear.Client/GlobalUsings.cs @@ -10,4 +10,7 @@ global using GasUnit = Substrate.NetApi.Model.Types.Primitive.U64; global using GearEvent = Substrate.Gear.Api.Generated.Model.pallet_gear.pallet.Event; global using SystemEvent = Substrate.Gear.Api.Generated.Model.frame_system.pallet.Event; +global using UserMessageSentEventData = Substrate.NetApi.Model.Types.Base.BaseTuple< + Substrate.Gear.Api.Generated.Model.gear_core.message.user.UserMessage, + Substrate.NetApi.Model.Types.Base.BaseOpt>; global using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128; diff --git a/net/tests/Sails.ClientGenerator.Tests/Snapshots/SailsClientGeneratorTests.Generate_DemoIdl#Demo.g.verified.cs b/net/tests/Sails.ClientGenerator.Tests/Snapshots/SailsClientGeneratorTests.Generate_DemoIdl#Demo.g.verified.cs index 8cfba485..1a0fa242 100644 --- a/net/tests/Sails.ClientGenerator.Tests/Snapshots/SailsClientGeneratorTests.Generate_DemoIdl#Demo.g.verified.cs +++ b/net/tests/Sails.ClientGenerator.Tests/Snapshots/SailsClientGeneratorTests.Generate_DemoIdl#Demo.g.verified.cs @@ -5,6 +5,8 @@ using global::Sails.Remoting.Abstractions.Core; using global::System; using global::System.Collections.Generic; +using global::System.Threading; +using global::System.Threading.Tasks; #nullable enable #pragma warning disable RCS0056 // A line is too long @@ -33,13 +35,13 @@ public DemoFactory(IRemoting remoting) /// public IActivation Default() { - return new RemotingAction(this.remoting, [28, 68, 101, 102, 97, 117, 108, 116]); + return new RemotingAction(this.remoting, nameof(Default), string.Empty); } /// public IActivation New(global::Substrate.NetApi.Model.Types.Base.BaseOpt counter, global::Substrate.NetApi.Model.Types.Base.BaseOpt> dogPosition) { - return new RemotingAction(this.remoting, [12, 78, 101, 119], counter, dogPosition); + return new RemotingAction(this.remoting, nameof(New), string.Empty, counter, dogPosition); } } @@ -52,6 +54,7 @@ public interface ICounter public sealed partial class Counter : ICounter { + private const string ROUTE = nameof(Counter); private readonly IRemoting remoting; public Counter(IRemoting remoting) { @@ -61,19 +64,19 @@ public Counter(IRemoting remoting) /// public ICall Add(global::Substrate.NetApi.Model.Types.Primitive.U32 value) { - return new RemotingAction(this.remoting, [28, 67, 111, 117, 110, 116, 101, 114, 12, 65, 100, 100], value); + return new RemotingAction(this.remoting, ROUTE, nameof(Add), value); } /// public ICall Sub(global::Substrate.NetApi.Model.Types.Primitive.U32 value) { - return new RemotingAction(this.remoting, [28, 67, 111, 117, 110, 116, 101, 114, 12, 83, 117, 98], value); + return new RemotingAction(this.remoting, ROUTE, nameof(Sub), value); } /// public IQuery Value() { - return new RemotingAction(this.remoting, [28, 67, 111, 117, 110, 116, 101, 114, 20, 86, 97, 108, 117, 101]); + return new RemotingAction(this.remoting, ROUTE, nameof(Value)); } } @@ -98,42 +101,20 @@ public EnumCounterEvents() } } -public sealed partial class CounterListener : IRemotingListener +public sealed partial class CounterListener { - private static readonly byte[][] EventRoutes = [[28, 67, 111, 117, 110, 116, 101, 114, 20, 65, 100, 100, 101, 100], [28, 67, 111, 117, 110, 116, 101, 114, 40, 83, 117, 98, 116, 114, 97, 99, 116, 101, 100], ]; - private readonly global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting; - public CounterListener(global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting) + private const string ROUTE = "Counter"; + private static readonly string[] EventRoutes = ["Added", "Subtracted", ]; + private readonly IRemoting remoting; + public CounterListener(IRemoting remoting) { this.remoting = remoting; } - public async global::System.Collections.Generic.IAsyncEnumerable ListenAsync([global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) - { - await foreach (var bytes in this.remoting.ListenAsync(cancellationToken)) - { - byte idx = 0; - foreach (var route in EventRoutes) - { - if (route.Length > bytes.Length) - { - continue; - } - - if (route.AsSpan().SequenceEqual(bytes.AsSpan()[..route.Length])) - { - var bytesLength = bytes.Length - route.Length + 1; - var data = new byte[bytesLength]; - data[0] = idx; - Buffer.BlockCopy(bytes, route.Length, data, 1, bytes.Length - route.Length); - var p = 0; - EnumCounterEvents ev = new(); - ev.Decode(bytes, ref p); - yield return ev; - } - - idx++; - } - } + public async Task> ListenAsync(CancellationToken cancellationToken = default) + { + var listener = await this.remoting.ListenAsync(cancellationToken); + return listener.ToServiceEventListener(ROUTE, EventRoutes); } } @@ -147,6 +128,7 @@ public interface IDog public sealed partial class Dog : IDog { + private const string ROUTE = nameof(Dog); private readonly IRemoting remoting; public Dog(IRemoting remoting) { @@ -156,25 +138,25 @@ public Dog(IRemoting remoting) /// public ICall MakeSound() { - return new RemotingAction(this.remoting, [12, 68, 111, 103, 36, 77, 97, 107, 101, 83, 111, 117, 110, 100]); + return new RemotingAction(this.remoting, ROUTE, nameof(MakeSound)); } /// public ICall Walk(global::Substrate.NetApi.Model.Types.Primitive.I32 dx, global::Substrate.NetApi.Model.Types.Primitive.I32 dy) { - return new RemotingAction(this.remoting, [12, 68, 111, 103, 16, 87, 97, 108, 107], dx, dy); + return new RemotingAction(this.remoting, ROUTE, nameof(Walk), dx, dy); } /// public IQuery AvgWeight() { - return new RemotingAction(this.remoting, [12, 68, 111, 103, 36, 65, 118, 103, 87, 101, 105, 103, 104, 116]); + return new RemotingAction(this.remoting, ROUTE, nameof(AvgWeight)); } /// public IQuery> Position() { - return new RemotingAction>(this.remoting, [12, 68, 111, 103, 32, 80, 111, 115, 105, 116, 105, 111, 110]); + return new RemotingAction>(this.remoting, ROUTE, nameof(Position)); } } @@ -193,42 +175,20 @@ public EnumDogEvents() } } -public sealed partial class DogListener : IRemotingListener +public sealed partial class DogListener { - private static readonly byte[][] EventRoutes = [[12, 68, 111, 103, 24, 66, 97, 114, 107, 101, 100], [12, 68, 111, 103, 24, 87, 97, 108, 107, 101, 100], ]; - private readonly global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting; - public DogListener(global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting) + private const string ROUTE = "Dog"; + private static readonly string[] EventRoutes = ["Barked", "Walked", ]; + private readonly IRemoting remoting; + public DogListener(IRemoting remoting) { this.remoting = remoting; } - public async global::System.Collections.Generic.IAsyncEnumerable ListenAsync([global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) - { - await foreach (var bytes in this.remoting.ListenAsync(cancellationToken)) - { - byte idx = 0; - foreach (var route in EventRoutes) - { - if (route.Length > bytes.Length) - { - continue; - } - - if (route.AsSpan().SequenceEqual(bytes.AsSpan()[..route.Length])) - { - var bytesLength = bytes.Length - route.Length + 1; - var data = new byte[bytesLength]; - data[0] = idx; - Buffer.BlockCopy(bytes, route.Length, data, 1, bytes.Length - route.Length); - var p = 0; - EnumDogEvents ev = new(); - ev.Decode(bytes, ref p); - yield return ev; - } - - idx++; - } - } + public async Task> ListenAsync(CancellationToken cancellationToken = default) + { + var listener = await this.remoting.ListenAsync(cancellationToken); + return listener.ToServiceEventListener(ROUTE, EventRoutes); } } @@ -239,6 +199,7 @@ public interface IPingPong public sealed partial class PingPong : IPingPong { + private const string ROUTE = nameof(PingPong); private readonly IRemoting remoting; public PingPong(IRemoting remoting) { @@ -248,7 +209,7 @@ public PingPong(IRemoting remoting) /// public ICall> Ping(global::Substrate.NetApi.Model.Types.Primitive.Str input) { - return new RemotingAction>(this.remoting, [32, 80, 105, 110, 103, 80, 111, 110, 103, 16, 80, 105, 110, 103], input); + return new RemotingAction>(this.remoting, ROUTE, nameof(Ping), input); } } @@ -266,6 +227,7 @@ public interface IReferences public sealed partial class References : IReferences { + private const string ROUTE = nameof(References); private readonly IRemoting remoting; public References(IRemoting remoting) { @@ -275,49 +237,49 @@ public References(IRemoting remoting) /// public ICall Add(global::Substrate.NetApi.Model.Types.Primitive.U32 v) { - return new RemotingAction(this.remoting, [40, 82, 101, 102, 101, 114, 101, 110, 99, 101, 115, 12, 65, 100, 100], v); + return new RemotingAction(this.remoting, ROUTE, nameof(Add), v); } /// public ICall> AddByte(global::Substrate.NetApi.Model.Types.Primitive.U8 @byte) { - return new RemotingAction>(this.remoting, [40, 82, 101, 102, 101, 114, 101, 110, 99, 101, 115, 28, 65, 100, 100, 66, 121, 116, 101], @byte); + return new RemotingAction>(this.remoting, ROUTE, nameof(AddByte), @byte); } /// public ICall> GuessNum(global::Substrate.NetApi.Model.Types.Primitive.U8 number) { - return new RemotingAction>(this.remoting, [40, 82, 101, 102, 101, 114, 101, 110, 99, 101, 115, 32, 71, 117, 101, 115, 115, 78, 117, 109], number); + return new RemotingAction>(this.remoting, ROUTE, nameof(GuessNum), number); } /// public ICall Incr() { - return new RemotingAction(this.remoting, [40, 82, 101, 102, 101, 114, 101, 110, 99, 101, 115, 16, 73, 110, 99, 114]); + return new RemotingAction(this.remoting, ROUTE, nameof(Incr)); } /// public ICall> SetNum(global::Substrate.NetApi.Model.Types.Primitive.U8 number) { - return new RemotingAction>(this.remoting, [40, 82, 101, 102, 101, 114, 101, 110, 99, 101, 115, 24, 83, 101, 116, 78, 117, 109], number); + return new RemotingAction>(this.remoting, ROUTE, nameof(SetNum), number); } /// public IQuery Baked() { - return new RemotingAction(this.remoting, [40, 82, 101, 102, 101, 114, 101, 110, 99, 101, 115, 20, 66, 97, 107, 101, 100]); + return new RemotingAction(this.remoting, ROUTE, nameof(Baked)); } /// public IQuery> LastByte() { - return new RemotingAction>(this.remoting, [40, 82, 101, 102, 101, 114, 101, 110, 99, 101, 115, 32, 76, 97, 115, 116, 66, 121, 116, 101]); + return new RemotingAction>(this.remoting, ROUTE, nameof(LastByte)); } /// public IQuery> Message() { - return new RemotingAction>(this.remoting, [40, 82, 101, 102, 101, 114, 101, 110, 99, 101, 115, 28, 77, 101, 115, 115, 97, 103, 101]); + return new RemotingAction>(this.remoting, ROUTE, nameof(Message)); } } @@ -332,6 +294,7 @@ public interface IThisThat public sealed partial class ThisThat : IThisThat { + private const string ROUTE = nameof(ThisThat); private readonly IRemoting remoting; public ThisThat(IRemoting remoting) { @@ -341,31 +304,31 @@ public ThisThat(IRemoting remoting) /// public ICall, global::Substrate.NetApi.Model.Types.Primitive.Str>> DoThat(DoThatParam param) { - return new RemotingAction, global::Substrate.NetApi.Model.Types.Primitive.Str>>(this.remoting, [32, 84, 104, 105, 115, 84, 104, 97, 116, 24, 68, 111, 84, 104, 97, 116], param); + return new RemotingAction, global::Substrate.NetApi.Model.Types.Primitive.Str>>(this.remoting, ROUTE, nameof(DoThat), param); } /// public ICall> DoThis(global::Substrate.NetApi.Model.Types.Primitive.U32 p1, global::Substrate.NetApi.Model.Types.Primitive.Str p2, global::Substrate.NetApi.Model.Types.Base.BaseTuple, global::Substrate.Gear.Client.NetApi.Model.Types.Primitive.NonZeroU8> p3, TupleStruct p4) { - return new RemotingAction>(this.remoting, [32, 84, 104, 105, 115, 84, 104, 97, 116, 24, 68, 111, 84, 104, 105, 115], p1, p2, p3, p4); + return new RemotingAction>(this.remoting, ROUTE, nameof(DoThis), p1, p2, p3, p4); } /// public ICall Noop() { - return new RemotingAction(this.remoting, [32, 84, 104, 105, 115, 84, 104, 97, 116, 16, 78, 111, 111, 112]); + return new RemotingAction(this.remoting, ROUTE, nameof(Noop)); } /// public IQuery> That() { - return new RemotingAction>(this.remoting, [32, 84, 104, 105, 115, 84, 104, 97, 116, 16, 84, 104, 97, 116]); + return new RemotingAction>(this.remoting, ROUTE, nameof(That)); } /// public IQuery This() { - return new RemotingAction(this.remoting, [32, 84, 104, 105, 115, 84, 104, 97, 116, 16, 84, 104, 105, 115]); + return new RemotingAction(this.remoting, ROUTE, nameof(This)); } } @@ -376,6 +339,7 @@ public interface IValueFee public sealed partial class ValueFee : IValueFee { + private const string ROUTE = nameof(ValueFee); private readonly IRemoting remoting; public ValueFee(IRemoting remoting) { @@ -385,7 +349,7 @@ public ValueFee(IRemoting remoting) /// public ICall DoSomethingAndTakeFee() { - return new RemotingAction(this.remoting, [32, 86, 97, 108, 117, 101, 70, 101, 101, 84, 68, 111, 83, 111, 109, 101, 116, 104, 105, 110, 103, 65, 110, 100, 84, 97, 107, 101, 70, 101, 101]); + return new RemotingAction(this.remoting, ROUTE, nameof(DoSomethingAndTakeFee)); } } @@ -402,42 +366,20 @@ public EnumValueFeeEvents() } } -public sealed partial class ValueFeeListener : IRemotingListener +public sealed partial class ValueFeeListener { - private static readonly byte[][] EventRoutes = [[32, 86, 97, 108, 117, 101, 70, 101, 101, 32, 87, 105, 116, 104, 104, 101, 108, 100], ]; - private readonly global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting; - public ValueFeeListener(global::Sails.Remoting.Abstractions.Core.IRemotingListener remoting) + private const string ROUTE = "ValueFee"; + private static readonly string[] EventRoutes = ["Withheld", ]; + private readonly IRemoting remoting; + public ValueFeeListener(IRemoting remoting) { this.remoting = remoting; } - public async global::System.Collections.Generic.IAsyncEnumerable ListenAsync([global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) - { - await foreach (var bytes in this.remoting.ListenAsync(cancellationToken)) - { - byte idx = 0; - foreach (var route in EventRoutes) - { - if (route.Length > bytes.Length) - { - continue; - } - - if (route.AsSpan().SequenceEqual(bytes.AsSpan()[..route.Length])) - { - var bytesLength = bytes.Length - route.Length + 1; - var data = new byte[bytesLength]; - data[0] = idx; - Buffer.BlockCopy(bytes, route.Length, data, 1, bytes.Length - route.Length); - var p = 0; - EnumValueFeeEvents ev = new(); - ev.Decode(bytes, ref p); - yield return ev; - } - - idx++; - } - } + public async Task> ListenAsync(CancellationToken cancellationToken = default) + { + var listener = await this.remoting.ListenAsync(cancellationToken); + return listener.ToServiceEventListener(ROUTE, EventRoutes); } } diff --git a/net/tests/Sails.Remoting.Tests/Core/RemotingViaNodeClientTests.cs b/net/tests/Sails.Remoting.Tests/Core/RemotingViaNodeClientTests.cs index 1024a7c7..0d1936f5 100644 --- a/net/tests/Sails.Remoting.Tests/Core/RemotingViaNodeClientTests.cs +++ b/net/tests/Sails.Remoting.Tests/Core/RemotingViaNodeClientTests.cs @@ -1,4 +1,5 @@ -using Sails.Remoting.Tests._Infra.XUnit.Fixtures; +using System.Collections.Generic; +using Sails.Remoting.Tests._Infra.XUnit.Fixtures; using Substrate.Gear.Client.GearApi.Model.gprimitives; using Substrate.NetApi.Model.Types.Primitive; @@ -116,4 +117,41 @@ public async Task Querying_Program_State_Works() [.. encodedPayload, .. new U32(42).Encode()], options => options.WithStrictOrdering()); } + + [Fact] + public async Task EventListener_Works() + { + // Arrange + var codeId = await this.sailsFixture.GetDemoContractCodeIdAsync(); + var activationReply = await this.remoting.ActivateAsync( + codeId, + salt: BitConverter.GetBytes(Random.NextInt64()), + new Str("Default").Encode(), + CancellationToken.None); + var (programId, _) = await activationReply.ReadAsync(CancellationToken.None); + + var encodedPayload = new Str("Counter").Encode() + .Concat(new Str("Add").Encode()) + .Concat(new U32(42).Encode()) + .ToArray(); + + var expectedEventPayload = new List(); + expectedEventPayload.AddRange(new Str("Counter").Encode()); + expectedEventPayload.AddRange(new Str("Added").Encode()); + expectedEventPayload.AddRange(new U32(42).Encode()); + + await using var listener = await this.remoting.ListenAsync(CancellationToken.None); + + // Act + var messageReply = await this.remoting.MessageAsync( + programId, + encodedPayload, + CancellationToken.None); + + var (source, payload) = await listener.ReadAllAsync(CancellationToken.None).FirstAsync(CancellationToken.None); + + // Assert + source.Should().BeEquivalentTo(programId); + payload.Should().BeEquivalentTo(expectedEventPayload, options => options.WithStrictOrdering()); + } }