From ca42629540b9855a8201da49dc861a74691143bb Mon Sep 17 00:00:00 2001 From: Larko <59736843+Larkooo@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:54:11 -0400 Subject: [PATCH] feat(torii-grpc): support multiple entity models in queries & complex keys clause for subscriptions + queries (#2095) * feat: start working on keys entity subscription * refactor: implementing keys subscriptions and using existing queries * refactor: finish refactor on clauses * fix: if felt bytes 0 then wildcard * fmt * feat: update torii client and fix tets * fix: clippy * refactor: do not consider overflowing key patterns * feat: fully use new keysclause & add pattern matching * feat: support pattern matching in queries * fmt * refactor: new types in torii client * clippy * fix: ignroe non matching entity keys in fixedlen * add a model to test keys clause pattern matching * fix: fix tests * fix: lock version * chore: remove debug print * fix: match fixed len for queries * chore: use regex for key pattern matching * feat: add optional model clause to keysclause * feat: add model specf to query by keys * fix: queries * feat: wrap up queries * fmt & clippy * chore: enable regexp for tests * chore: update test for new model * refactor: use array of models for keysclause --------- Co-authored-by: glihm --- crates/dojo-bindgen/src/lib.rs | 2 +- .../dojo-world/src/manifest/manifest_test.rs | 4 +- crates/torii/client/src/client/mod.rs | 45 +- .../torii/client/src/client/subscription.rs | 18 +- crates/torii/core/src/sql.rs | 4 +- crates/torii/core/src/sql_test.rs | 2 +- crates/torii/core/src/types.rs | 4 + crates/torii/grpc/proto/types.proto | 28 +- crates/torii/grpc/proto/world.proto | 8 +- crates/torii/grpc/src/client.rs | 23 +- crates/torii/grpc/src/server/mod.rs | 357 +++++++-------- .../grpc/src/server/subscriptions/entity.rs | 164 ++++--- .../grpc/src/server/subscriptions/event.rs | 65 ++- .../src/server/subscriptions/event_message.rs | 157 ++++--- .../src/server/subscriptions/model_diff.rs | 6 +- .../grpc/src/server/tests/entities_test.rs | 18 +- crates/torii/grpc/src/types/mod.rs | 110 ++++- crates/torii/types-test/Scarb.lock | 2 +- .../dojo_examples_actions_actions.json | 16 + .../dojo_examples_models_server_profile.json | 367 ++++++++++++++++ .../dojo_examples_actions_actions.json | 16 + .../dojo_examples_models_server_profile.json | 367 ++++++++++++++++ .../dojo_examples_actions_actions.toml | 4 +- .../dojo_examples_models_server_profile.toml | 20 + .../manifests/dev/manifest.json | 410 +++++++++++++++++- .../manifests/dev/manifest.toml | 26 +- examples/spawn-and-move/src/actions.cairo | 10 +- examples/spawn-and-move/src/models.cairo | 10 + 28 files changed, 1865 insertions(+), 398 deletions(-) create mode 100644 examples/spawn-and-move/manifests/dev/abis/base/models/dojo_examples_models_server_profile.json create mode 100644 examples/spawn-and-move/manifests/dev/abis/deployments/models/dojo_examples_models_server_profile.json create mode 100644 examples/spawn-and-move/manifests/dev/base/models/dojo_examples_models_server_profile.toml diff --git a/crates/dojo-bindgen/src/lib.rs b/crates/dojo-bindgen/src/lib.rs index c8cbe96172..97e8074868 100644 --- a/crates/dojo-bindgen/src/lib.rs +++ b/crates/dojo-bindgen/src/lib.rs @@ -283,7 +283,7 @@ mod tests { gather_dojo_data(&manifest_path, "dojo_example", "dev", dojo_metadata.skip_migration) .unwrap(); - assert_eq!(data.models.len(), 7); + assert_eq!(data.models.len(), 8); assert_eq!(data.world.name, "dojo_example"); diff --git a/crates/dojo-world/src/manifest/manifest_test.rs b/crates/dojo-world/src/manifest/manifest_test.rs index d19a69b01b..b4894c453c 100644 --- a/crates/dojo-world/src/manifest/manifest_test.rs +++ b/crates/dojo-world/src/manifest/manifest_test.rs @@ -419,10 +419,10 @@ fn fetch_remote_manifest() { DeploymentManifest::load_from_remote(provider, world_address).await.unwrap() }); - assert_eq!(local_manifest.models.len(), 7); + assert_eq!(local_manifest.models.len(), 8); assert_eq!(local_manifest.contracts.len(), 3); - assert_eq!(remote_manifest.models.len(), 7); + assert_eq!(remote_manifest.models.len(), 8); assert_eq!(remote_manifest.contracts.len(), 3); // compute diff from local and remote manifest diff --git a/crates/torii/client/src/client/mod.rs b/crates/torii/client/src/client/mod.rs index dead3ab1b0..0f7a096317 100644 --- a/crates/torii/client/src/client/mod.rs +++ b/crates/torii/client/src/client/mod.rs @@ -20,7 +20,7 @@ use tokio::sync::RwLock as AsyncRwLock; use torii_grpc::client::{EntityUpdateStreaming, EventUpdateStreaming, ModelDiffsStreaming}; use torii_grpc::proto::world::{RetrieveEntitiesResponse, RetrieveEventsResponse}; use torii_grpc::types::schema::{Entity, SchemaError}; -use torii_grpc::types::{Event, EventQuery, KeysClause, Query}; +use torii_grpc::types::{EntityKeysClause, Event, EventQuery, KeysClause, ModelKeysClause, Query}; use torii_relay::client::EventLoop; use torii_relay::types::Message; @@ -56,7 +56,6 @@ impl Client { rpc_url: String, relay_url: String, world: FieldElement, - models_keys: Option>, ) -> Result { let mut grpc_client = torii_grpc::client::WorldClient::new(torii_url, world).await?; @@ -73,23 +72,6 @@ impl Client { let provider = JsonRpcClient::new(HttpTransport::new(rpc_url)); let world_reader = WorldContractReader::new(world, provider); - if let Some(keys) = models_keys { - subbed_models.add_models(keys)?; - - // TODO: change this to querying the gRPC url instead - let subbed_models = subbed_models.models_keys.read().clone(); - for keys in subbed_models { - let model_reader = world_reader.model_reader(&keys.model).await?; - let values = model_reader.entity_storage(&keys.keys).await?; - - client_storage.set_model_storage( - cairo_short_string_to_felt(&keys.model).unwrap(), - keys.keys, - values, - )?; - } - } - Ok(Self { world_reader, storage: client_storage, @@ -123,7 +105,7 @@ impl Client { self.metadata.read() } - pub fn subscribed_models(&self) -> RwLockReadGuard<'_, HashSet> { + pub fn subscribed_models(&self) -> RwLockReadGuard<'_, HashSet> { self.subscribed_models.models_keys.read() } @@ -163,26 +145,26 @@ impl Client { /// A direct stream to grpc subscribe entities pub async fn on_entity_updated( &self, - ids: Vec, + clause: Option, ) -> Result { let mut grpc_client = self.inner.write().await; - let stream = grpc_client.subscribe_entities(ids).await?; + let stream = grpc_client.subscribe_entities(clause).await?; Ok(stream) } /// A direct stream to grpc subscribe event messages pub async fn on_event_message_updated( &self, - ids: Vec, + clause: Option, ) -> Result { let mut grpc_client = self.inner.write().await; - let stream = grpc_client.subscribe_event_messages(ids).await?; + let stream = grpc_client.subscribe_event_messages(clause).await?; Ok(stream) } pub async fn on_starknet_event( &self, - keys: Option>, + keys: Option, ) -> Result { let mut grpc_client = self.inner.write().await; let stream = grpc_client.subscribe_events(keys).await?; @@ -196,7 +178,7 @@ impl Client { /// /// If the requested model is not among the synced models, it will attempt to fetch it from /// the RPC. - pub async fn model(&self, keys: &KeysClause) -> Result, Error> { + pub async fn model(&self, keys: &ModelKeysClause) -> Result, Error> { let Some(mut schema) = self.metadata.read().model(&keys.model).map(|m| m.schema.clone()) else { return Ok(None); @@ -232,7 +214,7 @@ impl Client { /// Initiate the model subscriptions and returns a [SubscriptionService] which when await'ed /// will execute the subscription service and starts the syncing process. pub async fn start_subscription(&self) -> Result { - let models_keys: Vec = + let models_keys: Vec = self.subscribed_models.models_keys.read().clone().into_iter().collect(); let sub_res_stream = self.initiate_subscription(models_keys).await?; @@ -250,7 +232,7 @@ impl Client { /// Adds entities to the list of entities to be synced. /// /// NOTE: This will establish a new subscription stream with the server. - pub async fn add_models_to_sync(&self, models_keys: Vec) -> Result<(), Error> { + pub async fn add_models_to_sync(&self, models_keys: Vec) -> Result<(), Error> { for keys in &models_keys { self.initiate_model(&keys.model, keys.keys.clone()).await?; } @@ -271,7 +253,10 @@ impl Client { /// Removes models from the list of models to be synced. /// /// NOTE: This will establish a new subscription stream with the server. - pub async fn remove_models_to_sync(&self, models_keys: Vec) -> Result<(), Error> { + pub async fn remove_models_to_sync( + &self, + models_keys: Vec, + ) -> Result<(), Error> { self.subscribed_models.remove_models(models_keys)?; let updated_entities = @@ -291,7 +276,7 @@ impl Client { async fn initiate_subscription( &self, - keys: Vec, + keys: Vec, ) -> Result { let mut grpc_client = self.inner.write().await; let stream = grpc_client.subscribe_model_diffs(keys).await?; diff --git a/crates/torii/client/src/client/subscription.rs b/crates/torii/client/src/client/subscription.rs index ac21707104..73415975e3 100644 --- a/crates/torii/client/src/client/subscription.rs +++ b/crates/torii/client/src/client/subscription.rs @@ -12,7 +12,7 @@ use starknet::core::types::{StateDiff, StateUpdate}; use starknet::core::utils::cairo_short_string_to_felt; use starknet_crypto::FieldElement; use torii_grpc::client::ModelDiffsStreaming; -use torii_grpc::types::KeysClause; +use torii_grpc::types::ModelKeysClause; use crate::client::error::{Error, ParseError}; use crate::client::storage::ModelStorage; @@ -24,13 +24,13 @@ pub enum SubscriptionEvent { pub struct SubscribedModels { metadata: Arc>, - pub(crate) models_keys: RwLock>, + pub(crate) models_keys: RwLock>, /// All the relevant storage addresses derived from the subscribed models pub(crate) subscribed_storage_addresses: RwLock>, } impl SubscribedModels { - pub(crate) fn is_synced(&self, keys: &KeysClause) -> bool { + pub(crate) fn is_synced(&self, keys: &ModelKeysClause) -> bool { self.models_keys.read().contains(keys) } @@ -42,21 +42,21 @@ impl SubscribedModels { } } - pub(crate) fn add_models(&self, models_keys: Vec) -> Result<(), Error> { + pub(crate) fn add_models(&self, models_keys: Vec) -> Result<(), Error> { for keys in models_keys { Self::add_model(self, keys)?; } Ok(()) } - pub(crate) fn remove_models(&self, entities_keys: Vec) -> Result<(), Error> { + pub(crate) fn remove_models(&self, entities_keys: Vec) -> Result<(), Error> { for keys in entities_keys { Self::remove_model(self, keys)?; } Ok(()) } - pub(crate) fn add_model(&self, keys: KeysClause) -> Result<(), Error> { + pub(crate) fn add_model(&self, keys: ModelKeysClause) -> Result<(), Error> { if !self.models_keys.write().insert(keys.clone()) { return Ok(()); } @@ -83,7 +83,7 @@ impl SubscribedModels { Ok(()) } - pub(crate) fn remove_model(&self, keys: KeysClause) -> Result<(), Error> { + pub(crate) fn remove_model(&self, keys: ModelKeysClause) -> Result<(), Error> { if !self.models_keys.write().remove(&keys) { return Ok(()); } @@ -247,7 +247,7 @@ mod tests { use parking_lot::RwLock; use starknet::core::utils::cairo_short_string_to_felt; use starknet::macros::felt; - use torii_grpc::types::KeysClause; + use torii_grpc::types::ModelKeysClause; use crate::utils::compute_all_storage_addresses; @@ -283,7 +283,7 @@ mod tests { let metadata = self::create_dummy_metadata(); - let keys = KeysClause { model: model_name, keys }; + let keys = ModelKeysClause { model: model_name, keys }; let subscribed_models = super::SubscribedModels::new(Arc::new(RwLock::new(metadata))); subscribed_models.add_models(vec![keys.clone()]).expect("able to add model"); diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 8edf873afa..d7a692c728 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -228,7 +228,7 @@ impl Sql { VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET \ updated_at=CURRENT_TIMESTAMP, event_id=EXCLUDED.event_id RETURNING \ *"; - let event_message_updated: EventMessageUpdated = sqlx::query_as(insert_entities) + let mut event_message_updated: EventMessageUpdated = sqlx::query_as(insert_entities) .bind(&entity_id) .bind(&keys_str) .bind(event_id) @@ -236,6 +236,8 @@ impl Sql { .fetch_one(&self.pool) .await?; + event_message_updated.updated_model = Some(entity.clone()); + let path = vec![entity.name()]; self.build_set_entity_queries_recursive( path, diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index 92d45a68e8..baf7b65e6f 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -123,7 +123,7 @@ async fn test_load_from_remote() { let _block_timestamp = 1710754478_u64; let models = sqlx::query("SELECT * FROM models").fetch_all(&pool).await.unwrap(); - assert_eq!(models.len(), 7); + assert_eq!(models.len(), 8); let (id, name, packed_size, unpacked_size): (String, String, u8, u8) = sqlx::query_as( "SELECT id, name, packed_size, unpacked_size FROM models WHERE name = 'Position'", diff --git a/crates/torii/core/src/types.rs b/crates/torii/core/src/types.rs index bef551258c..f410c594c4 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/core/src/types.rs @@ -52,6 +52,10 @@ pub struct EventMessage { pub executed_at: DateTime, pub created_at: DateTime, pub updated_at: DateTime, + + // this should never be None. as a EventMessage cannot be deleted + #[sqlx(skip)] + pub updated_model: Option, } #[derive(FromRow, Deserialize, Debug, Clone)] diff --git a/crates/torii/grpc/proto/types.proto b/crates/torii/grpc/proto/types.proto index 7ca3b3cf17..9575820858 100644 --- a/crates/torii/grpc/proto/types.proto +++ b/crates/torii/grpc/proto/types.proto @@ -85,7 +85,7 @@ message Query { } message EventQuery { - EventKeysClause keys =1; + KeysClause keys = 1; uint32 limit = 2; uint32 offset = 3; } @@ -99,17 +99,26 @@ message Clause { } } -message HashedKeysClause { - repeated bytes hashed_keys = 1; +message ModelKeysClause { + string model = 1; + repeated bytes keys = 2; } -message EventKeysClause { - repeated bytes keys = 1; +message EntityKeysClause { + oneof clause_type { + HashedKeysClause hashed_keys = 1; + KeysClause keys = 2; + } } message KeysClause { - string model = 1; - repeated bytes keys = 2; + repeated bytes keys = 1; + PatternMatching pattern_matching = 2; + repeated string models = 3; +} + +message HashedKeysClause { + repeated bytes hashed_keys = 1; } message MemberClause { @@ -125,6 +134,11 @@ message CompositeClause { repeated Clause clauses = 3; } +enum PatternMatching { + FixedLen = 0; + VariableLen = 1; +} + enum LogicalOperator { AND = 0; OR = 1; diff --git a/crates/torii/grpc/proto/world.proto b/crates/torii/grpc/proto/world.proto index 7ab0c6f971..76b88981a7 100644 --- a/crates/torii/grpc/proto/world.proto +++ b/crates/torii/grpc/proto/world.proto @@ -43,7 +43,7 @@ message MetadataResponse { message SubscribeModelsRequest { // The list of model keys to subscribe to. - repeated types.KeysClause models_keys = 1; + repeated types.ModelKeysClause models_keys = 1; } message SubscribeModelsResponse { @@ -52,11 +52,11 @@ message SubscribeModelsResponse { } message SubscribeEntitiesRequest { - repeated bytes hashed_keys = 1; + types.EntityKeysClause clause = 1; } message SubscribeEventMessagesRequest { - repeated bytes hashed_keys = 1; + types.EntityKeysClause clause = 1; } message SubscribeEntityResponse { @@ -83,7 +83,7 @@ message RetrieveEventsResponse { } message SubscribeEventsRequest { - types.EventKeysClause keys = 1; + types.KeysClause keys = 1; } message SubscribeEventsResponse { diff --git a/crates/torii/grpc/src/client.rs b/crates/torii/grpc/src/client.rs index e2db66576e..2bd5bc4411 100644 --- a/crates/torii/grpc/src/client.rs +++ b/crates/torii/grpc/src/client.rs @@ -6,7 +6,6 @@ use futures_util::{Stream, StreamExt, TryStreamExt}; use starknet::core::types::{FromStrError, StateDiff, StateUpdate}; use starknet_crypto::FieldElement; -use crate::proto::types::EventKeysClause; use crate::proto::world::{ world_client, MetadataRequest, RetrieveEntitiesRequest, RetrieveEntitiesResponse, RetrieveEventsRequest, RetrieveEventsResponse, SubscribeEntitiesRequest, @@ -14,7 +13,7 @@ use crate::proto::world::{ SubscribeModelsRequest, SubscribeModelsResponse, }; use crate::types::schema::{self, Entity, SchemaError}; -use crate::types::{Event, EventQuery, KeysClause, Query}; +use crate::types::{EntityKeysClause, Event, EventQuery, KeysClause, ModelKeysClause, Query}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -105,12 +104,12 @@ impl WorldClient { /// Subscribe to entities updates of a World. pub async fn subscribe_entities( &mut self, - hashed_keys: Vec, + clause: Option, ) -> Result { - let hashed_keys = hashed_keys.iter().map(|hashed| hashed.to_bytes_be().to_vec()).collect(); + let clause = clause.map(|c| c.into()); let stream = self .inner - .subscribe_entities(SubscribeEntitiesRequest { hashed_keys }) + .subscribe_entities(SubscribeEntitiesRequest { clause }) .await .map_err(Error::Grpc) .map(|res| res.into_inner())?; @@ -124,12 +123,12 @@ impl WorldClient { /// Subscribe to event messages of a World. pub async fn subscribe_event_messages( &mut self, - hashed_keys: Vec, + clause: Option, ) -> Result { - let hashed_keys = hashed_keys.iter().map(|hashed| hashed.to_bytes_be().to_vec()).collect(); + let clause = clause.map(|c| c.into()); let stream = self .inner - .subscribe_event_messages(SubscribeEntitiesRequest { hashed_keys }) + .subscribe_event_messages(SubscribeEntitiesRequest { clause }) .await .map_err(Error::Grpc) .map(|res| res.into_inner())?; @@ -143,11 +142,9 @@ impl WorldClient { /// Subscribe to the events of a World. pub async fn subscribe_events( &mut self, - keys: Option>, + keys: Option, ) -> Result { - let keys = keys.map(|keys| EventKeysClause { - keys: keys.iter().map(|key| key.to_bytes_be().to_vec()).collect(), - }); + let keys = keys.map(|c| c.into()); let stream = self .inner @@ -165,7 +162,7 @@ impl WorldClient { /// Subscribe to the model diff for a set of models of a World. pub async fn subscribe_model_diffs( &mut self, - models_keys: Vec, + models_keys: Vec, ) -> Result { let stream = self .inner diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index ca1eee2c79..9d2ad65e56 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -165,8 +165,8 @@ impl DojoWorld { ENTITIES_MODEL_RELATION_TABLE, ENTITIES_ENTITY_RELATION_COLUMN, None, - limit, - offset, + Some(limit), + Some(offset), ) .await } @@ -181,8 +181,8 @@ impl DojoWorld { EVENT_MESSAGES_MODEL_RELATION_TABLE, EVENT_MESSAGES_ENTITY_RELATION_COLUMN, None, - limit, - offset, + Some(limit), + Some(offset), ) .await } @@ -207,8 +207,8 @@ impl DojoWorld { model_relation_table: &str, entity_relation_column: &str, hashed_keys: Option, - limit: u32, - offset: u32, + limit: Option, + offset: Option, ) -> Result<(Vec, u32), Error> { // TODO: use prepared statement for where clause let filter_ids = match hashed_keys { @@ -244,7 +244,7 @@ impl DojoWorld { } // query to filter with limit and offset - let query = format!( + let mut query = format!( r#" SELECT {table}.id, group_concat({model_relation_table}.model_id) as model_ids FROM {table} @@ -252,15 +252,22 @@ impl DojoWorld { {filter_ids} GROUP BY {table}.id ORDER BY {table}.event_id DESC - LIMIT ? OFFSET ? "# ); + if limit.is_some() { + query += " LIMIT ?" + } + + if offset.is_some() { + query += " OFFSET ?" + } + let db_entities: Vec<(String, String)> = sqlx::query_as(&query).bind(limit).bind(offset).fetch_all(&self.pool).await?; let mut entities = Vec::with_capacity(db_entities.len()); - for (entity_id, models_str) in db_entities { + for (entity_id, models_str) in &db_entities { let model_ids: Vec<&str> = models_str.split(',').collect(); let schemas = self.model_cache.schemas(model_ids).await?; @@ -272,31 +279,14 @@ impl DojoWorld { Some(&format!("{table}.id = ?")), )?; - let row = sqlx::query(&entity_query).bind(&entity_id).fetch_one(&self.pool).await?; + let row = sqlx::query(&entity_query).bind(entity_id).fetch_one(&self.pool).await?; let mut arrays_rows = HashMap::new(); for (name, query) in arrays_queries { - let rows = sqlx::query(&query).bind(&entity_id).fetch_all(&self.pool).await?; + let rows = sqlx::query(&query).bind(entity_id).fetch_all(&self.pool).await?; arrays_rows.insert(name, rows); } - let models = schemas - .into_iter() - .map(|mut s| { - map_row_to_ty("", &s.name(), &mut s, &row, &arrays_rows)?; - - Ok(s.as_struct() - .expect("schema should be struct") - .to_owned() - .try_into() - .unwrap()) - }) - .collect::, Error>>()?; - - let hashed_keys = FieldElement::from_str(&entity_id).map_err(ParseError::FromStr)?; - entities.push(proto::types::Entity { - hashed_keys: hashed_keys.to_bytes_be().to_vec(), - models, - }) + entities.push(map_row_to_entity(&row, &arrays_rows, &schemas)?); } Ok((entities, total_count)) @@ -308,34 +298,60 @@ impl DojoWorld { model_relation_table: &str, entity_relation_column: &str, keys_clause: proto::types::KeysClause, - limit: u32, - offset: u32, + limit: Option, + offset: Option, ) -> Result<(Vec, u32), Error> { let keys = keys_clause .keys .iter() .map(|bytes| { if bytes.is_empty() { - return Ok("%".to_string()); + return Ok("0x[0-9a-fA-F]+".to_string()); } Ok(FieldElement::from_byte_slice_be(bytes) .map(|felt| format!("{felt:#x}")) .map_err(ParseError::FromByteSliceError)?) }) .collect::, Error>>()?; - let keys_pattern = keys.join("/") + "/%"; + let mut keys_pattern = format!("^{}", keys.join("/")); + + if keys_clause.pattern_matching == proto::types::PatternMatching::VariableLen as i32 { + keys_pattern += "(/0x[0-9a-fA-F]+)*"; + } + keys_pattern += "/$"; + // total count of rows that matches keys_pattern without limit and offset let count_query = format!( r#" SELECT count(*) FROM {table} - JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id - WHERE {model_relation_table}.model_id = '{:#x}' and {table}.keys LIKE ? + {} "#, - get_selector_from_name(&keys_clause.model).map_err(ParseError::NonAsciiName)? + if !keys_clause.models.is_empty() { + let model_ids = keys_clause + .models + .iter() + .map(|model| get_selector_from_name(model).map_err(ParseError::NonAsciiName)) + .collect::, _>>()?; + let model_ids_str = + model_ids.iter().map(|id| format!("'{:#x}'", id)).collect::>().join(","); + format!( + r#" + JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id + WHERE {model_relation_table}.model_id IN ({}) + AND {table}.keys REGEXP ? + "#, + model_ids_str + ) + } else { + format!( + r#" + WHERE {table}.keys REGEXP ? + "# + ) + } ); - // total count of rows that matches keys_pattern without limit and offset let total_count = sqlx::query_scalar(&count_query).bind(&keys_pattern).fetch_one(&self.pool).await?; @@ -343,83 +359,105 @@ impl DojoWorld { return Ok((Vec::new(), 0)); } - let models_query = format!( + let mut models_query = format!( r#" - SELECT group_concat({model_relation_table}.model_id) as model_ids + SELECT {table}.id, group_concat({model_relation_table}.model_id) as model_ids FROM {table} JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id - WHERE {table}.keys LIKE ? + WHERE {table}.keys REGEXP ? GROUP BY {table}.id - HAVING INSTR(model_ids, '{:#x}') > 0 - LIMIT 1 - "#, - get_selector_from_name(&keys_clause.model).map_err(ParseError::NonAsciiName)? + "# ); - let (models_str,): (String,) = - sqlx::query_as(&models_query).bind(&keys_pattern).fetch_one(&self.pool).await?; - let model_ids = models_str.split(',').collect::>(); - let schemas = self.model_cache.schemas(model_ids).await?; + if !keys_clause.models.is_empty() { + // filter by models + models_query += &format!( + "HAVING {}", + keys_clause + .models + .iter() + .map(|model| { + let model_id = + get_selector_from_name(model).map_err(ParseError::NonAsciiName)?; + Ok(format!("INSTR(model_ids, '{:#x}') > 0", model_id)) + }) + .collect::, Error>>()? + .join(" OR ") + .as_str() + ); + } - // query to filter with limit and offset - let (entities_query, arrays_queries) = build_sql_query( - &schemas, - table, - entity_relation_column, - Some(&format!("{table}.keys LIKE ? ORDER BY {table}.event_id DESC LIMIT ? OFFSET ?")), - Some(&format!("{table}.keys LIKE ? ORDER BY {table}.event_id DESC LIMIT ? OFFSET ?")), - )?; - let db_entities = sqlx::query(&entities_query) + models_query += &format!(" ORDER BY {table}.event_id DESC"); + + if limit.is_some() { + models_query += " LIMIT ?"; + } + if offset.is_some() { + models_query += " OFFSET ?"; + } + + let db_entities: Vec<(String, String)> = sqlx::query_as(&models_query) .bind(&keys_pattern) .bind(limit) .bind(offset) .fetch_all(&self.pool) .await?; - let mut arrays_rows = HashMap::new(); - for (name, query) in arrays_queries { - let rows = sqlx::query(&query) - .bind(&keys_pattern) - .bind(limit) - .bind(offset) - .fetch_all(&self.pool) - .await?; - arrays_rows.insert(name, rows); + + let mut entities = Vec::with_capacity(db_entities.len()); + for (entity_id, models_strs) in &db_entities { + let model_ids: Vec<&str> = models_strs.split(',').collect(); + let schemas = self.model_cache.schemas(model_ids).await?; + + let (entity_query, arrays_queries) = build_sql_query( + &schemas, + table, + entity_relation_column, + Some(&format!("{table}.id = ?")), + Some(&format!("{table}.id = ?")), + )?; + + let row = sqlx::query(&entity_query).bind(entity_id).fetch_one(&self.pool).await?; + let mut arrays_rows = HashMap::new(); + for (name, query) in arrays_queries { + let rows = sqlx::query(&query).bind(entity_id).fetch_all(&self.pool).await?; + arrays_rows.insert(name, rows); + } + + entities.push(map_row_to_entity(&row, &arrays_rows, &schemas)?); } - Ok(( - db_entities - .iter() - .map(|row| Self::map_row_to_entity(row, &arrays_rows, &schemas)) - .collect::, Error>>()?, - total_count, - )) + Ok((entities, total_count)) } pub(crate) async fn events_by_keys( &self, - keys_clause: proto::types::EventKeysClause, - limit: u32, - offset: u32, + keys_clause: proto::types::KeysClause, + limit: Option, + offset: Option, ) -> Result, Error> { let keys = keys_clause .keys .iter() .map(|bytes| { if bytes.is_empty() { - return Ok("%".to_string()); + return Ok("0x[0-9a-fA-F]+".to_string()); } - Ok(FieldElement::from_byte_slice_be(bytes) .map(|felt| format!("{felt:#x}")) .map_err(ParseError::FromByteSliceError)?) }) .collect::, Error>>()?; - let keys_pattern = keys.join("/") + "/%"; + let mut keys_pattern = format!("^{}", keys.join("/")); + + if keys_clause.pattern_matching == proto::types::PatternMatching::VariableLen as i32 { + keys_pattern += "(/0x[0-9a-fA-F]+)*"; + } + keys_pattern += "/$"; let events_query = r#" SELECT keys, data, transaction_hash FROM events - WHERE keys LIKE ? + WHERE keys REGEXP ? ORDER BY id DESC LIMIT ? OFFSET ? "# @@ -441,8 +479,8 @@ impl DojoWorld { model_relation_table: &str, entity_relation_column: &str, member_clause: proto::types::MemberClause, - _limit: u32, - _offset: u32, + limit: Option, + offset: Option, ) -> Result<(Vec, u32), Error> { let comparison_operator = ComparisonOperator::from_repr(member_clause.operator as usize) .expect("invalid comparison operator"); @@ -489,12 +527,19 @@ impl DojoWorld { &schemas, table, entity_relation_column, - Some(&format!("{table_name}.{column_name} {comparison_operator} ?")), + Some(&format!( + "{table_name}.{column_name} {comparison_operator} ? ORDER BY {table}.event_id \ + DESC LIMIT ? OFFSET ?" + )), None, )?; - let db_entities = - sqlx::query(&entity_query).bind(comparison_value.clone()).fetch_all(&self.pool).await?; + let db_entities = sqlx::query(&entity_query) + .bind(comparison_value.clone()) + .bind(limit) + .bind(offset) + .fetch_all(&self.pool) + .await?; let mut arrays_rows = HashMap::new(); for (name, query) in arrays_queries { let rows = @@ -504,7 +549,7 @@ impl DojoWorld { let entities_collection = db_entities .iter() - .map(|row| Self::map_row_to_entity(row, &arrays_rows, &schemas)) + .map(|row| map_row_to_entity(row, &arrays_rows, &schemas)) .collect::, Error>>()?; // Since there is not limit and offset, total_count is same as number of entities let total_count = entities_collection.len() as u32; @@ -517,8 +562,8 @@ impl DojoWorld { _model_relation_table: &str, _entity_relation_column: &str, _composite: proto::types::CompositeClause, - _limit: u32, - _offset: u32, + _limit: Option, + _offset: Option, ) -> Result<(Vec, u32), Error> { // TODO: Implement Err(QueryError::UnsupportedQuery.into()) @@ -560,7 +605,7 @@ impl DojoWorld { async fn subscribe_models( &self, - models_keys: Vec, + models_keys: Vec, ) -> Result>, Error> { let mut subs = Vec::with_capacity(models_keys.len()); for keys in models_keys { @@ -584,9 +629,9 @@ impl DojoWorld { async fn subscribe_entities( &self, - hashed_keys: Vec, + keys: Option, ) -> Result>, Error> { - self.entity_manager.add_subscriber(hashed_keys).await + self.entity_manager.add_subscriber(keys.map(|keys| keys.try_into().unwrap())).await } async fn retrieve_entities( @@ -610,8 +655,8 @@ impl DojoWorld { ENTITIES_MODEL_RELATION_TABLE, ENTITIES_ENTITY_RELATION_COLUMN, Some(hashed_keys), - query.limit, - query.offset, + Some(query.limit), + Some(query.offset), ) .await? } @@ -620,17 +665,13 @@ impl DojoWorld { return Err(QueryError::MissingParam("keys".into()).into()); } - if keys.model.is_empty() { - return Err(QueryError::MissingParam("model".into()).into()); - } - self.query_by_keys( ENTITIES_TABLE, ENTITIES_MODEL_RELATION_TABLE, ENTITIES_ENTITY_RELATION_COLUMN, keys, - query.limit, - query.offset, + Some(query.limit), + Some(query.offset), ) .await? } @@ -640,8 +681,8 @@ impl DojoWorld { ENTITIES_MODEL_RELATION_TABLE, ENTITIES_ENTITY_RELATION_COLUMN, member, - query.limit, - query.offset, + Some(query.limit), + Some(query.offset), ) .await? } @@ -651,8 +692,8 @@ impl DojoWorld { ENTITIES_MODEL_RELATION_TABLE, ENTITIES_ENTITY_RELATION_COLUMN, composite, - query.limit, - query.offset, + Some(query.limit), + Some(query.offset), ) .await? } @@ -665,9 +706,9 @@ impl DojoWorld { async fn subscribe_event_messages( &self, - hashed_keys: Vec, + keys: Option, ) -> Result>, Error> { - self.event_message_manager.add_subscriber(hashed_keys).await + self.event_message_manager.add_subscriber(keys.map(|keys| keys.try_into().unwrap())).await } async fn retrieve_event_messages( @@ -691,8 +732,8 @@ impl DojoWorld { EVENT_MESSAGES_MODEL_RELATION_TABLE, EVENT_MESSAGES_ENTITY_RELATION_COLUMN, Some(hashed_keys), - query.limit, - query.offset, + Some(query.limit), + Some(query.offset), ) .await? } @@ -701,17 +742,13 @@ impl DojoWorld { return Err(QueryError::MissingParam("keys".into()).into()); } - if keys.model.is_empty() { - return Err(QueryError::MissingParam("model".into()).into()); - } - self.query_by_keys( EVENT_MESSAGES_TABLE, EVENT_MESSAGES_MODEL_RELATION_TABLE, EVENT_MESSAGES_ENTITY_RELATION_COLUMN, keys, - query.limit, - query.offset, + Some(query.limit), + Some(query.offset), ) .await? } @@ -721,8 +758,8 @@ impl DojoWorld { EVENT_MESSAGES_MODEL_RELATION_TABLE, EVENT_MESSAGES_ENTITY_RELATION_COLUMN, member, - query.limit, - query.offset, + Some(query.limit), + Some(query.offset), ) .await? } @@ -732,8 +769,8 @@ impl DojoWorld { EVENT_MESSAGES_MODEL_RELATION_TABLE, ENTITIES_ENTITY_RELATION_COLUMN, composite, - query.limit, - query.offset, + Some(query.limit), + Some(query.offset), ) .await? } @@ -750,51 +787,16 @@ impl DojoWorld { ) -> Result { let events = match query.keys { None => self.events_all(query.limit, query.offset).await?, - Some(keys) => self.events_by_keys(keys, query.limit, query.offset).await?, + Some(keys) => self.events_by_keys(keys, Some(query.limit), Some(query.offset)).await?, }; Ok(RetrieveEventsResponse { events }) } async fn subscribe_events( &self, - clause: proto::types::EventKeysClause, + clause: proto::types::KeysClause, ) -> Result>, Error> { - self.event_manager - .add_subscriber( - clause - .keys - .iter() - .map(|key| { - FieldElement::from_byte_slice_be(key) - .map_err(ParseError::FromByteSliceError) - }) - .collect::, _>>()?, - ) - .await - } - - fn map_row_to_entity( - row: &SqliteRow, - arrays_rows: &HashMap>, - schemas: &[Ty], - ) -> Result { - let hashed_keys = - FieldElement::from_str(&row.get::("id")).map_err(ParseError::FromStr)?; - let models = schemas - .iter() - .map(|schema| { - let mut schema = schema.to_owned(); - map_row_to_ty("", &schema.name(), &mut schema, row, arrays_rows)?; - Ok(schema - .as_struct() - .expect("schema should be struct") - .to_owned() - .try_into() - .unwrap()) - }) - .collect::, Error>>()?; - - Ok(proto::types::Entity { hashed_keys: hashed_keys.to_bytes_be().to_vec(), models }) + self.event_manager.add_subscriber(clause.try_into().unwrap()).await } } @@ -817,6 +819,25 @@ fn map_row_to_event(row: &(String, String, String)) -> Result>, + schemas: &[Ty], +) -> Result { + let hashed_keys = + FieldElement::from_str(&row.get::("id")).map_err(ParseError::FromStr)?; + let models = schemas + .iter() + .map(|schema| { + let mut schema = schema.to_owned(); + map_row_to_ty("", &schema.name(), &mut schema, row, arrays_rows)?; + Ok(schema.as_struct().expect("schema should be struct").to_owned().try_into().unwrap()) + }) + .collect::, Error>>()?; + + Ok(proto::types::Entity { hashed_keys: hashed_keys.to_bytes_be().to_vec(), models }) +} + type ServiceResult = Result, Status>; type SubscribeModelsResponseStream = Pin> + Send>>; @@ -860,18 +881,9 @@ impl proto::world::world_server::World for DojoWorld { &self, request: Request, ) -> ServiceResult { - let SubscribeEntitiesRequest { hashed_keys } = request.into_inner(); - let hashed_keys = hashed_keys - .iter() - .map(|id| { - FieldElement::from_byte_slice_be(id) - .map_err(|e| Status::invalid_argument(e.to_string())) - }) - .collect::, _>>()?; - let rx = self - .subscribe_entities(hashed_keys) - .await - .map_err(|e| Status::internal(e.to_string()))?; + let SubscribeEntitiesRequest { clause } = request.into_inner(); + let rx = + self.subscribe_entities(clause).await.map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(Box::pin(ReceiverStream::new(rx)) as Self::SubscribeEntitiesStream)) } @@ -895,16 +907,9 @@ impl proto::world::world_server::World for DojoWorld { &self, request: Request, ) -> ServiceResult { - let SubscribeEntitiesRequest { hashed_keys } = request.into_inner(); - let hashed_keys = hashed_keys - .iter() - .map(|id| { - FieldElement::from_byte_slice_be(id) - .map_err(|e| Status::invalid_argument(e.to_string())) - }) - .collect::, _>>()?; + let SubscribeEntitiesRequest { clause } = request.into_inner(); let rx = self - .subscribe_event_messages(hashed_keys) + .subscribe_event_messages(clause) .await .map_err(|e| Status::internal(e.to_string()))?; diff --git a/crates/torii/grpc/src/server/subscriptions/entity.rs b/crates/torii/grpc/src/server/subscriptions/entity.rs index f9d4ae0d96..a62ee8dd69 100644 --- a/crates/torii/grpc/src/server/subscriptions/entity.rs +++ b/crates/torii/grpc/src/server/subscriptions/entity.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use std::str::FromStr; @@ -14,19 +14,22 @@ use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::RwLock; use torii_core::cache::ModelCache; use torii_core::error::{Error, ParseError}; -use torii_core::model::{build_sql_query, map_row_to_ty}; +use torii_core::model::build_sql_query; use torii_core::simple_broker::SimpleBroker; +use torii_core::sql::FELT_DELIMITER; use torii_core::types::Entity; use tracing::{error, trace}; use crate::proto; use crate::proto::world::SubscribeEntityResponse; +use crate::server::map_row_to_entity; +use crate::types::{EntityKeysClause, PatternMatching}; pub(crate) const LOG_TARGET: &str = "torii::grpc::server::subscriptions::entity"; pub struct EntitiesSubscriber { /// Entity ids that the subscriber is interested in - hashed_keys: HashSet, + keys: Option, /// The channel to send the response back to the subscriber. sender: Sender>, } @@ -39,7 +42,7 @@ pub struct EntityManager { impl EntityManager { pub async fn add_subscriber( &self, - hashed_keys: Vec, + keys: Option, ) -> Result>, Error> { let id = rand::thread_rng().gen::(); let (sender, receiver) = channel(1); @@ -49,10 +52,7 @@ impl EntityManager { // initial subscribe call let _ = sender.send(Ok(SubscribeEntityResponse { entity: None })).await; - self.subscribers.write().await.insert( - id, - EntitiesSubscriber { hashed_keys: hashed_keys.iter().cloned().collect(), sender }, - ); + self.subscribers.write().await.insert(id, EntitiesSubscriber { keys, sender }); Ok(receiver) } @@ -88,64 +88,124 @@ impl Service { subs: Arc, cache: Arc, pool: Pool, - hashed_keys: &str, + entity: &Entity, ) -> Result<(), Error> { let mut closed_stream = Vec::new(); + let hashed = FieldElement::from_str(&entity.id).map_err(ParseError::FromStr)?; + let keys = entity + .keys + .trim_end_matches(FELT_DELIMITER) + .split(FELT_DELIMITER) + .map(FieldElement::from_str) + .collect::, _>>() + .map_err(ParseError::FromStr)?; for (idx, sub) in subs.subscribers.read().await.iter() { - let hashed = FieldElement::from_str(hashed_keys).map_err(ParseError::FromStr)?; - // publish all updates if ids is empty or only ids that are subscribed to - if sub.hashed_keys.is_empty() || sub.hashed_keys.contains(&hashed) { - let models_query = r#" - SELECT group_concat(entity_model.model_id) as model_ids - FROM entities - JOIN entity_model ON entities.id = entity_model.entity_id - WHERE entities.id = ? - GROUP BY entities.id - "#; - let (model_ids,): (String,) = - sqlx::query_as(models_query).bind(hashed_keys).fetch_one(&pool).await?; - let model_ids: Vec<&str> = model_ids.split(',').collect(); - let schemas = cache.schemas(model_ids).await?; - - let (entity_query, arrays_queries) = build_sql_query( - &schemas, - "entities", - "entity_id", - Some("entities.id = ?"), - Some("entities.id = ?"), - )?; - - let row = sqlx::query(&entity_query).bind(hashed_keys).fetch_one(&pool).await?; - let mut arrays_rows = HashMap::new(); - for (name, query) in arrays_queries { - let row = sqlx::query(&query).bind(hashed_keys).fetch_all(&pool).await?; - arrays_rows.insert(name, row); + // Check if the subscriber is interested in this entity + // If we have a clause of hashed keys, then check that the id of the entity + // is in the list of hashed keys. + + // If we have a clause of keys, then check that the key pattern of the entity + // matches the key pattern of the subscriber. + match &sub.keys { + Some(EntityKeysClause::HashedKeys(hashed_keys)) => { + if !hashed_keys.contains(&hashed) { + continue; + } } + Some(EntityKeysClause::Keys(clause)) => { + // if we have a model clause, then we need to check that the entity + // has an updated model and that the model name matches the clause + if let Some(updated_model) = &entity.updated_model { + if !clause.models.is_empty() + && !clause.models.contains(&updated_model.name()) + { + continue; + } + } + + // if the key pattern doesnt match our subscribers key pattern, skip + // ["", "0x0"] would match with keys ["0x...", "0x0", ...] + if clause.pattern_matching == PatternMatching::FixedLen + && keys.len() != clause.keys.len() + { + continue; + } + + if !keys.iter().enumerate().all(|(idx, key)| { + // this is going to be None if our key pattern overflows the subscriber + // key pattern in this case we should skip + let sub_key = clause.keys.get(idx); + + match sub_key { + Some(sub_key) => { + if sub_key == &FieldElement::ZERO { + true + } else { + key == sub_key + } + } + // we overflowed the subscriber key pattern + // but we're in VariableLen pattern matching + // so we should match all next keys + None => true, + } + }) { + continue; + } + } + // if None, then we are interested in all entities + None => {} + } - let models = schemas - .into_iter() - .map(|mut s| { - map_row_to_ty("", &s.name(), &mut s, &row, &arrays_rows)?; - - Ok(s.as_struct() - .expect("schema should be a struct") - .to_owned() - .try_into() - .unwrap()) - }) - .collect::, Error>>()?; - + if entity.updated_model.is_none() { let resp = proto::world::SubscribeEntityResponse { entity: Some(proto::types::Entity { hashed_keys: hashed.to_bytes_be().to_vec(), - models, + models: vec![], }), }; if sub.sender.send(Ok(resp)).await.is_err() { closed_stream.push(*idx); } + + continue; + } + + let models_query = r#" + SELECT group_concat(entity_model.model_id) as model_ids + FROM entities + JOIN entity_model ON entities.id = entity_model.entity_id + WHERE entities.id = ? + GROUP BY entities.id + "#; + let (model_ids,): (String,) = + sqlx::query_as(models_query).bind(&entity.id).fetch_one(&pool).await?; + let model_ids: Vec<&str> = model_ids.split(',').collect(); + let schemas = cache.schemas(model_ids).await?; + + let (entity_query, arrays_queries) = build_sql_query( + &schemas, + "entities", + "entity_id", + Some("entities.id = ?"), + Some("entities.id = ?"), + )?; + + let row = sqlx::query(&entity_query).bind(&entity.id).fetch_one(&pool).await?; + let mut arrays_rows = HashMap::new(); + for (name, query) in arrays_queries { + let row = sqlx::query(&query).bind(&entity.id).fetch_all(&pool).await?; + arrays_rows.insert(name, row); + } + + let resp = proto::world::SubscribeEntityResponse { + entity: Some(map_row_to_entity(&row, &arrays_rows, &schemas)?), + }; + + if sub.sender.send(Ok(resp)).await.is_err() { + closed_stream.push(*idx); } } @@ -169,7 +229,7 @@ impl Future for Service { let cache = Arc::clone(&pin.model_cache); let pool = pin.pool.clone(); tokio::spawn(async move { - if let Err(e) = Service::publish_updates(subs, cache, pool, &entity.id).await { + if let Err(e) = Service::publish_updates(subs, cache, pool, &entity).await { error!(target = LOG_TARGET, error = %e, "Publishing entity update."); } }); diff --git a/crates/torii/grpc/src/server/subscriptions/event.rs b/crates/torii/grpc/src/server/subscriptions/event.rs index 1b44ed7c23..6a0aa38924 100644 --- a/crates/torii/grpc/src/server/subscriptions/event.rs +++ b/crates/torii/grpc/src/server/subscriptions/event.rs @@ -19,12 +19,13 @@ use tracing::{error, trace}; use crate::proto; use crate::proto::world::SubscribeEventsResponse; +use crate::types::{KeysClause, PatternMatching}; pub(crate) const LOG_TARGET: &str = "torii::grpc::server::subscriptions::event"; pub struct EventSubscriber { /// Event keys that the subscriber is interested in - keys: Vec, + keys: KeysClause, /// The channel to send the response back to the subscriber. sender: Sender>, } @@ -37,7 +38,7 @@ pub struct EventManager { impl EventManager { pub async fn add_subscriber( &self, - keys: Vec, + keys: KeysClause, ) -> Result>, Error> { let id = rand::thread_rng().gen::(); let (sender, receiver) = channel(1); @@ -86,22 +87,52 @@ impl Service { .map_err(ParseError::from)?; for (idx, sub) in subs.subscribers.read().await.iter() { - // publish all updates if ids is empty or only ids that are subscribed to - if sub.keys.is_empty() || keys.starts_with(&sub.keys) { - let resp = proto::world::SubscribeEventsResponse { - event: Some(proto::types::Event { - keys: keys.iter().map(|k| k.to_bytes_be().to_vec()).collect(), - data: data.iter().map(|d| d.to_bytes_be().to_vec()).collect(), - transaction_hash: FieldElement::from_str(&event.transaction_hash) - .map_err(ParseError::from)? - .to_bytes_be() - .to_vec(), - }), - }; - - if sub.sender.send(Ok(resp)).await.is_err() { - closed_stream.push(*idx); + // if the key pattern doesnt match our subscribers key pattern, skip + // ["", "0x0"] would match with keys ["0x...", "0x0", ...] + if sub.keys.pattern_matching == PatternMatching::FixedLen + && keys.len() != sub.keys.keys.len() + { + continue; + } + + if !keys.iter().enumerate().all(|(idx, key)| { + // this is going to be None if our key pattern overflows the subscriber key pattern + // in this case we might want to list all events with the same + // key selector so we can match them all + let sub_key = sub.keys.keys.get(idx); + + // if we have a key in the subscriber, it must match the key in the event + // unless its empty, which is a wildcard + match sub_key { + Some(sub_key) => { + if sub_key == &FieldElement::ZERO { + true + } else { + key == sub_key + } + } + // we overflowed the subscriber key pattern + // but we're in VariableLen pattern matching + // so we should match all next keys + None => true, } + }) { + continue; + } + + let resp = proto::world::SubscribeEventsResponse { + event: Some(proto::types::Event { + keys: keys.iter().map(|k| k.to_bytes_be().to_vec()).collect(), + data: data.iter().map(|d| d.to_bytes_be().to_vec()).collect(), + transaction_hash: FieldElement::from_str(&event.transaction_hash) + .map_err(ParseError::from)? + .to_bytes_be() + .to_vec(), + }), + }; + + if sub.sender.send(Ok(resp)).await.is_err() { + closed_stream.push(*idx); } } diff --git a/crates/torii/grpc/src/server/subscriptions/event_message.rs b/crates/torii/grpc/src/server/subscriptions/event_message.rs index 67cf1cf172..0b4b490ded 100644 --- a/crates/torii/grpc/src/server/subscriptions/event_message.rs +++ b/crates/torii/grpc/src/server/subscriptions/event_message.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use std::str::FromStr; @@ -14,18 +14,21 @@ use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::RwLock; use torii_core::cache::ModelCache; use torii_core::error::{Error, ParseError}; -use torii_core::model::{build_sql_query, map_row_to_ty}; +use torii_core::model::build_sql_query; use torii_core::simple_broker::SimpleBroker; +use torii_core::sql::FELT_DELIMITER; use torii_core::types::EventMessage; use tracing::{error, trace}; use crate::proto; use crate::proto::world::SubscribeEntityResponse; +use crate::server::map_row_to_entity; +use crate::types::{EntityKeysClause, PatternMatching}; pub(crate) const LOG_TARGET: &str = "torii::grpc::server::subscriptions::event_message"; pub struct EventMessagesSubscriber { - /// Entity ids that the subscriber is interested in - hashed_keys: HashSet, + /// Entity keys that the subscriber is interested in + keys: Option, /// The channel to send the response back to the subscriber. sender: Sender>, } @@ -38,7 +41,7 @@ pub struct EventMessageManager { impl EventMessageManager { pub async fn add_subscriber( &self, - hashed_keys: Vec, + keys: Option, ) -> Result>, Error> { let id = rand::thread_rng().gen::(); let (sender, receiver) = channel(1); @@ -48,10 +51,7 @@ impl EventMessageManager { // initial subscribe call let _ = sender.send(Ok(SubscribeEntityResponse { entity: None })).await; - self.subscribers.write().await.insert( - id, - EventMessagesSubscriber { hashed_keys: hashed_keys.iter().cloned().collect(), sender }, - ); + self.subscribers.write().await.insert(id, EventMessagesSubscriber { keys, sender }); Ok(receiver) } @@ -87,63 +87,110 @@ impl Service { subs: Arc, cache: Arc, pool: Pool, - hashed_keys: &str, + entity: &EventMessage, ) -> Result<(), Error> { let mut closed_stream = Vec::new(); + let hashed = FieldElement::from_str(&entity.id).map_err(ParseError::FromStr)?; + let keys = entity + .keys + .trim_end_matches(FELT_DELIMITER) + .split(FELT_DELIMITER) + .map(FieldElement::from_str) + .collect::, _>>() + .map_err(ParseError::FromStr)?; for (idx, sub) in subs.subscribers.read().await.iter() { - let hashed = FieldElement::from_str(hashed_keys).map_err(ParseError::FromStr)?; + // Check if the subscriber is interested in this entity + // If we have a clause of hashed keys, then check that the id of the entity + // is in the list of hashed keys. + + // If we have a clause of keys, then check that the key pattern of the entity + // matches the key pattern of the subscriber. + match &sub.keys { + Some(EntityKeysClause::HashedKeys(hashed_keys)) => { + if !hashed_keys.contains(&hashed) { + continue; + } + } + Some(EntityKeysClause::Keys(clause)) => { + // if we have a model clause, then we need to check that the entity + // has an updated model and that the model name matches the clause + if let Some(updated_model) = &entity.updated_model { + if !clause.models.is_empty() + && !clause.models.contains(&updated_model.name()) + { + continue; + } + } + + // if the key pattern doesnt match our subscribers key pattern, skip + // ["", "0x0"] would match with keys ["0x...", "0x0", ...] + if clause.pattern_matching == PatternMatching::FixedLen + && keys.len() != clause.keys.len() + { + continue; + } + + if !keys.iter().enumerate().all(|(idx, key)| { + // this is going to be None if our key pattern overflows the subscriber + // key pattern in this case we should skip + let sub_key = clause.keys.get(idx); + + match sub_key { + Some(sub_key) => { + if sub_key == &FieldElement::ZERO { + true + } else { + key == sub_key + } + } + // we overflowed the subscriber key pattern + // but we're in VariableLen pattern matching + // so we should match all next keys + None => true, + } + }) { + continue; + } + } + // if None, then we are interested in all entities + None => {} + } + // publish all updates if ids is empty or only ids that are subscribed to - if sub.hashed_keys.is_empty() || sub.hashed_keys.contains(&hashed) { - let models_query = r#" + let models_query = r#" SELECT group_concat(event_model.model_id) as model_ids FROM event_messages JOIN event_model ON event_messages.id = event_model.entity_id WHERE event_messages.id = ? GROUP BY event_messages.id "#; - let (model_ids,): (String,) = - sqlx::query_as(models_query).bind(hashed_keys).fetch_one(&pool).await?; - let model_ids: Vec<&str> = model_ids.split(',').collect(); - let schemas = cache.schemas(model_ids).await?; - - let (entity_query, arrays_queries) = build_sql_query( - &schemas, - "event_messages", - "event_message_id", - Some("event_messages.id = ?"), - Some("event_messages.id = ?"), - )?; - - let row = sqlx::query(&entity_query).bind(hashed_keys).fetch_one(&pool).await?; - let mut arrays_rows = HashMap::new(); - for (name, query) in arrays_queries { - let rows = sqlx::query(&query).bind(hashed_keys).fetch_all(&pool).await?; - arrays_rows.insert(name, rows); - } + let (model_ids,): (String,) = + sqlx::query_as(models_query).bind(&entity.id).fetch_one(&pool).await?; + let model_ids: Vec<&str> = model_ids.split(',').collect(); + let schemas = cache.schemas(model_ids).await?; + + let (entity_query, arrays_queries) = build_sql_query( + &schemas, + "event_messages", + "event_message_id", + Some("event_messages.id = ?"), + Some("event_messages.id = ?"), + )?; + + let row = sqlx::query(&entity_query).bind(&entity.id).fetch_one(&pool).await?; + let mut arrays_rows = HashMap::new(); + for (name, query) in arrays_queries { + let rows = sqlx::query(&query).bind(&entity.id).fetch_all(&pool).await?; + arrays_rows.insert(name, rows); + } - let models = schemas - .into_iter() - .map(|mut s| { - map_row_to_ty("", &s.name(), &mut s, &row, &arrays_rows)?; - Ok(s.as_struct() - .expect("schema should be a struct") - .to_owned() - .try_into() - .unwrap()) - }) - .collect::, Error>>()?; - - let resp = proto::world::SubscribeEntityResponse { - entity: Some(proto::types::Entity { - hashed_keys: hashed.to_bytes_be().to_vec(), - models, - }), - }; - - if sub.sender.send(Ok(resp)).await.is_err() { - closed_stream.push(*idx); - } + let resp = proto::world::SubscribeEntityResponse { + entity: Some(map_row_to_entity(&row, &arrays_rows, &schemas)?), + }; + + if sub.sender.send(Ok(resp)).await.is_err() { + closed_stream.push(*idx); } } @@ -167,7 +214,7 @@ impl Future for Service { let cache = Arc::clone(&pin.model_cache); let pool = pin.pool.clone(); tokio::spawn(async move { - if let Err(e) = Service::publish_updates(subs, cache, pool, &entity.id).await { + if let Err(e) = Service::publish_updates(subs, cache, pool, &entity).await { error!(target = LOG_TARGET, error = %e, "Publishing entity update."); } }); diff --git a/crates/torii/grpc/src/server/subscriptions/model_diff.rs b/crates/torii/grpc/src/server/subscriptions/model_diff.rs index 8e1f4e80cf..fbd90d01b8 100644 --- a/crates/torii/grpc/src/server/subscriptions/model_diff.rs +++ b/crates/torii/grpc/src/server/subscriptions/model_diff.rs @@ -21,7 +21,7 @@ use tracing::{debug, error, trace}; use super::error::SubscriptionError; use crate::proto; use crate::proto::world::SubscribeModelsResponse; -use crate::types::KeysClause; +use crate::types::ModelKeysClause; pub(crate) const LOG_TARGET: &str = "torii::grpc::server::subscriptions::model_diff"; @@ -32,7 +32,7 @@ pub struct ModelMetadata { pub struct ModelDiffRequest { pub model: ModelMetadata, - pub keys: proto::types::KeysClause, + pub keys: proto::types::ModelKeysClause, } impl ModelDiffRequest {} @@ -62,7 +62,7 @@ impl StateDiffManager { let storage_addresses = reqs .into_iter() .map(|req| { - let keys: KeysClause = + let keys: ModelKeysClause = req.keys.try_into().map_err(ParseError::FromByteSliceError)?; let base = poseidon_hash_many(&[ diff --git a/crates/torii/grpc/src/server/tests/entities_test.rs b/crates/torii/grpc/src/server/tests/entities_test.rs index 6d5cdbe9a1..9761d86335 100644 --- a/crates/torii/grpc/src/server/tests/entities_test.rs +++ b/crates/torii/grpc/src/server/tests/entities_test.rs @@ -26,14 +26,16 @@ use torii_core::processors::register_model::RegisterModelProcessor; use torii_core::processors::store_set_record::StoreSetRecordProcessor; use torii_core::sql::Sql; +use crate::proto::types::KeysClause; use crate::server::DojoWorld; use crate::types::schema::Entity; -use crate::types::KeysClause; #[tokio::test(flavor = "multi_thread")] async fn test_entities_queries() { - let options = - SqliteConnectOptions::from_str("sqlite::memory:").unwrap().create_if_missing(true); + let options = SqliteConnectOptions::from_str("sqlite::memory:") + .unwrap() + .create_if_missing(true) + .with_regexp(); let pool = SqlitePoolOptions::new().max_connections(5).connect_with(options).await.unwrap(); sqlx::migrate!("../migrations").run(&pool).await.unwrap(); @@ -116,9 +118,13 @@ async fn test_entities_queries() { "entities", "entity_model", "entity_id", - KeysClause { model: "Moves".to_string(), keys: vec![account.address()] }.into(), - 1, - 0, + KeysClause { + keys: vec![account.address().to_bytes_be().to_vec()], + pattern_matching: 0, + models: vec![], + }, + Some(1), + None, ) .await .unwrap() diff --git a/crates/torii/grpc/src/types/mod.rs b/crates/torii/grpc/src/types/mod.rs index 63c108e96b..cf00ede93f 100644 --- a/crates/torii/grpc/src/types/mod.rs +++ b/crates/torii/grpc/src/types/mod.rs @@ -30,11 +30,30 @@ pub enum Clause { } #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] -pub struct KeysClause { +pub enum EntityKeysClause { + HashedKeys(Vec), + Keys(KeysClause), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] +pub struct ModelKeysClause { pub model: String, pub keys: Vec, } +#[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] +pub struct KeysClause { + pub keys: Vec, + pub pattern_matching: PatternMatching, + pub models: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] +pub enum PatternMatching { + FixedLen, + VariableLen, +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] pub struct MemberClause { pub model: String, @@ -153,6 +172,39 @@ impl From for proto::types::Query { } } +impl From for PatternMatching { + fn from(value: proto::types::PatternMatching) -> Self { + match value { + proto::types::PatternMatching::FixedLen => PatternMatching::FixedLen, + proto::types::PatternMatching::VariableLen => PatternMatching::VariableLen, + } + } +} + +impl From for proto::types::KeysClause { + fn from(value: KeysClause) -> Self { + Self { + keys: value.keys.iter().map(|k| k.to_bytes_be().into()).collect(), + pattern_matching: value.pattern_matching as i32, + models: value.models, + } + } +} + +impl TryFrom for KeysClause { + type Error = FromByteSliceError; + + fn try_from(value: proto::types::KeysClause) -> Result { + let keys = value + .keys + .iter() + .map(|k| FieldElement::from_byte_slice_be(k)) + .collect::, _>>()?; + + Ok(Self { keys, pattern_matching: value.pattern_matching().into(), models: value.models }) + } +} + impl From for proto::types::Clause { fn from(value: Clause) -> Self { match value { @@ -169,8 +221,46 @@ impl From for proto::types::Clause { } } -impl From for proto::types::KeysClause { - fn from(value: KeysClause) -> Self { +impl From for proto::types::EntityKeysClause { + fn from(value: EntityKeysClause) -> Self { + match value { + EntityKeysClause::HashedKeys(hashed_keys) => Self { + clause_type: Some(proto::types::entity_keys_clause::ClauseType::HashedKeys( + proto::types::HashedKeysClause { + hashed_keys: hashed_keys.iter().map(|k| k.to_bytes_be().into()).collect(), + }, + )), + }, + EntityKeysClause::Keys(keys) => Self { + clause_type: Some(proto::types::entity_keys_clause::ClauseType::Keys(keys.into())), + }, + } + } +} + +impl TryFrom for EntityKeysClause { + type Error = FromByteSliceError; + + fn try_from(value: proto::types::EntityKeysClause) -> Result { + match value.clause_type.expect("must have") { + proto::types::entity_keys_clause::ClauseType::HashedKeys(clause) => { + let keys = clause + .hashed_keys + .into_iter() + .map(|k| FieldElement::from_byte_slice_be(&k)) + .collect::, _>>()?; + + Ok(Self::HashedKeys(keys)) + } + proto::types::entity_keys_clause::ClauseType::Keys(clause) => { + Ok(Self::Keys(clause.try_into()?)) + } + } + } +} + +impl From for proto::types::ModelKeysClause { + fn from(value: ModelKeysClause) -> Self { Self { model: value.model, keys: value.keys.iter().map(|k| k.to_bytes_be().into()).collect(), @@ -178,10 +268,10 @@ impl From for proto::types::KeysClause { } } -impl TryFrom for KeysClause { +impl TryFrom for ModelKeysClause { type Error = FromByteSliceError; - fn try_from(value: proto::types::KeysClause) -> Result { + fn try_from(value: proto::types::ModelKeysClause) -> Result { let keys = value .keys .into_iter() @@ -314,19 +404,13 @@ impl TryFrom for Event { #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] pub struct EventQuery { - pub keys: Vec, + pub keys: KeysClause, pub limit: u32, pub offset: u32, } impl From for proto::types::EventQuery { fn from(value: EventQuery) -> Self { - Self { - keys: Some(proto::types::EventKeysClause { - keys: value.keys.iter().map(|k| k.to_bytes_be().into()).collect(), - }), - limit: value.limit, - offset: value.offset, - } + Self { keys: Some(value.keys.into()), limit: value.limit, offset: value.offset } } } diff --git a/crates/torii/types-test/Scarb.lock b/crates/torii/types-test/Scarb.lock index fbf70e2be2..eb11adee81 100644 --- a/crates/torii/types-test/Scarb.lock +++ b/crates/torii/types-test/Scarb.lock @@ -15,7 +15,7 @@ source = "git+https://github.com/dojoengine/dojo?tag=v0.7.2#3da5cad9fdd39b81551e [[package]] name = "types_test" -version = "0.7.0" +version = "0.7.2" dependencies = [ "dojo", ] diff --git a/examples/spawn-and-move/manifests/dev/abis/base/contracts/dojo_examples_actions_actions.json b/examples/spawn-and-move/manifests/dev/abis/base/contracts/dojo_examples_actions_actions.json index 4be28b68dd..574aa89668 100644 --- a/examples/spawn-and-move/manifests/dev/abis/base/contracts/dojo_examples_actions_actions.json +++ b/examples/spawn-and-move/manifests/dev/abis/base/contracts/dojo_examples_actions_actions.json @@ -225,6 +225,22 @@ "inputs": [], "outputs": [], "state_mutability": "external" + }, + { + "type": "function", + "name": "set_player_server_profile", + "inputs": [ + { + "name": "server_id", + "type": "core::integer::u32" + }, + { + "name": "name", + "type": "core::byte_array::ByteArray" + } + ], + "outputs": [], + "state_mutability": "external" } ] }, diff --git a/examples/spawn-and-move/manifests/dev/abis/base/models/dojo_examples_models_server_profile.json b/examples/spawn-and-move/manifests/dev/abis/base/models/dojo_examples_models_server_profile.json new file mode 100644 index 0000000000..9b523d5d2b --- /dev/null +++ b/examples/spawn-and-move/manifests/dev/abis/base/models/dojo_examples_models_server_profile.json @@ -0,0 +1,367 @@ +[ + { + "type": "impl", + "name": "DojoModelImpl", + "interface_name": "dojo::model::IModel" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::integer::u32" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::FieldLayout", + "members": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "layout", + "type": "dojo::database::introspect::Layout" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Layout", + "variants": [ + { + "name": "Fixed", + "type": "core::array::Span::" + }, + { + "name": "Struct", + "type": "core::array::Span::" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + }, + { + "name": "Enum", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Member", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "ty", + "type": "dojo::database::introspect::Ty" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Struct", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::<(core::felt252, dojo::database::introspect::Ty)>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::<(core::felt252, dojo::database::introspect::Ty)>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Enum", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::<(core::felt252, dojo::database::introspect::Ty)>" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Ty", + "variants": [ + { + "name": "Primitive", + "type": "core::felt252" + }, + { + "name": "Struct", + "type": "dojo::database::introspect::Struct" + }, + { + "name": "Enum", + "type": "dojo::database::introspect::Enum" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "dojo::model::IModel", + "items": [ + { + "type": "function", + "name": "selector", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "version", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u8" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "unpacked_size", + "inputs": [], + "outputs": [ + { + "type": "core::option::Option::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "packed_size", + "inputs": [], + "outputs": [ + { + "type": "core::option::Option::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "layout", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Layout" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "schema", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Ty" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "server_profileImpl", + "interface_name": "dojo_examples::models::Iserver_profile" + }, + { + "type": "struct", + "name": "dojo_examples::models::ServerProfile", + "members": [ + { + "name": "player", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "server_id", + "type": "core::integer::u32" + }, + { + "name": "name", + "type": "core::byte_array::ByteArray" + } + ] + }, + { + "type": "interface", + "name": "dojo_examples::models::Iserver_profile", + "items": [ + { + "type": "function", + "name": "ensure_abi", + "inputs": [ + { + "name": "model", + "type": "dojo_examples::models::ServerProfile" + } + ], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "event", + "name": "dojo_examples::models::server_profile::Event", + "kind": "enum", + "variants": [] + } +] \ No newline at end of file diff --git a/examples/spawn-and-move/manifests/dev/abis/deployments/contracts/dojo_examples_actions_actions.json b/examples/spawn-and-move/manifests/dev/abis/deployments/contracts/dojo_examples_actions_actions.json index 4be28b68dd..574aa89668 100644 --- a/examples/spawn-and-move/manifests/dev/abis/deployments/contracts/dojo_examples_actions_actions.json +++ b/examples/spawn-and-move/manifests/dev/abis/deployments/contracts/dojo_examples_actions_actions.json @@ -225,6 +225,22 @@ "inputs": [], "outputs": [], "state_mutability": "external" + }, + { + "type": "function", + "name": "set_player_server_profile", + "inputs": [ + { + "name": "server_id", + "type": "core::integer::u32" + }, + { + "name": "name", + "type": "core::byte_array::ByteArray" + } + ], + "outputs": [], + "state_mutability": "external" } ] }, diff --git a/examples/spawn-and-move/manifests/dev/abis/deployments/models/dojo_examples_models_server_profile.json b/examples/spawn-and-move/manifests/dev/abis/deployments/models/dojo_examples_models_server_profile.json new file mode 100644 index 0000000000..9b523d5d2b --- /dev/null +++ b/examples/spawn-and-move/manifests/dev/abis/deployments/models/dojo_examples_models_server_profile.json @@ -0,0 +1,367 @@ +[ + { + "type": "impl", + "name": "DojoModelImpl", + "interface_name": "dojo::model::IModel" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::integer::u32" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::FieldLayout", + "members": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "layout", + "type": "dojo::database::introspect::Layout" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Layout", + "variants": [ + { + "name": "Fixed", + "type": "core::array::Span::" + }, + { + "name": "Struct", + "type": "core::array::Span::" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + }, + { + "name": "Enum", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Member", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "ty", + "type": "dojo::database::introspect::Ty" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Struct", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::<(core::felt252, dojo::database::introspect::Ty)>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::<(core::felt252, dojo::database::introspect::Ty)>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Enum", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::<(core::felt252, dojo::database::introspect::Ty)>" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Ty", + "variants": [ + { + "name": "Primitive", + "type": "core::felt252" + }, + { + "name": "Struct", + "type": "dojo::database::introspect::Struct" + }, + { + "name": "Enum", + "type": "dojo::database::introspect::Enum" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "dojo::model::IModel", + "items": [ + { + "type": "function", + "name": "selector", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "version", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u8" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "unpacked_size", + "inputs": [], + "outputs": [ + { + "type": "core::option::Option::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "packed_size", + "inputs": [], + "outputs": [ + { + "type": "core::option::Option::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "layout", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Layout" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "schema", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Ty" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "server_profileImpl", + "interface_name": "dojo_examples::models::Iserver_profile" + }, + { + "type": "struct", + "name": "dojo_examples::models::ServerProfile", + "members": [ + { + "name": "player", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "server_id", + "type": "core::integer::u32" + }, + { + "name": "name", + "type": "core::byte_array::ByteArray" + } + ] + }, + { + "type": "interface", + "name": "dojo_examples::models::Iserver_profile", + "items": [ + { + "type": "function", + "name": "ensure_abi", + "inputs": [ + { + "name": "model", + "type": "dojo_examples::models::ServerProfile" + } + ], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "event", + "name": "dojo_examples::models::server_profile::Event", + "kind": "enum", + "variants": [] + } +] \ No newline at end of file diff --git a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples_actions_actions.toml b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples_actions_actions.toml index b1680ae07c..3e9c232447 100644 --- a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples_actions_actions.toml +++ b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples_actions_actions.toml @@ -1,6 +1,6 @@ kind = "DojoContract" -class_hash = "0x3b42f80dc8ac4628b0ed6c89af9055314c0aa2192ea0d9601f262138a1e50c3" -original_class_hash = "0x3b42f80dc8ac4628b0ed6c89af9055314c0aa2192ea0d9601f262138a1e50c3" +class_hash = "0x7d70dda5cb8dcb697ccc2c129254c554e8994f1b231527f7641a14706af016f" +original_class_hash = "0x7d70dda5cb8dcb697ccc2c129254c554e8994f1b231527f7641a14706af016f" base_class_hash = "0x0" abi = "manifests/dev/abis/base/contracts/dojo_examples_actions_actions.json" reads = [] diff --git a/examples/spawn-and-move/manifests/dev/base/models/dojo_examples_models_server_profile.toml b/examples/spawn-and-move/manifests/dev/base/models/dojo_examples_models_server_profile.toml new file mode 100644 index 0000000000..e2774133c8 --- /dev/null +++ b/examples/spawn-and-move/manifests/dev/base/models/dojo_examples_models_server_profile.toml @@ -0,0 +1,20 @@ +kind = "DojoModel" +class_hash = "0x6dc51a232ffcfaa02646636daf1b8acab8121fa69458258da8df1620aff07ab" +original_class_hash = "0x6dc51a232ffcfaa02646636daf1b8acab8121fa69458258da8df1620aff07ab" +abi = "manifests/dev/abis/base/models/dojo_examples_models_server_profile.json" +name = "dojo_examples::models::server_profile" + +[[members]] +name = "player" +type = "ContractAddress" +key = true + +[[members]] +name = "server_id" +type = "u32" +key = true + +[[members]] +name = "name" +type = "ByteArray" +key = false diff --git a/examples/spawn-and-move/manifests/dev/manifest.json b/examples/spawn-and-move/manifests/dev/manifest.json index a1a575d4aa..928d7596e1 100644 --- a/examples/spawn-and-move/manifests/dev/manifest.json +++ b/examples/spawn-and-move/manifests/dev/manifest.json @@ -1020,8 +1020,8 @@ { "kind": "DojoContract", "address": "0x5c70a663d6b48d8e4c6aaa9572e3735a732ac3765700d470463e670587852af", - "class_hash": "0x3b42f80dc8ac4628b0ed6c89af9055314c0aa2192ea0d9601f262138a1e50c3", - "original_class_hash": "0x3b42f80dc8ac4628b0ed6c89af9055314c0aa2192ea0d9601f262138a1e50c3", + "class_hash": "0x7d70dda5cb8dcb697ccc2c129254c554e8994f1b231527f7641a14706af016f", + "original_class_hash": "0x7d70dda5cb8dcb697ccc2c129254c554e8994f1b231527f7641a14706af016f", "base_class_hash": "0x22f3e55b61d86c2ac5239fa3b3b8761f26b9a5c0b5f61ddbd5d756ced498b46", "abi": [ { @@ -1250,6 +1250,22 @@ "inputs": [], "outputs": [], "state_mutability": "external" + }, + { + "type": "function", + "name": "set_player_server_profile", + "inputs": [ + { + "name": "server_id", + "type": "core::integer::u32" + }, + { + "name": "name", + "type": "core::byte_array::ByteArray" + } + ], + "outputs": [], + "state_mutability": "external" } ] }, @@ -4060,6 +4076,396 @@ ], "name": "dojo_examples::models::position" }, + { + "kind": "DojoModel", + "members": [ + { + "name": "player", + "type": "ContractAddress", + "key": true + }, + { + "name": "server_id", + "type": "u32", + "key": true + }, + { + "name": "name", + "type": "ByteArray", + "key": false + } + ], + "class_hash": "0x6dc51a232ffcfaa02646636daf1b8acab8121fa69458258da8df1620aff07ab", + "original_class_hash": "0x6dc51a232ffcfaa02646636daf1b8acab8121fa69458258da8df1620aff07ab", + "abi": [ + { + "type": "impl", + "name": "DojoModelImpl", + "interface_name": "dojo::model::IModel" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::integer::u32" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::FieldLayout", + "members": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "layout", + "type": "dojo::database::introspect::Layout" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Layout", + "variants": [ + { + "name": "Fixed", + "type": "core::array::Span::" + }, + { + "name": "Struct", + "type": "core::array::Span::" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + }, + { + "name": "Enum", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Member", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "ty", + "type": "dojo::database::introspect::Ty" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Struct", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::<(core::felt252, dojo::database::introspect::Ty)>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::<(core::felt252, dojo::database::introspect::Ty)>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Enum", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::<(core::felt252, dojo::database::introspect::Ty)>" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Ty", + "variants": [ + { + "name": "Primitive", + "type": "core::felt252" + }, + { + "name": "Struct", + "type": "dojo::database::introspect::Struct" + }, + { + "name": "Enum", + "type": "dojo::database::introspect::Enum" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "dojo::model::IModel", + "items": [ + { + "type": "function", + "name": "selector", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "version", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u8" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "unpacked_size", + "inputs": [], + "outputs": [ + { + "type": "core::option::Option::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "packed_size", + "inputs": [], + "outputs": [ + { + "type": "core::option::Option::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "layout", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Layout" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "schema", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Ty" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "server_profileImpl", + "interface_name": "dojo_examples::models::Iserver_profile" + }, + { + "type": "struct", + "name": "dojo_examples::models::ServerProfile", + "members": [ + { + "name": "player", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "server_id", + "type": "core::integer::u32" + }, + { + "name": "name", + "type": "core::byte_array::ByteArray" + } + ] + }, + { + "type": "interface", + "name": "dojo_examples::models::Iserver_profile", + "items": [ + { + "type": "function", + "name": "ensure_abi", + "inputs": [ + { + "name": "model", + "type": "dojo_examples::models::ServerProfile" + } + ], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "event", + "name": "dojo_examples::models::server_profile::Event", + "kind": "enum", + "variants": [] + } + ], + "name": "dojo_examples::models::server_profile" + }, { "kind": "DojoModel", "members": [ diff --git a/examples/spawn-and-move/manifests/dev/manifest.toml b/examples/spawn-and-move/manifests/dev/manifest.toml index c292ce25af..fd58cefd55 100644 --- a/examples/spawn-and-move/manifests/dev/manifest.toml +++ b/examples/spawn-and-move/manifests/dev/manifest.toml @@ -22,8 +22,8 @@ name = "dojo::base::base" [[contracts]] kind = "DojoContract" address = "0x5c70a663d6b48d8e4c6aaa9572e3735a732ac3765700d470463e670587852af" -class_hash = "0x3b42f80dc8ac4628b0ed6c89af9055314c0aa2192ea0d9601f262138a1e50c3" -original_class_hash = "0x3b42f80dc8ac4628b0ed6c89af9055314c0aa2192ea0d9601f262138a1e50c3" +class_hash = "0x7d70dda5cb8dcb697ccc2c129254c554e8994f1b231527f7641a14706af016f" +original_class_hash = "0x7d70dda5cb8dcb697ccc2c129254c554e8994f1b231527f7641a14706af016f" base_class_hash = "0x22f3e55b61d86c2ac5239fa3b3b8761f26b9a5c0b5f61ddbd5d756ced498b46" abi = "manifests/dev/abis/deployments/contracts/dojo_examples_actions_actions.json" reads = [] @@ -192,6 +192,28 @@ name = "vec" type = "Vec2" key = false +[[models]] +kind = "DojoModel" +class_hash = "0x6dc51a232ffcfaa02646636daf1b8acab8121fa69458258da8df1620aff07ab" +original_class_hash = "0x6dc51a232ffcfaa02646636daf1b8acab8121fa69458258da8df1620aff07ab" +abi = "manifests/dev/abis/deployments/models/dojo_examples_models_server_profile.json" +name = "dojo_examples::models::server_profile" + +[[models.members]] +name = "player" +type = "ContractAddress" +key = true + +[[models.members]] +name = "server_id" +type = "u32" +key = true + +[[models.members]] +name = "name" +type = "ByteArray" +key = false + [[models]] kind = "DojoModel" class_hash = "0x4b29afc6db744bd87f7276869620348557c11b984e9f3fcb27c4d55efb0ab6c" diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index 6737cdea48..241fe54ea3 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -7,6 +7,7 @@ trait IActions { fn set_player_config(ref world: IWorldDispatcher, name: ByteArray); fn get_player_position(world: @IWorldDispatcher) -> Position; fn reset_player_config(ref world: IWorldDispatcher); + fn set_player_server_profile(ref world: IWorldDispatcher, server_id: u32, name: ByteArray); } #[dojo::interface] @@ -21,7 +22,9 @@ mod actions { use super::IActionsComputed; use starknet::{ContractAddress, get_caller_address}; - use dojo_examples::models::{Position, Moves, Direction, Vec2, PlayerConfig, PlayerItem}; + use dojo_examples::models::{ + Position, Moves, Direction, Vec2, PlayerConfig, PlayerItem, ServerProfile + }; use dojo_examples::utils::next_position; #[derive(Copy, Drop, Serde)] @@ -111,6 +114,11 @@ mod actions { assert(config.name == empty_string, 'bad name'); } + fn set_player_server_profile(ref world: IWorldDispatcher, server_id: u32, name: ByteArray) { + let player = get_caller_address(); + set!(world, ServerProfile { player, server_id, name }); + } + fn get_player_position(world: @IWorldDispatcher) -> Position { let player = get_caller_address(); get!(world, player, (Position)) diff --git a/examples/spawn-and-move/src/models.cairo b/examples/spawn-and-move/src/models.cairo index 443c3a2e53..50da9c93f9 100644 --- a/examples/spawn-and-move/src/models.cairo +++ b/examples/spawn-and-move/src/models.cairo @@ -86,6 +86,16 @@ struct PlayerConfig { favorite_item: Option, } +#[derive(Drop, Serde)] +#[dojo::model] +struct ServerProfile { + #[key] + player: ContractAddress, + #[key] + server_id: u32, + name: ByteArray, +} + trait Vec2Trait { fn is_zero(self: Vec2) -> bool; fn is_equal(self: Vec2, b: Vec2) -> bool;