@@ -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
@@ -1370,6 +1333,68 @@ impl InstanceCommon {
13701333 }
13711334}
13721335
1336+ fn collect_subscribed_view_calls (
1337+ tx : & MutTxId ,
1338+ module_def : & ModuleDef ,
1339+ owner_identity : Identity ,
1340+ ) -> Result < Vec < CallViewParams > , anyhow:: Error > {
1341+ let mut view_calls = Vec :: new ( ) ;
1342+
1343+ for view in module_def. views ( ) {
1344+ let ViewDef {
1345+ name : view_name,
1346+ is_anonymous,
1347+ fn_ptr,
1348+ product_type_ref,
1349+ ..
1350+ } = view;
1351+
1352+ let st_view = tx
1353+ . view_from_name ( view_name) ?
1354+ . ok_or_else ( || anyhow:: anyhow!( "view {} not found in database" , & view_name) ) ?;
1355+
1356+ let view_id = st_view. view_id ;
1357+ let table_id = st_view
1358+ . table_id
1359+ . ok_or_else ( || anyhow:: anyhow!( "view {} does not have a backing table in database" , & view_name) ) ?;
1360+ let subs = tx. lookup_st_view_subs ( view_id) ?;
1361+
1362+ if * is_anonymous {
1363+ if subs. is_empty ( ) {
1364+ continue ;
1365+ }
1366+ view_calls. push ( CallViewParams {
1367+ view_name : view_name. clone ( ) ,
1368+ view_id,
1369+ table_id,
1370+ fn_ptr : * fn_ptr,
1371+ caller : owner_identity,
1372+ sender : None ,
1373+ args : ArgsTuple :: nullary ( ) ,
1374+ row_type : * product_type_ref,
1375+ timestamp : Timestamp :: now ( ) ,
1376+ } ) ;
1377+ continue ;
1378+ }
1379+
1380+ for sub in subs {
1381+ view_calls. push ( CallViewParams {
1382+ view_name : view_name. clone ( ) ,
1383+ view_id,
1384+ table_id,
1385+ fn_ptr : * fn_ptr,
1386+ caller : owner_identity,
1387+ sender : Some ( sub. identity . into ( ) ) ,
1388+ args : ArgsTuple :: nullary ( ) ,
1389+ row_type : * product_type_ref,
1390+ timestamp : Timestamp :: now ( ) ,
1391+ } ) ;
1392+ }
1393+ }
1394+
1395+ Ok ( view_calls)
1396+ }
1397+
13731398/// Pre-fetched VM metrics counters for all reducers and views in a module.
13741399/// Anonymous views have lazily fetched metrics counters.
13751400struct AllVmMetrics {
@@ -1712,3 +1737,91 @@ impl InstanceOp for ProcedureOp {
17121737 FuncCallType :: Procedure
17131738 }
17141739}
1740+
1741+ #[ cfg( test) ]
1742+ mod tests {
1743+ use super :: collect_subscribed_view_calls;
1744+ use crate :: db:: relational_db:: tests_utils:: { begin_mut_tx, TestDB } ;
1745+ use spacetimedb_lib:: db:: raw_def:: v9:: RawModuleDefV9Builder ;
1746+ use spacetimedb_lib:: { AlgebraicType , Identity , ProductType } ;
1747+ use spacetimedb_primitives:: ArgId ;
1748+ use spacetimedb_sats:: raw_identifier:: RawIdentifier ;
1749+ use spacetimedb_schema:: def:: ModuleDef ;
1750+
1751+ fn module_def_for_view ( name : & str , is_anonymous : bool ) -> ModuleDef {
1752+ let mut builder = RawModuleDefV9Builder :: new ( ) ;
1753+ let name = RawIdentifier :: new ( name) ;
1754+ let type_ref = builder. add_algebraic_type (
1755+ [ ] ,
1756+ name. clone ( ) ,
1757+ AlgebraicType :: Product ( ProductType :: from_iter ( [ ( "x" , AlgebraicType :: U8 ) ] ) ) ,
1758+ true ,
1759+ ) ;
1760+
1761+ builder. add_view (
1762+ name. clone ( ) ,
1763+ 0 ,
1764+ true ,
1765+ is_anonymous,
1766+ ProductType :: unit ( ) ,
1767+ AlgebraicType :: array ( AlgebraicType :: Ref ( type_ref) ) ,
1768+ ) ;
1769+
1770+ builder. finish ( ) . try_into ( ) . expect ( "test module def should be valid" )
1771+ }
1772+
1773+ /// Regression test for evaluating anonymous views.
1774+ ///
1775+ /// Anonymous views have one shared materialization,
1776+ /// so we should only re-evaluate once even if there are multiple subscribers.
1777+ #[ test]
1778+ fn test_dedup_anonymous_view_calls ( ) -> anyhow:: Result < ( ) > {
1779+ let stdb = TestDB :: in_memory ( ) ?;
1780+ let module_def = module_def_for_view ( "anonymous_view" , true ) ;
1781+ let view_def = module_def. view ( "anonymous_view" ) . expect ( "view should exist" ) ;
1782+
1783+ let mut tx = begin_mut_tx ( & stdb) ;
1784+ let ( view_id, _table_id) = stdb. create_view ( & mut tx, & module_def, view_def) ?;
1785+ tx. subscribe_view ( view_id, ArgId :: SENTINEL , Identity :: ZERO ) ?;
1786+ tx. subscribe_view ( view_id, ArgId :: SENTINEL , Identity :: ONE ) ?;
1787+
1788+ // Two subscriber rows exist, but anonymous views should still be reevaluated once
1789+ // because they share a single materialization.
1790+ let calls = collect_subscribed_view_calls ( & tx, & module_def, Identity :: ZERO ) ?;
1791+
1792+ assert_eq ! (
1793+ calls. len( ) ,
1794+ 1 ,
1795+ "anonymous views should only be reevaluated once even with multiple subscriber rows"
1796+ ) ;
1797+ assert_eq ! ( calls[ 0 ] . view_id, view_id) ;
1798+ assert_eq ! ( calls[ 0 ] . sender, None ) ;
1799+ Ok ( ( ) )
1800+ }
1801+
1802+ /// Regression test for evaluating sender-scoped views.
1803+ ///
1804+ /// These views have separate materializations per sender,
1805+ /// so reevaluation must emit one call per subscribed sender.
1806+ #[ test]
1807+ fn test_distinct_sender_scoped_view_calls ( ) -> anyhow:: Result < ( ) > {
1808+ let stdb = TestDB :: in_memory ( ) ?;
1809+ let module_def = module_def_for_view ( "sender_view" , false ) ;
1810+ let view_def = module_def. view ( "sender_view" ) . expect ( "view should exist" ) ;
1811+
1812+ let mut tx = begin_mut_tx ( & stdb) ;
1813+ let ( view_id, _table_id) = stdb. create_view ( & mut tx, & module_def, view_def) ?;
1814+ tx. subscribe_view ( view_id, ArgId :: SENTINEL , Identity :: ZERO ) ?;
1815+ tx. subscribe_view ( view_id, ArgId :: SENTINEL , Identity :: ONE ) ?;
1816+
1817+ // Sender-backed views keep one materialization per sender, so reevaluation must
1818+ // preserve both callers.
1819+ let calls = collect_subscribed_view_calls ( & tx, & module_def, Identity :: ZERO ) ?;
1820+ let senders: Vec < _ > = calls. iter ( ) . filter_map ( |call| call. sender ) . collect ( ) ;
1821+
1822+ assert_eq ! ( calls. len( ) , 2 , "sender views should still reevaluate once per sender" ) ;
1823+ assert ! ( senders. contains( & Identity :: ZERO ) ) ;
1824+ assert ! ( senders. contains( & Identity :: ONE ) ) ;
1825+ Ok ( ( ) )
1826+ }
1827+ }
0 commit comments