From 125be14545de87896376dfef3fe707500d9446d0 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:24:46 +0000 Subject: [PATCH 01/15] feat: add URL template support and scheduled resolver infrastructure Extend #[resolve] macro to support URL templates with field placeholders (e.g., url = "https://api.example.com/{field.path}/data") alongside the existing dotted-path syntax. Add AST types (UrlTemplatePart, UrlSource, ResolverCondition, ScheduledCallback), condition/schedule_at parameter parsing, compiler opcode extensions, and VM runtime template construction with field interpolation. Made-with: Cursor --- hyperstack-macros/src/ast/types.rs | 28 +++- hyperstack-macros/src/parse/attributes.rs | 58 ++++++-- .../src/stream_spec/ast_writer.rs | 59 +++++++- hyperstack-macros/src/stream_spec/entity.rs | 44 +++++- .../src/stream_spec/proto_struct.rs | 27 +++- hyperstack-macros/src/stream_spec/sections.rs | 42 ++++-- interpreter/src/ast.rs | 28 +++- interpreter/src/compiler.rs | 14 ++ interpreter/src/lib.rs | 4 +- interpreter/src/vm.rs | 127 +++++++++++++++++- 10 files changed, 382 insertions(+), 49 deletions(-) diff --git a/hyperstack-macros/src/ast/types.rs b/hyperstack-macros/src/ast/types.rs index 8fe03be0..a45446a5 100644 --- a/hyperstack-macros/src/ast/types.rs +++ b/hyperstack-macros/src/ast/types.rs @@ -269,14 +269,23 @@ pub enum HttpMethod { Post, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum UrlTemplatePart { + Literal(String), + FieldRef(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum UrlSource { + FieldPath(String), + Template(Vec), +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct UrlResolverConfig { - /// Field path to get the URL from (e.g., "info.uri") - pub url_path: String, - /// HTTP method to use (default: GET) + pub url_source: UrlSource, #[serde(default)] pub method: HttpMethod, - /// JSON path to extract from response (None = full payload) #[serde(default, skip_serializing_if = "Option::is_none")] pub extract_path: Option, } @@ -297,6 +306,13 @@ pub enum ResolveStrategy { LastWrite, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResolverCondition { + pub field_path: String, + pub op: ComparisonOp, + pub value: serde_json::Value, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResolverSpec { pub resolver: ResolverType, @@ -307,6 +323,10 @@ pub struct ResolverSpec { #[serde(default)] pub strategy: ResolveStrategy, pub extracts: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub schedule_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/hyperstack-macros/src/parse/attributes.rs b/hyperstack-macros/src/parse/attributes.rs index 564b86ed..80545adc 100644 --- a/hyperstack-macros/src/parse/attributes.rs +++ b/hyperstack-macros/src/parse/attributes.rs @@ -1005,11 +1005,14 @@ pub struct ResolveAttribute { pub from: Option, pub address: Option, pub url: Option, + pub url_is_template: bool, pub method: Option, pub extract: Option, pub target_field_name: String, pub resolver: Option, pub strategy: String, + pub condition: Option, + pub schedule_at: Option, } #[derive(Debug, Clone)] @@ -1020,16 +1023,21 @@ pub struct ResolveSpec { pub extract: Option, pub target_field_name: String, pub strategy: String, + pub condition: Option, + pub schedule_at: Option, } struct ResolveAttributeArgs { from: Option, address: Option, url: Option, + url_is_template: bool, method: Option, extract: Option, resolver: Option, strategy: Option, + condition: Option, + schedule_at: Option, } impl Parse for ResolveAttributeArgs { @@ -1037,10 +1045,13 @@ impl Parse for ResolveAttributeArgs { let mut from = None; let mut address = None; let mut url = None; + let mut url_is_template = false; let mut method = None; let mut extract = None; let mut resolver = None; let mut strategy = None; + let mut condition = None; + let mut schedule_at = None; while !input.is_empty() { let ident: syn::Ident = input.parse()?; @@ -1055,19 +1066,23 @@ impl Parse for ResolveAttributeArgs { let lit: syn::LitStr = input.parse()?; address = Some(lit.value()); } else if ident_str == "url" { - // Parse as dotted path (e.g., info.uri) - handle both dot-separated and single identifiers - let mut parts = Vec::new(); - let first: syn::Ident = input.parse()?; - parts.push(first.to_string()); + if input.peek(syn::LitStr) { + let lit: syn::LitStr = input.parse()?; + url = Some(lit.value()); + url_is_template = true; + } else { + let mut parts = Vec::new(); + let first: syn::Ident = input.parse()?; + parts.push(first.to_string()); + + while input.peek(Token![.]) { + input.parse::()?; + let next: syn::Ident = input.parse()?; + parts.push(next.to_string()); + } - // Parse any additional .identifier segments - while input.peek(Token![.]) { - input.parse::()?; - let next: syn::Ident = input.parse()?; - parts.push(next.to_string()); + url = Some(parts.join(".")); } - - url = Some(parts.join(".")); } else if ident_str == "method" { let method_ident: syn::Ident = input.parse()?; match method_ident.to_string().to_lowercase().as_str() { @@ -1091,6 +1106,21 @@ impl Parse for ResolveAttributeArgs { } else if ident_str == "strategy" { let ident: syn::Ident = input.parse()?; strategy = Some(ident.to_string()); + } else if ident_str == "condition" { + let lit: syn::LitStr = input.parse()?; + condition = Some(lit.value()); + } else if ident_str == "schedule_at" { + let mut parts = Vec::new(); + let first: syn::Ident = input.parse()?; + parts.push(first.to_string()); + + while input.peek(Token![.]) { + input.parse::()?; + let next: syn::Ident = input.parse()?; + parts.push(next.to_string()); + } + + schedule_at = Some(parts.join(".")); } else { return Err(syn::Error::new( ident.span(), @@ -1107,10 +1137,13 @@ impl Parse for ResolveAttributeArgs { from, address, url, + url_is_template, method, extract, resolver, strategy, + condition, + schedule_at, }) } } @@ -1165,11 +1198,14 @@ pub fn parse_resolve_attribute( from: args.from, address: args.address, url: args.url, + url_is_template: args.url_is_template, method: args.method.map(|m| m.to_string()), extract: args.extract, target_field_name: target_field_name.to_string(), resolver: args.resolver, strategy, + condition: args.condition, + schedule_at: args.schedule_at, })) } diff --git a/hyperstack-macros/src/stream_spec/ast_writer.rs b/hyperstack-macros/src/stream_spec/ast_writer.rs index ef6e7517..3db7f001 100644 --- a/hyperstack-macros/src/stream_spec/ast_writer.rs +++ b/hyperstack-macros/src/stream_spec/ast_writer.rs @@ -13,11 +13,11 @@ use crate::ast::writer::{ convert_idl_to_snapshot, parse_population_strategy, parse_transformation, }; use crate::ast::{ - ComputedFieldSpec, ConditionExpr, EntitySection, FieldPath, HookAction, IdentitySpec, - IdlSerializationSnapshot, InstructionHook, KeyResolutionStrategy, LookupIndexSpec, - MappingSource, ResolveStrategy, ResolverExtractSpec, ResolverHook, ResolverSpec, - ResolverStrategy, ResolverType, SerializableFieldMapping, SerializableHandlerSpec, - SerializableStreamSpec, SourceSpec, + ComparisonOp, ComputedFieldSpec, ConditionExpr, EntitySection, FieldPath, HookAction, + IdentitySpec, IdlSerializationSnapshot, InstructionHook, KeyResolutionStrategy, + LookupIndexSpec, MappingSource, ResolveStrategy, ResolverCondition, ResolverExtractSpec, + ResolverHook, ResolverSpec, ResolverStrategy, ResolverType, SerializableFieldMapping, + SerializableHandlerSpec, SerializableStreamSpec, SourceSpec, }; use crate::event_type_helpers::{find_idl_for_type, program_name_for_type, IdlLookup}; use crate::parse; @@ -197,6 +197,8 @@ fn build_resolver_specs(resolve_specs: &[parse::ResolveSpec]) -> Vec Vec ResolveStrategy { } } +fn parse_resolver_condition(s: &str) -> ResolverCondition { + let operators = ["==", "!=", ">=", "<=", ">", "<"]; + for op_str in &operators { + if let Some(pos) = s.find(op_str) { + let field_path = s[..pos].trim().to_string(); + let raw_value = s[pos + op_str.len()..].trim(); + let op = match *op_str { + "==" => ComparisonOp::Equal, + "!=" => ComparisonOp::NotEqual, + ">=" => ComparisonOp::GreaterThanOrEqual, + "<=" => ComparisonOp::LessThanOrEqual, + ">" => ComparisonOp::GreaterThan, + "<" => ComparisonOp::LessThan, + _ => unreachable!(), + }; + let value = match raw_value { + "null" => serde_json::Value::Null, + "true" => serde_json::Value::Bool(true), + "false" => serde_json::Value::Bool(false), + s if s.parse::().is_ok() => { + serde_json::json!(s.parse::().unwrap()) + } + s => serde_json::Value::String(s.trim_matches('"').to_string()), + }; + return ResolverCondition { + field_path, + op, + value, + }; + } + } + panic!("Invalid condition expression: '{}'. Expected format: 'field.path op value'", s); +} + fn resolver_type_key(resolver: &ResolverType) -> String { match resolver { ResolverType::Token => "token".to_string(), - ResolverType::Url(config) => format!("url:{}", config.url_path), + ResolverType::Url(config) => match &config.url_source { + crate::ast::UrlSource::FieldPath(path) => format!("url:{}", path), + crate::ast::UrlSource::Template(parts) => { + let key: String = parts.iter().map(|p| match p { + crate::ast::UrlTemplatePart::Literal(s) => s.clone(), + crate::ast::UrlTemplatePart::FieldRef(f) => format!("{{{}}}", f), + }).collect(); + format!("url:{}", key) + } + }, } } diff --git a/hyperstack-macros/src/stream_spec/entity.rs b/hyperstack-macros/src/stream_spec/entity.rs index efa608b7..a396771e 100644 --- a/hyperstack-macros/src/stream_spec/entity.rs +++ b/hyperstack-macros/src/stream_spec/entity.rs @@ -18,7 +18,7 @@ use std::collections::{HashMap, HashSet}; use quote::{format_ident, quote}; use syn::{Fields, GenericArgument, ItemStruct, PathArguments, Type}; -use crate::ast::{EntitySection, FieldTypeInfo, HttpMethod, ResolverHook, ResolverType, UrlResolverConfig}; +use crate::ast::{EntitySection, FieldTypeInfo, HttpMethod, ResolverHook, ResolverType, UrlResolverConfig, UrlSource, UrlTemplatePart}; use crate::codegen; use crate::event_type_helpers::IdlLookup; use crate::parse; @@ -54,6 +54,27 @@ use super::sections::{ process_nested_struct, }; +pub fn parse_url_template(s: &str) -> Vec { + let mut parts = Vec::new(); + let mut rest = s; + + while let Some(open) = rest.find('{') { + if open > 0 { + parts.push(UrlTemplatePart::Literal(rest[..open].to_string())); + } + let close = rest[open..].find('}').expect("Unclosed '{' in URL template") + open; + let field_ref = rest[open + 1..close].trim().to_string(); + parts.push(UrlTemplatePart::FieldRef(field_ref)); + rest = &rest[close + 1..]; + } + + if !rest.is_empty() { + parts.push(UrlTemplatePart::Literal(rest.to_string())); + } + + parts +} + // ============================================================================ // Entity Processing // ============================================================================ @@ -425,8 +446,7 @@ pub fn process_entity_struct_with_idl( }); // Determine resolver type: URL resolver if url is present, otherwise Token resolver - let resolver = if let Some(url_path) = resolve_attr.url.clone() { - // URL resolver + let resolver = if let Some(url_val) = resolve_attr.url.clone() { let method = resolve_attr.method.as_deref().map(|m| { match m.to_lowercase().as_str() { "post" => HttpMethod::Post, @@ -434,8 +454,14 @@ pub fn process_entity_struct_with_idl( } }).unwrap_or(HttpMethod::Get); + let url_source = if resolve_attr.url_is_template { + UrlSource::Template(parse_url_template(&url_val)) + } else { + UrlSource::FieldPath(url_val) + }; + ResolverType::Url(UrlResolverConfig { - url_path, + url_source, method, extract_path: resolve_attr.extract.clone(), }) @@ -449,13 +475,21 @@ pub fn process_entity_struct_with_idl( .unwrap_or_else(|err| panic!("{}", err)) }; + let from = if resolve_attr.url_is_template { + None + } else { + resolve_attr.url.clone().or(resolve_attr.from) + }; + resolve_specs.push(parse::ResolveSpec { resolver, - from: resolve_attr.url.clone().or(resolve_attr.from), + from, address: resolve_attr.address, extract: resolve_attr.extract, target_field_name: resolve_attr.target_field_name, strategy: resolve_attr.strategy, + condition: resolve_attr.condition, + schedule_at: resolve_attr.schedule_at, }); } else if let Ok(Some(computed_attr)) = parse::parse_computed_attribute(attr, &field_name.to_string()) diff --git a/hyperstack-macros/src/stream_spec/proto_struct.rs b/hyperstack-macros/src/stream_spec/proto_struct.rs index 6302e719..eb702af6 100644 --- a/hyperstack-macros/src/stream_spec/proto_struct.rs +++ b/hyperstack-macros/src/stream_spec/proto_struct.rs @@ -148,6 +148,8 @@ pub fn process_struct_with_context( extract: resolve_attr.extract, target_field_name: resolve_attr.target_field_name, strategy: resolve_attr.strategy, + condition: resolve_attr.condition, + schedule_at: resolve_attr.schedule_at, }); } } @@ -449,7 +451,26 @@ pub fn process_struct_with_context( hyperstack::runtime::hyperstack_interpreter::ast::ResolverType::Token }, crate::ast::ResolverType::Url(config) => { - let url_path = &config.url_path; + let url_source_code = match &config.url_source { + crate::ast::UrlSource::FieldPath(path) => { + quote! { + hyperstack::runtime::hyperstack_interpreter::ast::UrlSource::FieldPath(#path.to_string()) + } + } + crate::ast::UrlSource::Template(parts) => { + let parts_code: Vec<_> = parts.iter().map(|part| match part { + crate::ast::UrlTemplatePart::Literal(s) => quote! { + hyperstack::runtime::hyperstack_interpreter::ast::UrlTemplatePart::Literal(#s.to_string()) + }, + crate::ast::UrlTemplatePart::FieldRef(f) => quote! { + hyperstack::runtime::hyperstack_interpreter::ast::UrlTemplatePart::FieldRef(#f.to_string()) + }, + }).collect(); + quote! { + hyperstack::runtime::hyperstack_interpreter::ast::UrlSource::Template(vec![#(#parts_code),*]) + } + } + }; let method_code = match config.method { crate::ast::HttpMethod::Get => quote! { hyperstack::runtime::hyperstack_interpreter::ast::HttpMethod::Get @@ -465,7 +486,7 @@ pub fn process_struct_with_context( quote! { hyperstack::runtime::hyperstack_interpreter::ast::ResolverType::Url( hyperstack::runtime::hyperstack_interpreter::ast::UrlResolverConfig { - url_path: #url_path.to_string(), + url_source: #url_source_code, method: #method_code, extract_path: #extract_path_code, } @@ -525,6 +546,8 @@ pub fn process_struct_with_context( extracts: vec![ #(#extracts_code),* ], + condition: None, + schedule_at: None, } } }) diff --git a/hyperstack-macros/src/stream_spec/sections.rs b/hyperstack-macros/src/stream_spec/sections.rs index b301ff29..5bed3ac5 100644 --- a/hyperstack-macros/src/stream_spec/sections.rs +++ b/hyperstack-macros/src/stream_spec/sections.rs @@ -633,16 +633,7 @@ pub fn process_nested_struct( } else if let Ok(Some(resolve_attr)) = parse::parse_resolve_attribute(attr, &field_name.to_string()) { - // Determine resolver type: URL resolver if url is present, otherwise Token resolver - let qualified_url = resolve_attr.url.as_deref().map(|url_path_raw| { - if url_path_raw.contains('.') { - url_path_raw.to_string() - } else { - format!("{}.{}", section_name, url_path_raw) - } - }); - - let resolver = if let Some(ref url_path) = qualified_url { + let resolver = if resolve_attr.url.is_some() { let method = resolve_attr.method.as_deref().map(|m| { match m.to_lowercase().as_str() { "post" => crate::ast::HttpMethod::Post, @@ -650,8 +641,22 @@ pub fn process_nested_struct( } }).unwrap_or(crate::ast::HttpMethod::Get); + let url_source = if resolve_attr.url_is_template { + crate::ast::UrlSource::Template( + super::entity::parse_url_template(resolve_attr.url.as_deref().unwrap()) + ) + } else { + let url_path_raw = resolve_attr.url.as_deref().unwrap(); + let qualified = if url_path_raw.contains('.') { + url_path_raw.to_string() + } else { + format!("{}.{}", section_name, url_path_raw) + }; + crate::ast::UrlSource::FieldPath(qualified) + }; + crate::ast::ResolverType::Url(crate::ast::UrlResolverConfig { - url_path: url_path.clone(), + url_source, method, extract_path: resolve_attr.extract.clone(), }) @@ -668,7 +673,18 @@ pub fn process_nested_struct( target_field_name = format!("{}.{}", section_name, target_field_name); } - let from = qualified_url.or(resolve_attr.from); + let from = if resolve_attr.url_is_template { + None + } else { + let qualified_url = resolve_attr.url.as_deref().map(|url_path_raw| { + if url_path_raw.contains('.') { + url_path_raw.to_string() + } else { + format!("{}.{}", section_name, url_path_raw) + } + }); + qualified_url.or(resolve_attr.from) + }; resolve_specs.push(parse::ResolveSpec { resolver, @@ -677,6 +693,8 @@ pub fn process_nested_struct( extract: resolve_attr.extract, target_field_name, strategy: resolve_attr.strategy, + condition: resolve_attr.condition, + schedule_at: resolve_attr.schedule_at, }); } } diff --git a/interpreter/src/ast.rs b/interpreter/src/ast.rs index 4432658b..d3695baa 100644 --- a/interpreter/src/ast.rs +++ b/interpreter/src/ast.rs @@ -400,14 +400,23 @@ pub enum HttpMethod { Post, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum UrlTemplatePart { + Literal(String), + FieldRef(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum UrlSource { + FieldPath(String), + Template(Vec), +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct UrlResolverConfig { - /// Field path to get the URL from (e.g., "info.uri") - pub url_path: String, - /// HTTP method to use (default: GET) + pub url_source: UrlSource, #[serde(default)] pub method: HttpMethod, - /// JSON path to extract from response (None = full payload) #[serde(default, skip_serializing_if = "Option::is_none")] pub extract_path: Option, } @@ -428,6 +437,13 @@ pub enum ResolveStrategy { LastWrite, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResolverCondition { + pub field_path: String, + pub op: ComparisonOp, + pub value: Value, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResolverSpec { pub resolver: ResolverType, @@ -438,6 +454,10 @@ pub struct ResolverSpec { #[serde(default)] pub strategy: ResolveStrategy, pub extracts: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub schedule_at: Option, } /// AST for computed field expressions diff --git a/interpreter/src/compiler.rs b/interpreter/src/compiler.rs index d6390762..2a38fdc8 100644 --- a/interpreter/src/compiler.rs +++ b/interpreter/src/compiler.rs @@ -202,8 +202,11 @@ pub enum OpCode { resolver: ResolverType, input_path: Option, input_value: Option, + url_template: Option>, strategy: ResolveStrategy, extracts: Vec, + condition: Option, + schedule_at: Option, state: Register, key: Register, }, @@ -713,14 +716,25 @@ impl TypedCompiler { let mut ops = Vec::new(); for resolver_spec in &self.spec.resolver_specs { + let url_template = match &resolver_spec.resolver { + ResolverType::Url(config) => match &config.url_source { + UrlSource::Template(parts) => Some(parts.clone()), + _ => None, + }, + _ => None, + }; + ops.push(OpCode::QueueResolver { state_id: self.state_id, entity_name: self.entity_name.clone(), resolver: resolver_spec.resolver.clone(), input_path: resolver_spec.input_path.clone(), input_value: resolver_spec.input_value.clone(), + url_template, strategy: resolver_spec.strategy.clone(), extracts: resolver_spec.extracts.clone(), + condition: resolver_spec.condition.clone(), + schedule_at: resolver_spec.schedule_at.clone(), state: state_reg, key: key_reg, }); diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index f61125b2..4e9ec050 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -45,8 +45,8 @@ pub use resolvers::{ pub use typescript::{write_typescript_to_file, TypeScriptCompiler, TypeScriptConfig}; pub use vm::{ CapacityWarning, CleanupResult, DirtyTracker, FieldChange, PendingAccountUpdate, - PendingQueueStats, QueuedAccountUpdate, ResolverRequest, StateTableConfig, UpdateContext, - VmMemoryStats, + PendingQueueStats, QueuedAccountUpdate, ResolverRequest, ScheduledCallback, StateTableConfig, + UpdateContext, VmMemoryStats, }; // Re-export macros for convenient use diff --git a/interpreter/src/vm.rs b/interpreter/src/vm.rs index 0b352265..ef39ba6b 100644 --- a/interpreter/src/vm.rs +++ b/interpreter/src/vm.rs @@ -1,5 +1,5 @@ use crate::ast::{ - BinaryOp, ComparisonOp, ComputedExpr, ComputedFieldSpec, FieldPath, ResolveStrategy, + self, BinaryOp, ComparisonOp, ComputedExpr, ComputedFieldSpec, FieldPath, ResolveStrategy, ResolverExtractSpec, ResolverType, Transformation, }; use crate::compiler::{MultiEntityBytecode, OpCode}; @@ -321,6 +321,7 @@ pub struct VmContext { last_lookup_index_miss: Option, last_pda_registered: Option, last_lookup_index_keys: Vec, + scheduled_callbacks: Vec<(u64, ScheduledCallback)>, } #[derive(Debug)] @@ -379,7 +380,19 @@ fn value_to_cache_key(value: &Value) -> String { fn resolver_type_key(resolver: &ResolverType) -> String { match resolver { ResolverType::Token => "token".to_string(), - ResolverType::Url(config) => format!("url:{}", config.url_path), + ResolverType::Url(config) => match &config.url_source { + ast::UrlSource::FieldPath(path) => format!("url:{}", path), + ast::UrlSource::Template(parts) => { + let key: String = parts + .iter() + .map(|p| match p { + ast::UrlTemplatePart::Literal(s) => s.clone(), + ast::UrlTemplatePart::FieldRef(f) => format!("{{{}}}", f), + }) + .collect(); + format!("url:{}", key) + } + }, } } @@ -608,6 +621,20 @@ pub struct PendingResolverEntry { pub queued_at: i64, } +#[derive(Debug, Clone)] +pub struct ScheduledCallback { + pub state_id: u32, + pub entity_name: String, + pub primary_key: Value, + pub resolver: ResolverType, + pub url_template: Option>, + pub condition: Option, + pub strategy: ResolveStrategy, + pub extracts: Vec, + pub registered_at_slot: u64, + pub retry_count: u32, +} + impl PendingResolverEntry { fn add_target(&mut self, target: ResolverTarget) { if let Some(existing) = self.targets.iter_mut().find(|t| { @@ -906,6 +933,7 @@ impl VmContext { last_lookup_index_miss: None, last_pda_registered: None, last_lookup_index_keys: Vec::new(), + scheduled_callbacks: Vec::new(), }; vm.states.insert( 0, @@ -956,6 +984,7 @@ impl VmContext { last_lookup_index_miss: None, last_pda_registered: None, last_lookup_index_keys: Vec::new(), + scheduled_callbacks: Vec::new(), }; vm.states.insert( 0, @@ -986,6 +1015,10 @@ impl VmContext { self.resolver_requests.drain(..).collect() } + pub fn take_scheduled_callbacks(&mut self) -> Vec<(u64, ScheduledCallback)> { + std::mem::take(&mut self.scheduled_callbacks) + } + pub fn restore_resolver_requests(&mut self, requests: Vec) { if requests.is_empty() { return; @@ -2539,13 +2572,101 @@ impl VmContext { resolver, input_path, input_value, + url_template, strategy, extracts, + condition, + schedule_at, state, key, } => { let actual_state_id = override_state_id; - let resolved_input = if let Some(value) = input_value { + + // Evaluate condition if present + if let Some(cond) = condition { + let field_val = Self::get_value_at_path( + &self.registers[*state], + &cond.field_path, + ) + .unwrap_or(Value::Null); + if !self.evaluate_comparison(&field_val, &cond.op, &cond.value)? { + pc += 1; + continue; + } + } + + // Check schedule_at: defer if target slot is in the future + if let Some(schedule_path) = schedule_at { + if let Some(target_val) = Self::get_value_at_path( + &self.registers[*state], + schedule_path, + ) { + if let Some(target_slot) = target_val.as_u64() { + let current_slot = self + .current_context + .as_ref() + .and_then(|ctx| ctx.slot) + .unwrap_or(0); + if current_slot < target_slot { + let key_value = &self.registers[*key]; + if !key_value.is_null() { + self.scheduled_callbacks.push(( + target_slot, + ScheduledCallback { + state_id: actual_state_id, + entity_name: entity_name.clone(), + primary_key: key_value.clone(), + resolver: resolver.clone(), + url_template: url_template.clone(), + condition: condition.clone(), + strategy: strategy.clone(), + extracts: extracts.clone(), + registered_at_slot: current_slot, + retry_count: 0, + }, + )); + } + pc += 1; + continue; + } + } + } + } + + // Build resolver input from template, literal value, or field path + let resolved_input = if let Some(template) = url_template { + let mut url = String::new(); + let mut all_resolved = true; + for part in template { + match part { + ast::UrlTemplatePart::Literal(s) => url.push_str(s), + ast::UrlTemplatePart::FieldRef(path) => { + match Self::get_value_at_path( + &self.registers[*state], + path, + ) { + Some(val) if !val.is_null() => { + match val.as_str() { + Some(s) => url.push_str(s), + None => url.push_str( + &val.to_string().trim_matches('"').to_string(), + ), + } + } + _ => { + all_resolved = false; + break; + } + } + } + } + } + if all_resolved { + Some(Value::String(url)) + } else { + None + } + } else if let Some(value) = input_value { Some(value.clone()) } else if let Some(path) = input_path.as_ref() { Self::get_value_at_path(&self.registers[*state], path) From bad2dc9e66727f0d11ade7e2fca77fb47c3f6d6e Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:27:48 +0000 Subject: [PATCH 02/15] feat: wire condition and schedule_at codegen in proto_struct Generate actual ResolverCondition and schedule_at values in the proto_struct code path instead of hardcoded None, completing the condition parameter support across both AST-writer and codegen paths. Made-with: Cursor --- .../src/stream_spec/ast_writer.rs | 4 ++ .../src/stream_spec/proto_struct.rs | 42 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/hyperstack-macros/src/stream_spec/ast_writer.rs b/hyperstack-macros/src/stream_spec/ast_writer.rs index 3db7f001..db08d5f4 100644 --- a/hyperstack-macros/src/stream_spec/ast_writer.rs +++ b/hyperstack-macros/src/stream_spec/ast_writer.rs @@ -238,6 +238,10 @@ fn parse_resolve_strategy(strategy: &str) -> ResolveStrategy { } } +pub fn parse_resolver_condition_from_str(s: &str) -> ResolverCondition { + parse_resolver_condition(s) +} + fn parse_resolver_condition(s: &str) -> ResolverCondition { let operators = ["==", "!=", ">=", "<=", ">", "<"]; for op_str in &operators { diff --git a/hyperstack-macros/src/stream_spec/proto_struct.rs b/hyperstack-macros/src/stream_spec/proto_struct.rs index eb702af6..3869fe40 100644 --- a/hyperstack-macros/src/stream_spec/proto_struct.rs +++ b/hyperstack-macros/src/stream_spec/proto_struct.rs @@ -537,6 +537,44 @@ pub fn process_struct_with_context( }) .collect(); + let condition_code = match specs.first().and_then(|s| s.condition.as_deref()) { + Some(cond_str) => { + let parsed = super::ast_writer::parse_resolver_condition_from_str(cond_str); + let field_path = &parsed.field_path; + let op_code = match parsed.op { + crate::ast::ComparisonOp::Equal => quote! { hyperstack::runtime::hyperstack_interpreter::ast::ComparisonOp::Equal }, + crate::ast::ComparisonOp::NotEqual => quote! { hyperstack::runtime::hyperstack_interpreter::ast::ComparisonOp::NotEqual }, + crate::ast::ComparisonOp::GreaterThan => quote! { hyperstack::runtime::hyperstack_interpreter::ast::ComparisonOp::GreaterThan }, + crate::ast::ComparisonOp::LessThan => quote! { hyperstack::runtime::hyperstack_interpreter::ast::ComparisonOp::LessThan }, + crate::ast::ComparisonOp::GreaterThanOrEqual => quote! { hyperstack::runtime::hyperstack_interpreter::ast::ComparisonOp::GreaterThanOrEqual }, + crate::ast::ComparisonOp::LessThanOrEqual => quote! { hyperstack::runtime::hyperstack_interpreter::ast::ComparisonOp::LessThanOrEqual }, + }; + let val_code = match &parsed.value { + serde_json::Value::Null => quote! { hyperstack::runtime::serde_json::Value::Null }, + serde_json::Value::Bool(b) => quote! { hyperstack::runtime::serde_json::Value::Bool(#b) }, + serde_json::Value::Number(n) => { + let n_str = n.to_string(); + quote! { hyperstack::runtime::serde_json::json!(#n_str.parse::().unwrap()) } + } + serde_json::Value::String(s) => quote! { hyperstack::runtime::serde_json::Value::String(#s.to_string()) }, + _ => quote! { hyperstack::runtime::serde_json::Value::Null }, + }; + quote! { + Some(hyperstack::runtime::hyperstack_interpreter::ast::ResolverCondition { + field_path: #field_path.to_string(), + op: #op_code, + value: #val_code, + }) + } + } + None => quote! { None }, + }; + + let schedule_at_code = match specs.first().and_then(|s| s.schedule_at.as_ref()) { + Some(path) => quote! { Some(#path.to_string()) }, + None => quote! { None }, + }; + quote! { hyperstack::runtime::hyperstack_interpreter::ast::ResolverSpec { resolver: #resolver_code, @@ -546,8 +584,8 @@ pub fn process_struct_with_context( extracts: vec![ #(#extracts_code),* ], - condition: None, - schedule_at: None, + condition: #condition_code, + schedule_at: #schedule_at_code, } } }) From 1c1e4e64dc06c2cea7f2723780f020263e7a3c9d Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:31:37 +0000 Subject: [PATCH 03/15] feat: add SlotScheduler module and read-only state accessor New scheduler.rs with BTreeMap-based SlotScheduler for registering, deduplicating, and dispatching slot-triggered resolver callbacks with retry support. Adds get_entity_state() to VmContext for read-only state access needed by the scheduler background task. Made-with: Cursor --- interpreter/src/lib.rs | 1 + interpreter/src/scheduler.rs | 123 +++++++++++++++++++++++++++++++++++ interpreter/src/vm.rs | 4 ++ 3 files changed, 128 insertions(+) create mode 100644 interpreter/src/scheduler.rs diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index 4e9ec050..627777f9 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -32,6 +32,7 @@ pub mod metrics_context; pub mod proto_router; pub mod resolvers; pub mod rust; +pub mod scheduler; pub mod spec_trait; pub mod typescript; pub mod vm; diff --git a/interpreter/src/scheduler.rs b/interpreter/src/scheduler.rs new file mode 100644 index 00000000..b630e79f --- /dev/null +++ b/interpreter/src/scheduler.rs @@ -0,0 +1,123 @@ +use crate::ast::{ComparisonOp, ResolverCondition, UrlTemplatePart}; +use crate::vm::ScheduledCallback; +use serde_json::Value; +use std::collections::{BTreeMap, HashSet}; + +const MAX_RETRIES: u32 = 100; + +pub struct SlotScheduler { + callbacks: BTreeMap>, + registered: HashSet<(String, String, String)>, +} + +impl SlotScheduler { + pub fn new() -> Self { + Self { + callbacks: BTreeMap::new(), + registered: HashSet::new(), + } + } + + pub fn register(&mut self, target_slot: u64, callback: ScheduledCallback) { + let dedup_key = Self::dedup_key(&callback); + if self.registered.contains(&dedup_key) { + return; + } + self.registered.insert(dedup_key); + self.callbacks + .entry(target_slot) + .or_default() + .push(callback); + } + + pub fn take_due(&mut self, current_slot: u64) -> Vec { + let future = self.callbacks.split_off(&(current_slot + 1)); + let due = std::mem::replace(&mut self.callbacks, future); + + let mut result = Vec::new(); + for (_slot, callbacks) in due { + for cb in callbacks { + let dedup_key = Self::dedup_key(&cb); + self.registered.remove(&dedup_key); + result.push(cb); + } + } + result + } + + pub fn re_register(&mut self, callback: ScheduledCallback, next_slot: u64) { + let dedup_key = Self::dedup_key(&callback); + self.registered.insert(dedup_key); + self.callbacks + .entry(next_slot) + .or_default() + .push(callback); + } + + pub fn pending_count(&self) -> usize { + self.callbacks.values().map(|v| v.len()).sum() + } + + fn dedup_key(cb: &ScheduledCallback) -> (String, String, String) { + let resolver_key = format!("{:?}", cb.resolver); + let pk_key = cb.primary_key.to_string(); + (cb.entity_name.clone(), pk_key, resolver_key) + } +} + +pub fn evaluate_condition(condition: &ResolverCondition, state: &Value) -> bool { + let field_val = get_value_at_path(state, &condition.field_path).unwrap_or(Value::Null); + evaluate_comparison(&field_val, &condition.op, &condition.value) +} + +pub fn build_url_from_template(template: &[UrlTemplatePart], state: &Value) -> Option { + let mut url = String::new(); + for part in template { + match part { + UrlTemplatePart::Literal(s) => url.push_str(s), + UrlTemplatePart::FieldRef(path) => { + let val = get_value_at_path(state, path)?; + if val.is_null() { + return None; + } + match val.as_str() { + Some(s) => url.push_str(s), + None => url.push_str(&val.to_string().trim_matches('"').to_string()), + } + } + } + } + Some(url) +} + +pub fn get_value_at_path(value: &Value, path: &str) -> Option { + let mut current = value; + for segment in path.split('.') { + current = current.get(segment)?; + } + Some(current.clone()) +} + +fn evaluate_comparison(field_value: &Value, op: &ComparisonOp, condition_value: &Value) -> bool { + match op { + ComparisonOp::Equal => field_value == condition_value, + ComparisonOp::NotEqual => field_value != condition_value, + ComparisonOp::GreaterThan => compare_numeric(field_value, condition_value, |a, b| a > b), + ComparisonOp::GreaterThanOrEqual => { + compare_numeric(field_value, condition_value, |a, b| a >= b) + } + ComparisonOp::LessThan => compare_numeric(field_value, condition_value, |a, b| a < b), + ComparisonOp::LessThanOrEqual => { + compare_numeric(field_value, condition_value, |a, b| a <= b) + } + } +} + +fn compare_numeric(a: &Value, b: &Value, cmp: fn(f64, f64) -> bool) -> bool { + match (a.as_f64(), b.as_f64()) { + (Some(a), Some(b)) => cmp(a, b), + _ => false, + } +} + +pub const MAX_SCHEDULER_RETRIES: u32 = MAX_RETRIES; diff --git a/interpreter/src/vm.rs b/interpreter/src/vm.rs index ef39ba6b..f24f630f 100644 --- a/interpreter/src/vm.rs +++ b/interpreter/src/vm.rs @@ -1019,6 +1019,10 @@ impl VmContext { std::mem::take(&mut self.scheduled_callbacks) } + pub fn get_entity_state(&self, state_id: u32, key: &Value) -> Option { + self.states.get(&state_id)?.get_and_touch(key) + } + pub fn restore_resolver_requests(&mut self, requests: Vec) { if requests.is_empty() { return; From e673257d541714f5a27493a55f3ae7ecf4413075 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:47:30 +0000 Subject: [PATCH 04/15] feat: integrate SlotScheduler into VmHandler with background polling Wire SlotScheduler into both single-entity and multi-entity VmHandler codegen. After each handler execution, scheduled callbacks are collected and registered. A background task polls every 400ms, evaluates conditions, retries up to 100 slots, builds URLs from templates, and fires resolver requests through the standard resolve_url_batch pipeline. Made-with: Cursor --- .../src/codegen/vixen_runtime.rs | 280 +++++++++++++++++- interpreter/src/lib.rs | 4 +- interpreter/src/vm.rs | 2 +- 3 files changed, 275 insertions(+), 11 deletions(-) diff --git a/hyperstack-macros/src/codegen/vixen_runtime.rs b/hyperstack-macros/src/codegen/vixen_runtime.rs index b2f2fcca..8e142ec3 100644 --- a/hyperstack-macros/src/codegen/vixen_runtime.rs +++ b/hyperstack-macros/src/codegen/vixen_runtime.rs @@ -260,6 +260,7 @@ pub fn generate_vm_handler( slot_tracker: hyperstack::runtime::hyperstack_server::SlotTracker, resolver_client: Option>, url_resolver_client: std::sync::Arc, + slot_scheduler: std::sync::Arc>, } impl std::fmt::Debug for VmHandler { @@ -280,6 +281,7 @@ pub fn generate_vm_handler( slot_tracker: hyperstack::runtime::hyperstack_server::SlotTracker, resolver_client: Option>, url_resolver_client: std::sync::Arc, + slot_scheduler: std::sync::Arc>, ) -> Self { Self { vm, @@ -289,6 +291,7 @@ pub fn generate_vm_handler( slot_tracker, resolver_client, url_resolver_client, + slot_scheduler, } } @@ -543,7 +546,7 @@ pub fn generate_vm_handler( } } - let (mutations_result, resolver_requests) = { + let (mutations_result, resolver_requests, scheduled_callbacks) = { let mut vm = self.vm.lock().unwrap_or_else(|e| e.into_inner()); let context = hyperstack::runtime::hyperstack_interpreter::UpdateContext::new_account(slot, signature.clone(), write_version); @@ -557,9 +560,22 @@ pub fn generate_vm_handler( Vec::new() }; - (result, requests) + let scheduled = if result.is_ok() { + vm.take_scheduled_callbacks() + } else { + Vec::new() + }; + + (result, requests, scheduled) }; + if !scheduled_callbacks.is_empty() { + let mut scheduler = self.slot_scheduler.lock().unwrap_or_else(|e| e.into_inner()); + for (target_slot, callback) in scheduled_callbacks { + scheduler.register(target_slot, callback); + } + } + let resolver_mutations = if mutations_result.is_ok() { self.resolve_and_apply_resolvers(resolver_requests).await } else { @@ -632,7 +648,7 @@ pub fn generate_vm_handler( let event_value = value.to_value_with_accounts(static_keys_vec); let bytecode = self.bytecode.clone(); - let (mutations_result, resolver_requests) = { + let (mutations_result, resolver_requests, scheduled_callbacks) = { let mut vm = self.vm.lock().unwrap_or_else(|e| e.into_inner()); let context = hyperstack::runtime::hyperstack_interpreter::UpdateContext::new_instruction(slot, signature.clone(), txn_index); @@ -729,9 +745,22 @@ pub fn generate_vm_handler( Vec::new() }; - (result, requests) + let scheduled = if result.is_ok() { + vm.take_scheduled_callbacks() + } else { + Vec::new() + }; + + (result, requests, scheduled) }; + if !scheduled_callbacks.is_empty() { + let mut scheduler = self.slot_scheduler.lock().unwrap_or_else(|e| e.into_inner()); + for (target_slot, callback) in scheduled_callbacks { + scheduler.register(target_slot, callback); + } + } + let resolver_mutations = if mutations_result.is_ok() { self.resolve_and_apply_resolvers(resolver_requests).await } else { @@ -880,6 +909,7 @@ pub fn generate_spec_function( let url_resolver_client = Arc::new(hyperstack::runtime::hyperstack_interpreter::resolvers::UrlResolverClient::new()); let slot_tracker = hyperstack::runtime::hyperstack_server::SlotTracker::new(); + let slot_scheduler = Arc::new(Mutex::new(hyperstack::runtime::hyperstack_interpreter::scheduler::SlotScheduler::new())); let mut attempt = 0u32; let mut backoff = reconnection_config.initial_delay; @@ -890,6 +920,107 @@ pub fn generate_spec_function( let vm = Arc::new(Mutex::new(hyperstack::runtime::hyperstack_interpreter::vm::VmContext::new())); let bytecode_arc = Arc::new(bytecode); + // Spawn slot scheduler background task + { + let scheduler = slot_scheduler.clone(); + let vm = vm.clone(); + let bytecode = bytecode_arc.clone(); + let url_client = url_resolver_client.clone(); + let slot_tracker = slot_tracker.clone(); + let mutations_tx = mutations_tx.clone(); + + hyperstack::runtime::tokio::spawn(async move { + loop { + let current_slot = slot_tracker.get(); + if current_slot == 0 { + hyperstack::runtime::tokio::time::sleep(std::time::Duration::from_millis(400)).await; + continue; + } + + let due = { + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.take_due(current_slot) + }; + + for mut callback in due { + let state = { + let vm = vm.lock().unwrap_or_else(|e| e.into_inner()); + vm.get_entity_state(callback.state_id, &callback.primary_key) + }; + + let state = match state { + Some(s) => s, + None => continue, + }; + + if let Some(ref condition) = callback.condition { + if !hyperstack::runtime::hyperstack_interpreter::scheduler::evaluate_condition(condition, &state) { + callback.retry_count += 1; + if callback.retry_count >= hyperstack::runtime::hyperstack_interpreter::scheduler::MAX_SCHEDULER_RETRIES { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + retries = callback.retry_count, + "Scheduled resolver discarded after max retries" + ); + } else { + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } + continue; + } + } + + let url = if let Some(ref template) = callback.url_template { + match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { + Some(u) => u, + None => continue, + } + } else { + continue; + }; + + let cache_key = format!("scheduled:{}:{}:{}", callback.entity_name, callback.primary_key, url); + + let requests = { + let mut vm = vm.lock().unwrap_or_else(|e| e.into_inner()); + let target = hyperstack::runtime::hyperstack_interpreter::vm::ResolverTarget { + state_id: callback.state_id, + entity_name: callback.entity_name.clone(), + primary_key: callback.primary_key.clone(), + extracts: callback.extracts.clone(), + }; + vm.enqueue_resolver_request( + cache_key.clone(), + callback.resolver.clone(), + hyperstack::runtime::serde_json::Value::String(url.clone()), + target, + ); + vm.take_resolver_requests() + }; + + let url_mutations = hyperstack::runtime::hyperstack_interpreter::resolvers::resolve_url_batch( + &vm, + bytecode.as_ref(), + &url_client, + requests, + ).await; + + if !url_mutations.is_empty() { + let slot_context = hyperstack::runtime::hyperstack_server::SlotContext::new(current_slot, 0); + let batch = hyperstack::runtime::hyperstack_server::MutationBatch::with_slot_context( + hyperstack::runtime::smallvec::SmallVec::from_vec(url_mutations), + slot_context, + ); + let _ = mutations_tx.send(batch).await; + } + } + + hyperstack::runtime::tokio::time::sleep(std::time::Duration::from_millis(400)).await; + } + }); + } + loop { let from_slot = { let last = slot_tracker.get(); @@ -921,6 +1052,7 @@ pub fn generate_spec_function( slot_tracker.clone(), resolver_client.clone(), url_resolver_client.clone(), + slot_scheduler.clone(), ); let account_parser = parsers::AccountParser; @@ -1190,6 +1322,7 @@ pub fn generate_vm_handler_struct() -> TokenStream { slot_tracker: hyperstack::runtime::hyperstack_server::SlotTracker, resolver_client: Option>, url_resolver_client: std::sync::Arc, + slot_scheduler: std::sync::Arc>, } impl std::fmt::Debug for VmHandler { @@ -1210,6 +1343,7 @@ pub fn generate_vm_handler_struct() -> TokenStream { slot_tracker: hyperstack::runtime::hyperstack_server::SlotTracker, resolver_client: Option>, url_resolver_client: std::sync::Arc, + slot_scheduler: std::sync::Arc>, ) -> Self { Self { vm, @@ -1219,6 +1353,7 @@ pub fn generate_vm_handler_struct() -> TokenStream { slot_tracker, resolver_client, url_resolver_client, + slot_scheduler, } } @@ -1484,7 +1619,7 @@ pub fn generate_account_handler_impl( } } - let (mutations_result, resolver_requests) = { + let (mutations_result, resolver_requests, scheduled_callbacks) = { let mut vm = self.vm.lock().unwrap_or_else(|e| e.into_inner()); let context = hyperstack::runtime::hyperstack_interpreter::UpdateContext::new_account(slot, signature.clone(), write_version); @@ -1498,9 +1633,22 @@ pub fn generate_account_handler_impl( Vec::new() }; - (result, requests) + let scheduled = if result.is_ok() { + vm.take_scheduled_callbacks() + } else { + Vec::new() + }; + + (result, requests, scheduled) }; + if !scheduled_callbacks.is_empty() { + let mut scheduler = self.slot_scheduler.lock().unwrap_or_else(|e| e.into_inner()); + for (target_slot, callback) in scheduled_callbacks { + scheduler.register(target_slot, callback); + } + } + let resolver_mutations = if mutations_result.is_ok() { self.resolve_and_apply_resolvers(resolver_requests).await } else { @@ -1578,7 +1726,7 @@ pub fn generate_instruction_handler_impl( let event_value = value.to_value_with_accounts(static_keys_vec); let bytecode = self.bytecode.clone(); - let (mutations_result, resolver_requests) = { + let (mutations_result, resolver_requests, scheduled_callbacks) = { let mut vm = self.vm.lock().unwrap_or_else(|e| e.into_inner()); let context = hyperstack::runtime::hyperstack_interpreter::UpdateContext::new_instruction(slot, signature.clone(), txn_index); @@ -1672,9 +1820,22 @@ pub fn generate_instruction_handler_impl( Vec::new() }; - (result, requests) + let scheduled = if result.is_ok() { + vm.take_scheduled_callbacks() + } else { + Vec::new() + }; + + (result, requests, scheduled) }; + if !scheduled_callbacks.is_empty() { + let mut scheduler = self.slot_scheduler.lock().unwrap_or_else(|e| e.into_inner()); + for (target_slot, callback) in scheduled_callbacks { + scheduler.register(target_slot, callback); + } + } + let resolver_mutations = if mutations_result.is_ok() { self.resolve_and_apply_resolvers(resolver_requests).await } else { @@ -1878,6 +2039,7 @@ pub fn generate_multi_pipeline_spec_function( let url_resolver_client = Arc::new(hyperstack::runtime::hyperstack_interpreter::resolvers::UrlResolverClient::new()); let slot_tracker = hyperstack::runtime::hyperstack_server::SlotTracker::new(); + let slot_scheduler = Arc::new(Mutex::new(hyperstack::runtime::hyperstack_interpreter::scheduler::SlotScheduler::new())); let mut attempt = 0u32; let mut backoff = reconnection_config.initial_delay; @@ -1888,6 +2050,107 @@ pub fn generate_multi_pipeline_spec_function( let vm = Arc::new(Mutex::new(hyperstack::runtime::hyperstack_interpreter::vm::VmContext::new())); let bytecode_arc = Arc::new(bytecode); + // Spawn slot scheduler background task + { + let scheduler = slot_scheduler.clone(); + let vm = vm.clone(); + let bytecode = bytecode_arc.clone(); + let url_client = url_resolver_client.clone(); + let slot_tracker = slot_tracker.clone(); + let mutations_tx = mutations_tx.clone(); + + hyperstack::runtime::tokio::spawn(async move { + loop { + let current_slot = slot_tracker.get(); + if current_slot == 0 { + hyperstack::runtime::tokio::time::sleep(std::time::Duration::from_millis(400)).await; + continue; + } + + let due = { + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.take_due(current_slot) + }; + + for mut callback in due { + let state = { + let vm = vm.lock().unwrap_or_else(|e| e.into_inner()); + vm.get_entity_state(callback.state_id, &callback.primary_key) + }; + + let state = match state { + Some(s) => s, + None => continue, + }; + + if let Some(ref condition) = callback.condition { + if !hyperstack::runtime::hyperstack_interpreter::scheduler::evaluate_condition(condition, &state) { + callback.retry_count += 1; + if callback.retry_count >= hyperstack::runtime::hyperstack_interpreter::scheduler::MAX_SCHEDULER_RETRIES { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + retries = callback.retry_count, + "Scheduled resolver discarded after max retries" + ); + } else { + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } + continue; + } + } + + let url = if let Some(ref template) = callback.url_template { + match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { + Some(u) => u, + None => continue, + } + } else { + continue; + }; + + let cache_key = format!("scheduled:{}:{}:{}", callback.entity_name, callback.primary_key, url); + + let requests = { + let mut vm = vm.lock().unwrap_or_else(|e| e.into_inner()); + let target = hyperstack::runtime::hyperstack_interpreter::vm::ResolverTarget { + state_id: callback.state_id, + entity_name: callback.entity_name.clone(), + primary_key: callback.primary_key.clone(), + extracts: callback.extracts.clone(), + }; + vm.enqueue_resolver_request( + cache_key.clone(), + callback.resolver.clone(), + hyperstack::runtime::serde_json::Value::String(url.clone()), + target, + ); + vm.take_resolver_requests() + }; + + let url_mutations = hyperstack::runtime::hyperstack_interpreter::resolvers::resolve_url_batch( + &vm, + bytecode.as_ref(), + &url_client, + requests, + ).await; + + if !url_mutations.is_empty() { + let slot_context = hyperstack::runtime::hyperstack_server::SlotContext::new(current_slot, 0); + let batch = hyperstack::runtime::hyperstack_server::MutationBatch::with_slot_context( + hyperstack::runtime::smallvec::SmallVec::from_vec(url_mutations), + slot_context, + ); + let _ = mutations_tx.send(batch).await; + } + } + + hyperstack::runtime::tokio::time::sleep(std::time::Duration::from_millis(400)).await; + } + }); + } + loop { let from_slot = { let last = slot_tracker.get(); @@ -1919,6 +2182,7 @@ pub fn generate_multi_pipeline_spec_function( slot_tracker.clone(), resolver_client.clone(), url_resolver_client.clone(), + slot_scheduler.clone(), ); if attempt == 0 { diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index 627777f9..ca20233c 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -46,8 +46,8 @@ pub use resolvers::{ pub use typescript::{write_typescript_to_file, TypeScriptCompiler, TypeScriptConfig}; pub use vm::{ CapacityWarning, CleanupResult, DirtyTracker, FieldChange, PendingAccountUpdate, - PendingQueueStats, QueuedAccountUpdate, ResolverRequest, ScheduledCallback, StateTableConfig, - UpdateContext, VmMemoryStats, + PendingQueueStats, QueuedAccountUpdate, ResolverRequest, ResolverTarget, ScheduledCallback, + StateTableConfig, UpdateContext, VmMemoryStats, }; // Re-export macros for convenient use diff --git a/interpreter/src/vm.rs b/interpreter/src/vm.rs index f24f630f..43702d17 100644 --- a/interpreter/src/vm.rs +++ b/interpreter/src/vm.rs @@ -1127,7 +1127,7 @@ impl VmContext { Ok(mutations) } - fn enqueue_resolver_request( + pub fn enqueue_resolver_request( &mut self, cache_key: String, resolver: ResolverType, From c70e1599456f89768282cb086a39cd79b14fc7b3 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:51:06 +0000 Subject: [PATCH 05/15] feat: add resolved_seed to Ore stack with scheduled URL resolver Use the new #[resolve] syntax to pre-fetch entropy seed from the Ore API when a round expires, conditioned on entropy_value not yet being revealed on-chain. Made-with: Cursor --- stacks/ore/src/stack.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/stacks/ore/src/stack.rs b/stacks/ore/src/stack.rs index ed44587e..708762c2 100644 --- a/stacks/ore/src/stack.rs +++ b/stacks/ore/src/stack.rs @@ -191,6 +191,15 @@ pub mod ore_stream { #[map(entropy_sdk::accounts::Var::__account_address, strategy = SetOnce)] pub entropy_var_address: Option, + + #[resolve( + url = "https://entropy-api.onrender.com/var/{entropy.entropy_var_address}/seed?samples={entropy.entropy_samples}", + extract = "seed", + schedule_at = state.expires_at, + condition = "entropy.entropy_value == null", + strategy = SetOnce + )] + pub resolved_seed: Option>, } // ======================================================================== From 35684c480e3288fd59333c32cf78cdc109ab6b48 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:31:47 +0000 Subject: [PATCH 06/15] chore: Update ore server cargo.lock & stack json --- examples/ore-server/Cargo.lock | 258 +++++++------------- stacks/ore/.hyperstack/OreStream.stack.json | 143 +++++++---- 2 files changed, 193 insertions(+), 208 deletions(-) diff --git a/examples/ore-server/Cargo.lock b/examples/ore-server/Cargo.lock index ba9fffcc..faec4158 100644 --- a/examples/ore-server/Cargo.lock +++ b/examples/ore-server/Cargo.lock @@ -726,21 +726,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -869,9 +854,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1110,6 +1097,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tower-service", + "webpki-roots 1.0.5", ] [[package]] @@ -1125,22 +1113,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.19" @@ -1160,16 +1132,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.1", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] name = "hyperstack" -version = "0.4.3" +version = "0.5.3" dependencies = [ "anyhow", "bs58", @@ -1192,10 +1162,11 @@ dependencies = [ [[package]] name = "hyperstack-interpreter" -version = "0.4.3" +version = "0.5.3" dependencies = [ "bs58", "dashmap", + "futures", "hex", "hyperstack-macros", "lru", @@ -1213,7 +1184,7 @@ dependencies = [ [[package]] name = "hyperstack-macros" -version = "0.4.3" +version = "0.5.3" dependencies = [ "bs58", "hex", @@ -1227,7 +1198,7 @@ dependencies = [ [[package]] name = "hyperstack-sdk" -version = "0.4.3" +version = "0.5.3" dependencies = [ "anyhow", "flate2", @@ -1244,7 +1215,7 @@ dependencies = [ [[package]] name = "hyperstack-server" -version = "0.4.3" +version = "0.5.3" dependencies = [ "anyhow", "base64 0.22.1", @@ -1510,6 +1481,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1570,23 +1547,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe 0.1.6", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1627,56 +1587,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ore-server" version = "0.1.0" @@ -2012,6 +1928,61 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.36", + "socket2 0.5.10", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.36", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.43" @@ -2182,29 +2153,26 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", - "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", "hyper-rustls 0.27.7", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls 0.23.36", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tokio-native-tls", + "tokio-rustls 0.26.4", "tower 0.5.3", "tower-http", "tower-service", @@ -2212,6 +2180,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 1.0.5", ] [[package]] @@ -2228,6 +2197,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.1.3" @@ -2288,10 +2263,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.0", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -2309,6 +2284,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -2381,19 +2357,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.5.1" @@ -2846,16 +2809,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -3399,12 +3352,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -3504,6 +3451,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -3556,35 +3513,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/stacks/ore/.hyperstack/OreStream.stack.json b/stacks/ore/.hyperstack/OreStream.stack.json index c367ff11..0f2eadd2 100644 --- a/stacks/ore/.hyperstack/OreStream.stack.json +++ b/stacks/ore/.hyperstack/OreStream.stack.json @@ -3350,6 +3350,16 @@ "inner_type": "String", "source_path": null, "resolved_type": null + }, + { + "field_name": "resolved_seed", + "rust_type_name": "Option < Vec < u8 > >", + "base_type": "Array", + "is_optional": true, + "is_array": true, + "inner_type": "Vec < u8 >", + "source_path": null, + "resolved_type": null } ], "is_nested_struct": false, @@ -3455,6 +3465,16 @@ "source_path": null, "resolved_type": null }, + "entropy.resolved_seed": { + "field_name": "resolved_seed", + "rust_type_name": "Option < Vec < u8 > >", + "base_type": "Array", + "is_optional": true, + "is_array": true, + "inner_type": "Vec < u8 >", + "source_path": null, + "resolved_type": null + }, "id.round_address": { "field_name": "round_address", "rust_type_name": "String", @@ -3687,15 +3707,6 @@ } }, "resolver_hooks": [ - { - "account_type": "entropy::VarState", - "strategy": { - "PdaReverseLookup": { - "lookup_name": "default_pda_lookup", - "queue_discriminators": [] - } - } - }, { "account_type": "ore::TreasuryState", "strategy": { @@ -3715,6 +3726,15 @@ ] } } + }, + { + "account_type": "entropy::VarState", + "strategy": { + "PdaReverseLookup": { + "lookup_name": "default_pda_lookup", + "queue_discriminators": [] + } + } } ], "instruction_hooks": [ @@ -3770,14 +3790,14 @@ "pda_field": { "segments": [ "accounts", - "entropyVar" + "treasury" ], "offsets": null }, "seed_field": { "segments": [ "accounts", - "round" + "roundNext" ], "offsets": null }, @@ -3789,14 +3809,14 @@ "pda_field": { "segments": [ "accounts", - "treasury" + "entropyVar" ], "offsets": null }, "seed_field": { "segments": [ "accounts", - "roundNext" + "round" ], "offsets": null }, @@ -3851,6 +3871,43 @@ "target_path": "ore_metadata" } ] + }, + { + "resolver": { + "url": { + "url_source": { + "Template": [ + { + "Literal": "https://entropy-api.onrender.com/var/" + }, + { + "FieldRef": "entropy.entropy_var_address" + }, + { + "Literal": "/seed?samples=" + }, + { + "FieldRef": "entropy.entropy_samples" + } + ] + }, + "method": "get", + "extract_path": "seed" + } + }, + "strategy": "SetOnce", + "extracts": [ + { + "target_path": "entropy.resolved_seed", + "source_path": "seed" + } + ], + "condition": { + "field_path": "entropy.entropy_value", + "op": "Equal", + "value": null + }, + "schedule_at": "state.expires_at" } ], "computed_fields": [ @@ -4556,7 +4613,7 @@ "result_type": "Option < f64 >" } ], - "content_hash": "fee0e768222fd6bab78adf26826ac1111fe5fd95dead39939b03b22e9e184eea", + "content_hash": "6656d3208291c8ac7d8fc757727ffd176be3a42fc66624e002623cc0ca840a16", "views": [ { "id": "OreRound/latest", @@ -5044,15 +5101,6 @@ } }, "resolver_hooks": [ - { - "account_type": "entropy::VarState", - "strategy": { - "PdaReverseLookup": { - "lookup_name": "default_pda_lookup", - "queue_discriminators": [] - } - } - }, { "account_type": "ore::TreasuryState", "strategy": { @@ -5072,6 +5120,15 @@ ] } } + }, + { + "account_type": "entropy::VarState", + "strategy": { + "PdaReverseLookup": { + "lookup_name": "default_pda_lookup", + "queue_discriminators": [] + } + } } ], "instruction_hooks": [ @@ -5127,14 +5184,14 @@ "pda_field": { "segments": [ "accounts", - "entropyVar" + "treasury" ], "offsets": null }, "seed_field": { "segments": [ "accounts", - "round" + "roundNext" ], "offsets": null }, @@ -5146,14 +5203,14 @@ "pda_field": { "segments": [ "accounts", - "treasury" + "entropyVar" ], "offsets": null }, "seed_field": { "segments": [ "accounts", - "roundNext" + "round" ], "offsets": null }, @@ -5280,7 +5337,7 @@ "result_type": "Option < f64 >" } ], - "content_hash": "c409570174a695cd8b4b2e0b8ac65181ffe477b99943e419af878d34f61cddce", + "content_hash": "6940e0c98d4f1b4ecc6592fb4b2fe825950f1205860b47de7723e81e7c921204", "views": [] }, { @@ -6604,15 +6661,6 @@ } }, "resolver_hooks": [ - { - "account_type": "entropy::VarState", - "strategy": { - "PdaReverseLookup": { - "lookup_name": "default_pda_lookup", - "queue_discriminators": [] - } - } - }, { "account_type": "ore::TreasuryState", "strategy": { @@ -6632,6 +6680,15 @@ ] } } + }, + { + "account_type": "entropy::VarState", + "strategy": { + "PdaReverseLookup": { + "lookup_name": "default_pda_lookup", + "queue_discriminators": [] + } + } } ], "instruction_hooks": [ @@ -6687,14 +6744,14 @@ "pda_field": { "segments": [ "accounts", - "entropyVar" + "treasury" ], "offsets": null }, "seed_field": { "segments": [ "accounts", - "round" + "roundNext" ], "offsets": null }, @@ -6706,14 +6763,14 @@ "pda_field": { "segments": [ "accounts", - "treasury" + "entropyVar" ], "offsets": null }, "seed_field": { "segments": [ "accounts", - "roundNext" + "round" ], "offsets": null }, @@ -6746,7 +6803,7 @@ "resolver_specs": [], "computed_fields": [], "computed_field_specs": [], - "content_hash": "fa965125488f32c6c794a56c517d6455d2b04d90f29250d419b73efd957ea3d3", + "content_hash": "cc153e8334a5d6bff0710db82de32f84037aaee14db0eeb7f443209e23f02e71", "views": [] } ], @@ -8854,5 +8911,5 @@ ] } ], - "content_hash": "23b682bb628f14e33ffc389f62dc06cac3119d72ee29b59da9535e96d8acea8d" + "content_hash": "e3f5ab05df7fb576313c02f6f942748f9ef597a1df30de66fa7a24a6cfe39f25" } \ No newline at end of file From 875471f23a6ca6d18723e03183bfd0e41b38958e Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:32:20 +0000 Subject: [PATCH 07/15] refactor: Removed unnecessary mutable references and added warnings for missing entity states --- .../src/codegen/vixen_runtime.rs | 70 +++++++++---------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/hyperstack-macros/src/codegen/vixen_runtime.rs b/hyperstack-macros/src/codegen/vixen_runtime.rs index 8e142ec3..59c07550 100644 --- a/hyperstack-macros/src/codegen/vixen_runtime.rs +++ b/hyperstack-macros/src/codegen/vixen_runtime.rs @@ -942,7 +942,8 @@ pub fn generate_spec_function( sched.take_due(current_slot) }; - for mut callback in due { + + for callback in due { let state = { let vm = vm.lock().unwrap_or_else(|e| e.into_inner()); vm.get_entity_state(callback.state_id, &callback.primary_key) @@ -950,31 +951,27 @@ pub fn generate_spec_function( let state = match state { Some(s) => s, - None => continue, + None => { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + "SlotScheduler: entity state not found, skipping" + ); + continue; + } }; - if let Some(ref condition) = callback.condition { - if !hyperstack::runtime::hyperstack_interpreter::scheduler::evaluate_condition(condition, &state) { - callback.retry_count += 1; - if callback.retry_count >= hyperstack::runtime::hyperstack_interpreter::scheduler::MAX_SCHEDULER_RETRIES { + let url = if let Some(ref template) = callback.url_template { + match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { + Some(u) => u, + None => { hyperstack::runtime::tracing::warn!( entity = %callback.entity_name, key = ?callback.primary_key, - retries = callback.retry_count, - "Scheduled resolver discarded after max retries" + "SlotScheduler: URL template could not be resolved" ); - } else { - let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); - sched.re_register(callback, current_slot + 1); + continue; } - continue; - } - } - - let url = if let Some(ref template) = callback.url_template { - match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { - Some(u) => u, - None => continue, } } else { continue; @@ -2072,7 +2069,8 @@ pub fn generate_multi_pipeline_spec_function( sched.take_due(current_slot) }; - for mut callback in due { + + for callback in due { let state = { let vm = vm.lock().unwrap_or_else(|e| e.into_inner()); vm.get_entity_state(callback.state_id, &callback.primary_key) @@ -2080,31 +2078,27 @@ pub fn generate_multi_pipeline_spec_function( let state = match state { Some(s) => s, - None => continue, + None => { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + "SlotScheduler: entity state not found, skipping" + ); + continue; + } }; - if let Some(ref condition) = callback.condition { - if !hyperstack::runtime::hyperstack_interpreter::scheduler::evaluate_condition(condition, &state) { - callback.retry_count += 1; - if callback.retry_count >= hyperstack::runtime::hyperstack_interpreter::scheduler::MAX_SCHEDULER_RETRIES { + let url = if let Some(ref template) = callback.url_template { + match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { + Some(u) => u, + None => { hyperstack::runtime::tracing::warn!( entity = %callback.entity_name, key = ?callback.primary_key, - retries = callback.retry_count, - "Scheduled resolver discarded after max retries" + "SlotScheduler: URL template could not be resolved" ); - } else { - let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); - sched.re_register(callback, current_slot + 1); + continue; } - continue; - } - } - - let url = if let Some(ref template) = callback.url_template { - match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { - Some(u) => u, - None => continue, } } else { continue; From b4ff1f5fefa00267a9211ea833a44d5d5c326f42 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:48:44 +0000 Subject: [PATCH 08/15] feat: enhance SlotScheduler with retry logic for entity state and URL resolution failures --- .../src/codegen/vixen_runtime.rs | 100 +++++++++++++----- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/hyperstack-macros/src/codegen/vixen_runtime.rs b/hyperstack-macros/src/codegen/vixen_runtime.rs index 59c07550..94cef82f 100644 --- a/hyperstack-macros/src/codegen/vixen_runtime.rs +++ b/hyperstack-macros/src/codegen/vixen_runtime.rs @@ -943,7 +943,9 @@ pub fn generate_spec_function( }; - for callback in due { + const MAX_RETRIES: u32 = 100; + + for mut callback in due { let state = { let vm = vm.lock().unwrap_or_else(|e| e.into_inner()); vm.get_entity_state(callback.state_id, &callback.primary_key) @@ -952,11 +954,17 @@ pub fn generate_spec_function( let state = match state { Some(s) => s, None => { - hyperstack::runtime::tracing::warn!( - entity = %callback.entity_name, - key = ?callback.primary_key, - "SlotScheduler: entity state not found, skipping" - ); + if callback.retry_count < MAX_RETRIES { + callback.retry_count += 1; + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } else { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + "SlotScheduler: entity state not found, discarding after max retries" + ); + } continue; } }; @@ -965,11 +973,17 @@ pub fn generate_spec_function( match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { Some(u) => u, None => { - hyperstack::runtime::tracing::warn!( - entity = %callback.entity_name, - key = ?callback.primary_key, - "SlotScheduler: URL template could not be resolved" - ); + if callback.retry_count < MAX_RETRIES { + callback.retry_count += 1; + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } else { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + "SlotScheduler: URL template unresolvable, discarding after max retries" + ); + } continue; } } @@ -1003,7 +1017,19 @@ pub fn generate_spec_function( requests, ).await; - if !url_mutations.is_empty() { + if url_mutations.is_empty() { + if callback.retry_count < MAX_RETRIES { + callback.retry_count += 1; + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } else { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + "SlotScheduler: resolver returned no data, discarding after max retries" + ); + } + } else { let slot_context = hyperstack::runtime::hyperstack_server::SlotContext::new(current_slot, 0); let batch = hyperstack::runtime::hyperstack_server::MutationBatch::with_slot_context( hyperstack::runtime::smallvec::SmallVec::from_vec(url_mutations), @@ -2070,7 +2096,9 @@ pub fn generate_multi_pipeline_spec_function( }; - for callback in due { + const MAX_RETRIES: u32 = 100; + + for mut callback in due { let state = { let vm = vm.lock().unwrap_or_else(|e| e.into_inner()); vm.get_entity_state(callback.state_id, &callback.primary_key) @@ -2079,11 +2107,17 @@ pub fn generate_multi_pipeline_spec_function( let state = match state { Some(s) => s, None => { - hyperstack::runtime::tracing::warn!( - entity = %callback.entity_name, - key = ?callback.primary_key, - "SlotScheduler: entity state not found, skipping" - ); + if callback.retry_count < MAX_RETRIES { + callback.retry_count += 1; + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } else { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + "SlotScheduler: entity state not found, discarding after max retries" + ); + } continue; } }; @@ -2092,11 +2126,17 @@ pub fn generate_multi_pipeline_spec_function( match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { Some(u) => u, None => { - hyperstack::runtime::tracing::warn!( - entity = %callback.entity_name, - key = ?callback.primary_key, - "SlotScheduler: URL template could not be resolved" - ); + if callback.retry_count < MAX_RETRIES { + callback.retry_count += 1; + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } else { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + "SlotScheduler: URL template unresolvable, discarding after max retries" + ); + } continue; } } @@ -2130,7 +2170,19 @@ pub fn generate_multi_pipeline_spec_function( requests, ).await; - if !url_mutations.is_empty() { + if url_mutations.is_empty() { + if callback.retry_count < MAX_RETRIES { + callback.retry_count += 1; + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } else { + hyperstack::runtime::tracing::warn!( + entity = %callback.entity_name, + key = ?callback.primary_key, + "SlotScheduler: resolver returned no data, discarding after max retries" + ); + } + } else { let slot_context = hyperstack::runtime::hyperstack_server::SlotContext::new(current_slot, 0); let batch = hyperstack::runtime::hyperstack_server::MutationBatch::with_slot_context( hyperstack::runtime::smallvec::SmallVec::from_vec(url_mutations), From 2549ee9c110f80d4cf3024b88611b0f566728289 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:45:42 +0000 Subject: [PATCH 09/15] fix: improve callback registration logic in SlotScheduler to ensure deduplication --- interpreter/src/scheduler.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interpreter/src/scheduler.rs b/interpreter/src/scheduler.rs index b630e79f..2ba35839 100644 --- a/interpreter/src/scheduler.rs +++ b/interpreter/src/scheduler.rs @@ -21,7 +21,9 @@ impl SlotScheduler { pub fn register(&mut self, target_slot: u64, callback: ScheduledCallback) { let dedup_key = Self::dedup_key(&callback); if self.registered.contains(&dedup_key) { - return; + for cbs in self.callbacks.values_mut() { + cbs.retain(|cb| Self::dedup_key(cb) != dedup_key); + } } self.registered.insert(dedup_key); self.callbacks From ee738f144011f7bda9c9da52f7cf28e714491ac4 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:47:41 +0000 Subject: [PATCH 10/15] fix: replace hardcoded MAX_RETRIES in vixen_runtime with reference to scheduler's MAX_RETRIES constant --- hyperstack-macros/src/codegen/vixen_runtime.rs | 4 ++-- interpreter/src/scheduler.rs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/hyperstack-macros/src/codegen/vixen_runtime.rs b/hyperstack-macros/src/codegen/vixen_runtime.rs index 94cef82f..49d29ab5 100644 --- a/hyperstack-macros/src/codegen/vixen_runtime.rs +++ b/hyperstack-macros/src/codegen/vixen_runtime.rs @@ -943,7 +943,7 @@ pub fn generate_spec_function( }; - const MAX_RETRIES: u32 = 100; + const MAX_RETRIES: u32 = hyperstack::runtime::hyperstack_interpreter::scheduler::MAX_RETRIES; for mut callback in due { let state = { @@ -2096,7 +2096,7 @@ pub fn generate_multi_pipeline_spec_function( }; - const MAX_RETRIES: u32 = 100; + const MAX_RETRIES: u32 = hyperstack::runtime::hyperstack_interpreter::scheduler::MAX_RETRIES; for mut callback in due { let state = { diff --git a/interpreter/src/scheduler.rs b/interpreter/src/scheduler.rs index 2ba35839..8b77eacf 100644 --- a/interpreter/src/scheduler.rs +++ b/interpreter/src/scheduler.rs @@ -3,7 +3,7 @@ use crate::vm::ScheduledCallback; use serde_json::Value; use std::collections::{BTreeMap, HashSet}; -const MAX_RETRIES: u32 = 100; +pub const MAX_RETRIES: u32 = 100; pub struct SlotScheduler { callbacks: BTreeMap>, @@ -121,5 +121,3 @@ fn compare_numeric(a: &Value, b: &Value, cmp: fn(f64, f64) -> bool) -> bool { _ => false, } } - -pub const MAX_SCHEDULER_RETRIES: u32 = MAX_RETRIES; From c838f25d61cf56524e314a57b26b32a09fa31b69 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:48:52 +0000 Subject: [PATCH 11/15] feat: add condition evaluation and SetOnce strategy handling in vixen_runtime callbacks --- .../src/codegen/vixen_runtime.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/hyperstack-macros/src/codegen/vixen_runtime.rs b/hyperstack-macros/src/codegen/vixen_runtime.rs index 49d29ab5..740fa82c 100644 --- a/hyperstack-macros/src/codegen/vixen_runtime.rs +++ b/hyperstack-macros/src/codegen/vixen_runtime.rs @@ -969,6 +969,23 @@ pub fn generate_spec_function( } }; + if let Some(ref condition) = callback.condition { + if !hyperstack::runtime::hyperstack_interpreter::scheduler::evaluate_condition(condition, &state) { + continue; + } + } + + if callback.strategy == hyperstack::runtime::hyperstack_interpreter::ast::ResolveStrategy::SetOnce { + let already_resolved = callback.extracts.iter().all(|ext| { + hyperstack::runtime::hyperstack_interpreter::scheduler::get_value_at_path(&state, &ext.target_path) + .map(|v| !v.is_null()) + .unwrap_or(false) + }); + if already_resolved { + continue; + } + } + let url = if let Some(ref template) = callback.url_template { match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { Some(u) => u, @@ -2122,6 +2139,23 @@ pub fn generate_multi_pipeline_spec_function( } }; + if let Some(ref condition) = callback.condition { + if !hyperstack::runtime::hyperstack_interpreter::scheduler::evaluate_condition(condition, &state) { + continue; + } + } + + if callback.strategy == hyperstack::runtime::hyperstack_interpreter::ast::ResolveStrategy::SetOnce { + let already_resolved = callback.extracts.iter().all(|ext| { + hyperstack::runtime::hyperstack_interpreter::scheduler::get_value_at_path(&state, &ext.target_path) + .map(|v| !v.is_null()) + .unwrap_or(false) + }); + if already_resolved { + continue; + } + } + let url = if let Some(ref template) = callback.url_template { match hyperstack::runtime::hyperstack_interpreter::scheduler::build_url_from_template(template, &state) { Some(u) => u, From 12947acff717285f65b105550d31377dea8876fe Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:52:47 +0000 Subject: [PATCH 12/15] feat: add input_value and input_path to ScheduledCallback for enhanced callback handling in vixen_runtime --- .../src/codegen/vixen_runtime.rs | 40 +++++++++++++++++++ interpreter/src/vm.rs | 4 ++ 2 files changed, 44 insertions(+) diff --git a/hyperstack-macros/src/codegen/vixen_runtime.rs b/hyperstack-macros/src/codegen/vixen_runtime.rs index 740fa82c..63986530 100644 --- a/hyperstack-macros/src/codegen/vixen_runtime.rs +++ b/hyperstack-macros/src/codegen/vixen_runtime.rs @@ -1004,6 +1004,26 @@ pub fn generate_spec_function( continue; } } + } else if let Some(ref val) = callback.input_value { + match val.as_str() { + Some(s) => s.to_string(), + None => val.to_string().trim_matches('"').to_string(), + } + } else if let Some(ref path) = callback.input_path { + match hyperstack::runtime::hyperstack_interpreter::scheduler::get_value_at_path(&state, path) { + Some(v) if !v.is_null() => match v.as_str() { + Some(s) => s.to_string(), + None => v.to_string().trim_matches('"').to_string(), + }, + _ => { + if callback.retry_count < MAX_RETRIES { + callback.retry_count += 1; + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } + continue; + } + } } else { continue; }; @@ -2174,6 +2194,26 @@ pub fn generate_multi_pipeline_spec_function( continue; } } + } else if let Some(ref val) = callback.input_value { + match val.as_str() { + Some(s) => s.to_string(), + None => val.to_string().trim_matches('"').to_string(), + } + } else if let Some(ref path) = callback.input_path { + match hyperstack::runtime::hyperstack_interpreter::scheduler::get_value_at_path(&state, path) { + Some(v) if !v.is_null() => match v.as_str() { + Some(s) => s.to_string(), + None => v.to_string().trim_matches('"').to_string(), + }, + _ => { + if callback.retry_count < MAX_RETRIES { + callback.retry_count += 1; + let mut sched = scheduler.lock().unwrap_or_else(|e| e.into_inner()); + sched.re_register(callback, current_slot + 1); + } + continue; + } + } } else { continue; }; diff --git a/interpreter/src/vm.rs b/interpreter/src/vm.rs index 43702d17..d9f3c8ad 100644 --- a/interpreter/src/vm.rs +++ b/interpreter/src/vm.rs @@ -628,6 +628,8 @@ pub struct ScheduledCallback { pub primary_key: Value, pub resolver: ResolverType, pub url_template: Option>, + pub input_value: Option, + pub input_path: Option, pub condition: Option, pub strategy: ResolveStrategy, pub extracts: Vec, @@ -2622,6 +2624,8 @@ impl VmContext { primary_key: key_value.clone(), resolver: resolver.clone(), url_template: url_template.clone(), + input_value: input_value.clone(), + input_path: input_path.clone(), condition: condition.clone(), strategy: strategy.clone(), extracts: extracts.clone(), From 5c977cebf36c86786b9783445f10ce0ada6e9b6d Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:55:43 +0000 Subject: [PATCH 13/15] feat: add PartialEq and Eq traits to various structs and enums --- interpreter/src/ast.rs | 6 +++--- interpreter/src/vm.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interpreter/src/ast.rs b/interpreter/src/ast.rs index 217a71a1..bf50da0e 100644 --- a/interpreter/src/ast.rs +++ b/interpreter/src/ast.rs @@ -162,7 +162,7 @@ pub struct UrlResolverConfig { pub extract_path: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ResolverExtractSpec { pub target_path: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -178,7 +178,7 @@ pub enum ResolveStrategy { LastWrite, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ResolverCondition { pub field_path: String, pub op: ComparisonOp, @@ -625,7 +625,7 @@ pub enum ParsedCondition { }, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum ComparisonOp { Equal, NotEqual, diff --git a/interpreter/src/vm.rs b/interpreter/src/vm.rs index d9f3c8ad..b094e603 100644 --- a/interpreter/src/vm.rs +++ b/interpreter/src/vm.rs @@ -621,7 +621,7 @@ pub struct PendingResolverEntry { pub queued_at: i64, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ScheduledCallback { pub state_id: u32, pub entity_name: String, From 0e2a8d648a116e3739983383d4818233bf052d73 Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:57:35 +0000 Subject: [PATCH 14/15] feat: implement Default trait for SlotScheduler and simplify URL string handling --- interpreter/src/scheduler.rs | 8 +++++++- interpreter/src/vm.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/interpreter/src/scheduler.rs b/interpreter/src/scheduler.rs index 8b77eacf..23cffa9f 100644 --- a/interpreter/src/scheduler.rs +++ b/interpreter/src/scheduler.rs @@ -10,6 +10,12 @@ pub struct SlotScheduler { registered: HashSet<(String, String, String)>, } +impl Default for SlotScheduler { + fn default() -> Self { + Self::new() + } +} + impl SlotScheduler { pub fn new() -> Self { Self { @@ -84,7 +90,7 @@ pub fn build_url_from_template(template: &[UrlTemplatePart], state: &Value) -> O } match val.as_str() { Some(s) => url.push_str(s), - None => url.push_str(&val.to_string().trim_matches('"').to_string()), + None => url.push_str(val.to_string().trim_matches('"')), } } } diff --git a/interpreter/src/vm.rs b/interpreter/src/vm.rs index b094e603..c4cf80a0 100644 --- a/interpreter/src/vm.rs +++ b/interpreter/src/vm.rs @@ -2657,7 +2657,7 @@ impl VmContext { match val.as_str() { Some(s) => url.push_str(s), None => url.push_str( - &val.to_string().trim_matches('"').to_string(), + val.to_string().trim_matches('"'), ), } } From 720069b603d31d8784a9b892277cd4f9d54a24ef Mon Sep 17 00:00:00 2001 From: VimMotions <220220743+vimmotions@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:03:52 +0000 Subject: [PATCH 15/15] chore: Generate new ore sdk --- stacks/sdk/rust/src/ore/types.rs | 2 ++ stacks/sdk/typescript/src/ore/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/stacks/sdk/rust/src/ore/types.rs b/stacks/sdk/rust/src/ore/types.rs index 41944ff9..b97fa4f2 100644 --- a/stacks/sdk/rust/src/ore/types.rs +++ b/stacks/sdk/rust/src/ore/types.rs @@ -80,6 +80,8 @@ pub struct OreRoundEntropy { pub entropy_samples: Option>, #[serde(default)] pub entropy_var_address: Option>, + #[serde(default)] + pub resolved_seed: Option>>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/stacks/sdk/typescript/src/ore/index.ts b/stacks/sdk/typescript/src/ore/index.ts index 46975eca..929235e0 100644 --- a/stacks/sdk/typescript/src/ore/index.ts +++ b/stacks/sdk/typescript/src/ore/index.ts @@ -8,6 +8,7 @@ export interface OreRoundEntropy { entropy_start_at?: number | null; entropy_value?: string | null; entropy_var_address?: string | null; + resolved_seed?: any[] | null; } export interface OreRoundId { @@ -81,6 +82,7 @@ export const OreRoundEntropySchema = z.object({ entropy_start_at: z.number().nullable().optional(), entropy_value: z.string().nullable().optional(), entropy_var_address: z.string().nullable().optional(), + resolved_seed: z.array(z.any()).nullable().optional(), }); export const OreRoundIdSchema = z.object({