@@ -688,44 +688,7 @@ impl InstanceCommon {
688688 tx : MutTxId ,
689689 inst : & mut I ,
690690 ) -> Result < ( ViewCallResult , bool ) , anyhow:: Error > {
691- let views = self . info . module_def . views ( ) . collect :: < Vec < _ > > ( ) ;
692- let owner_identity = self . info . owner_identity ;
693-
694- let mut view_calls = Vec :: new ( ) ;
695-
696- for view in views {
697- let ViewDef {
698- name : view_name,
699- is_anonymous,
700- fn_ptr,
701- product_type_ref,
702- ..
703- } = view;
704-
705- let st_view = tx
706- . view_from_name ( view_name) ?
707- . ok_or_else ( || anyhow:: anyhow!( "view {} not found in database" , & view_name) ) ?;
708-
709- let view_id = st_view. view_id ;
710- let table_id = st_view
711- . table_id
712- . ok_or_else ( || anyhow:: anyhow!( "view {} does not have a backing table in database" , & view_name) ) ?;
713-
714- for sub in tx. lookup_st_view_subs ( view_id) ? {
715- view_calls. push ( CallViewParams {
716- view_name : view_name. clone ( ) ,
717- view_id,
718- table_id,
719- fn_ptr : * fn_ptr,
720- caller : owner_identity,
721- sender : if * is_anonymous { None } else { Some ( sub. identity . into ( ) ) } ,
722- args : ArgsTuple :: nullary ( ) ,
723- row_type : * product_type_ref,
724- timestamp : Timestamp :: now ( ) ,
725- } ) ;
726- }
727- }
728-
691+ let view_calls = collect_subscribed_view_calls ( & tx, & self . info . module_def , self . info . owner_identity ) ?;
729692 Ok ( self . execute_view_calls ( tx, view_calls, inst) )
730693 }
731694
@@ -1388,6 +1351,68 @@ impl InstanceCommon {
13881351 }
13891352}
13901353
1354+ fn collect_subscribed_view_calls (
1355+ tx : & MutTxId ,
1356+ module_def : & ModuleDef ,
1357+ owner_identity : Identity ,
1358+ ) -> Result < Vec < CallViewParams > , anyhow:: Error > {
1359+ let mut view_calls = Vec :: new ( ) ;
1360+
1361+ for view in module_def. views ( ) {
1362+ let ViewDef {
1363+ name : view_name,
1364+ is_anonymous,
1365+ fn_ptr,
1366+ product_type_ref,
1367+ ..
1368+ } = view;
1369+
1370+ let st_view = tx
1371+ . view_from_name ( view_name) ?
1372+ . ok_or_else ( || anyhow:: anyhow!( "view {} not found in database" , & view_name) ) ?;
1373+
1374+ let view_id = st_view. view_id ;
1375+ let table_id = st_view
1376+ . table_id
1377+ . ok_or_else ( || anyhow:: anyhow!( "view {} does not have a backing table in database" , & view_name) ) ?;
1378+ let subs = tx. lookup_st_view_subs ( view_id) ?;
1379+
1380+ if * is_anonymous {
1381+ if subs. is_empty ( ) {
1382+ continue ;
1383+ }
1384+ view_calls. push ( CallViewParams {
1385+ view_name : view_name. clone ( ) ,
1386+ view_id,
1387+ table_id,
1388+ fn_ptr : * fn_ptr,
1389+ caller : owner_identity,
1390+ sender : None ,
1391+ args : ArgsTuple :: nullary ( ) ,
1392+ row_type : * product_type_ref,
1393+ timestamp : Timestamp :: now ( ) ,
1394+ } ) ;
1395+ continue ;
1396+ }
1397+
1398+ for sub in subs {
1399+ view_calls. push ( CallViewParams {
1400+ view_name : view_name. clone ( ) ,
1401+ view_id,
1402+ table_id,
1403+ fn_ptr : * fn_ptr,
1404+ caller : owner_identity,
1405+ sender : Some ( sub. identity . into ( ) ) ,
1406+ args : ArgsTuple :: nullary ( ) ,
1407+ row_type : * product_type_ref,
1408+ timestamp : Timestamp :: now ( ) ,
1409+ } ) ;
1410+ }
1411+ }
1412+
1413+ Ok ( view_calls)
1414+ }
1415+
13911416/// Pre-fetched VM metrics counters for all reducers and views in a module.
13921417/// Anonymous views have lazily fetched metrics counters.
13931418struct AllVmMetrics {
@@ -1730,3 +1755,91 @@ impl InstanceOp for ProcedureOp {
17301755 FuncCallType :: Procedure
17311756 }
17321757}
1758+
1759+ #[ cfg( test) ]
1760+ mod tests {
1761+ use super :: collect_subscribed_view_calls;
1762+ use crate :: db:: relational_db:: tests_utils:: { begin_mut_tx, TestDB } ;
1763+ use spacetimedb_lib:: db:: raw_def:: v9:: RawModuleDefV9Builder ;
1764+ use spacetimedb_lib:: { AlgebraicType , Identity , ProductType } ;
1765+ use spacetimedb_primitives:: ArgId ;
1766+ use spacetimedb_sats:: raw_identifier:: RawIdentifier ;
1767+ use spacetimedb_schema:: def:: ModuleDef ;
1768+
1769+ fn module_def_for_view ( name : & str , is_anonymous : bool ) -> ModuleDef {
1770+ let mut builder = RawModuleDefV9Builder :: new ( ) ;
1771+ let name = RawIdentifier :: new ( name) ;
1772+ let type_ref = builder. add_algebraic_type (
1773+ [ ] ,
1774+ name. clone ( ) ,
1775+ AlgebraicType :: Product ( ProductType :: from_iter ( [ ( "x" , AlgebraicType :: U8 ) ] ) ) ,
1776+ true ,
1777+ ) ;
1778+
1779+ builder. add_view (
1780+ name. clone ( ) ,
1781+ 0 ,
1782+ true ,
1783+ is_anonymous,
1784+ ProductType :: unit ( ) ,
1785+ AlgebraicType :: array ( AlgebraicType :: Ref ( type_ref) ) ,
1786+ ) ;
1787+
1788+ builder. finish ( ) . try_into ( ) . expect ( "test module def should be valid" )
1789+ }
1790+
1791+ /// Regression test for evaluating anonymous views.
1792+ ///
1793+ /// Anonymous views have one shared materialization,
1794+ /// so we should only re-evaluate once even if there are multiple subscribers.
1795+ #[ test]
1796+ fn test_dedup_anonymous_view_calls ( ) -> anyhow:: Result < ( ) > {
1797+ let stdb = TestDB :: in_memory ( ) ?;
1798+ let module_def = module_def_for_view ( "anonymous_view" , true ) ;
1799+ let view_def = module_def. view ( "anonymous_view" ) . expect ( "view should exist" ) ;
1800+
1801+ let mut tx = begin_mut_tx ( & stdb) ;
1802+ let ( view_id, _table_id) = stdb. create_view ( & mut tx, & module_def, view_def) ?;
1803+ tx. subscribe_view ( view_id, ArgId :: SENTINEL , Identity :: ZERO ) ?;
1804+ tx. subscribe_view ( view_id, ArgId :: SENTINEL , Identity :: ONE ) ?;
1805+
1806+ // Two subscriber rows exist, but anonymous views should still be reevaluated once
1807+ // because they share a single materialization.
1808+ let calls = collect_subscribed_view_calls ( & tx, & module_def, Identity :: ZERO ) ?;
1809+
1810+ assert_eq ! (
1811+ calls. len( ) ,
1812+ 1 ,
1813+ "anonymous views should only be reevaluated once even with multiple subscriber rows"
1814+ ) ;
1815+ assert_eq ! ( calls[ 0 ] . view_id, view_id) ;
1816+ assert_eq ! ( calls[ 0 ] . sender, None ) ;
1817+ Ok ( ( ) )
1818+ }
1819+
1820+ /// Regression test for evaluating sender-scoped views.
1821+ ///
1822+ /// These views have separate materializations per sender,
1823+ /// so reevaluation must emit one call per subscribed sender.
1824+ #[ test]
1825+ fn test_distinct_sender_scoped_view_calls ( ) -> anyhow:: Result < ( ) > {
1826+ let stdb = TestDB :: in_memory ( ) ?;
1827+ let module_def = module_def_for_view ( "sender_view" , false ) ;
1828+ let view_def = module_def. view ( "sender_view" ) . expect ( "view should exist" ) ;
1829+
1830+ let mut tx = begin_mut_tx ( & stdb) ;
1831+ let ( view_id, _table_id) = stdb. create_view ( & mut tx, & module_def, view_def) ?;
1832+ tx. subscribe_view ( view_id, ArgId :: SENTINEL , Identity :: ZERO ) ?;
1833+ tx. subscribe_view ( view_id, ArgId :: SENTINEL , Identity :: ONE ) ?;
1834+
1835+ // Sender-backed views keep one materialization per sender, so reevaluation must
1836+ // preserve both callers.
1837+ let calls = collect_subscribed_view_calls ( & tx, & module_def, Identity :: ZERO ) ?;
1838+ let senders: Vec < _ > = calls. iter ( ) . filter_map ( |call| call. sender ) . collect ( ) ;
1839+
1840+ assert_eq ! ( calls. len( ) , 2 , "sender views should still reevaluate once per sender" ) ;
1841+ assert ! ( senders. contains( & Identity :: ZERO ) ) ;
1842+ assert ! ( senders. contains( & Identity :: ONE ) ) ;
1843+ Ok ( ( ) )
1844+ }
1845+ }
0 commit comments