@@ -8,8 +8,10 @@ import Storage from './storage';
88import utils from './utils' ;
99import DevTools from './DevTools' ;
1010import type {
11+ AnyComputedKey ,
1112 Collection ,
1213 CollectionKeyBase ,
14+ ComputedKey ,
1315 ConnectOptions ,
1416 InitOptions ,
1517 KeyValueMapping ,
@@ -63,6 +65,15 @@ function init({
6365 Promise . all ( [ OnyxUtils . addAllSafeEvictionKeysToRecentlyAccessedList ( ) , OnyxUtils . initializeWithDefaultKeyStates ( ) ] ) . then ( deferredInitTask . resolve ) ;
6466}
6567
68+ function computeAndSendData ( mapping : Mapping < AnyComputedKey > , dependencies : Record < string , unknown > ) {
69+ let val = cache . getValue ( mapping . key . cacheKey ) ;
70+ if ( val === undefined ) {
71+ val = mapping . key . compute ( dependencies ) ;
72+ cache . set ( mapping . key . cacheKey , val ) ;
73+ }
74+ OnyxUtils . sendDataToConnection ( mapping , val , mapping . key . cacheKey , true ) ;
75+ }
76+
6677/**
6778 * Subscribes a react component's state directly to a store key
6879 *
@@ -91,12 +102,54 @@ function init({
91102 * Note that it will not cause the component to have the loading prop set to true.
92103 * @returns an ID to use when calling disconnect
93104 */
94- function connect < TKey extends OnyxKey > ( mapping : ConnectOptions < TKey > ) : number {
105+ function connect < TKey extends OnyxKey | AnyComputedKey > ( mapping : ConnectOptions < TKey > ) : number {
95106 const connectionID = lastConnectionID ++ ;
96107 const callbackToStateMapping = OnyxUtils . getCallbackToStateMapping ( ) ;
97108 callbackToStateMapping [ connectionID ] = mapping ;
98109 callbackToStateMapping [ connectionID ] . connectionID = connectionID ;
99110
111+ const mappingKey = mapping . key ;
112+ if ( OnyxUtils . isComputedKey ( mappingKey ) ) {
113+ deferredInitTask . promise
114+ . then ( ( ) => OnyxUtils . addKeyToRecentlyAccessedIfNeeded ( mapping ) )
115+ . then ( ( ) => {
116+ const mappingDependencies = mappingKey . dependencies || { } ;
117+ const dependenciesCount = _ . size ( mappingDependencies ) ;
118+ if ( dependenciesCount === 0 ) {
119+ // If we have no dependencies we can send the computed value immediately.
120+ computeAndSendData ( mapping as Mapping < AnyComputedKey > , { } ) ;
121+ } else {
122+ callbackToStateMapping [ connectionID ] . dependencyConnections = [ ] ;
123+
124+ const dependencyValues : Record < string , unknown > = { } ;
125+ _ . each ( mappingDependencies , ( dependency , dependencyKey ) => {
126+ // Create a mapping of dependent cache keys so when a key changes, all dependent keys
127+ // can also be cleared from the cache.
128+ const cacheKey = OnyxUtils . getCacheKey ( dependency ) ;
129+ OnyxUtils . addDependentCacheKey ( cacheKey , mappingKey . cacheKey ) ;
130+
131+ // Connect to dependencies.
132+ const dependencyConnection = connect ( {
133+ key : dependency ,
134+ waitForCollectionCallback : true ,
135+ callback : ( value ) => {
136+ dependencyValues [ dependencyKey ] = value ;
137+
138+ // Once all dependencies are ready, compute the value and send it to the connection.
139+ if ( _ . size ( dependencyValues ) === dependenciesCount ) {
140+ computeAndSendData ( mapping as Mapping < AnyComputedKey > , dependencyValues ) ;
141+ }
142+ } ,
143+ } ) ;
144+
145+ // Store dependency connections so we can disconnect them later.
146+ callbackToStateMapping [ connectionID ] . dependencyConnections . push ( dependencyConnection ) ;
147+ } ) ;
148+ }
149+ } ) ;
150+ return connectionID ;
151+ }
152+
100153 if ( mapping . initWithStoredValues === false ) {
101154 return connectionID ;
102155 }
@@ -108,24 +161,24 @@ function connect<TKey extends OnyxKey>(mapping: ConnectOptions<TKey>): number {
108161 // Performance improvement
109162 // If the mapping is connected to an onyx key that is not a collection
110163 // we can skip the call to getAllKeys() and return an array with a single item
111- if ( Boolean ( mapping . key ) && typeof mapping . key === 'string' && ! mapping . key . endsWith ( '_' ) && cache . storageKeys . has ( mapping . key ) ) {
112- return new Set ( [ mapping . key ] ) ;
164+ if ( Boolean ( mappingKey ) && typeof mappingKey === 'string' && ! mappingKey . endsWith ( '_' ) && cache . storageKeys . has ( mappingKey ) ) {
165+ return new Set ( [ mappingKey ] ) ;
113166 }
114167 return OnyxUtils . getAllKeys ( ) ;
115168 } )
116169 . then ( ( keys ) => {
117170 // We search all the keys in storage to see if any are a "match" for the subscriber we are connecting so that we
118171 // can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
119172 // subscribed to a "collection key" or a single key.
120- const matchingKeys = Array . from ( keys ) . filter ( ( key ) => OnyxUtils . isKeyMatch ( mapping . key , key ) ) ;
173+ const matchingKeys = Array . from ( keys ) . filter ( ( key ) => OnyxUtils . isKeyMatch ( mappingKey , key ) ) ;
121174
122175 // If the key being connected to does not exist we initialize the value with null. For subscribers that connected
123176 // directly via connect() they will simply get a null value sent to them without any information about which key matched
124177 // since there are none matched. In withOnyx() we wait for all connected keys to return a value before rendering the child
125178 // component. This null value will be filtered out so that the connected component can utilize defaultProps.
126179 if ( matchingKeys . length === 0 ) {
127- if ( mapping . key && ! OnyxUtils . isCollectionKey ( mapping . key ) ) {
128- cache . set ( mapping . key , null ) ;
180+ if ( mappingKey && ! OnyxUtils . isCollectionKey ( mappingKey ) ) {
181+ cache . set ( mappingKey , null ) ;
129182 }
130183
131184 // Here we cannot use batching because the null value is expected to be set immediately for default props
@@ -138,7 +191,7 @@ function connect<TKey extends OnyxKey>(mapping: ConnectOptions<TKey>): number {
138191 // into an object and just make a single call. The latter behavior is enabled by providing a waitForCollectionCallback key
139192 // combined with a subscription to a collection key.
140193 if ( typeof mapping . callback === 'function' ) {
141- if ( OnyxUtils . isCollectionKey ( mapping . key ) ) {
194+ if ( OnyxUtils . isCollectionKey ( mappingKey ) ) {
142195 if ( mapping . waitForCollectionCallback ) {
143196 OnyxUtils . getCollectionDataAndSendAsObject ( matchingKeys , mapping ) ;
144197 return ;
@@ -147,26 +200,26 @@ function connect<TKey extends OnyxKey>(mapping: ConnectOptions<TKey>): number {
147200 // We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
148201 // eslint-disable-next-line @typescript-eslint/prefer-for-of
149202 for ( let i = 0 ; i < matchingKeys . length ; i ++ ) {
150- OnyxUtils . get ( matchingKeys [ i ] ) . then ( ( val ) => OnyxUtils . sendDataToConnection ( mapping , val , matchingKeys [ i ] as TKey , true ) ) ;
203+ OnyxUtils . get ( matchingKeys [ i ] ) . then ( ( val ) => OnyxUtils . sendDataToConnection ( mapping , val , matchingKeys [ i ] , true ) ) ;
151204 }
152205 return ;
153206 }
154207
155208 // If we are not subscribed to a collection key then there's only a single key to send an update for.
156- OnyxUtils . get ( mapping . key ) . then ( ( val ) => OnyxUtils . sendDataToConnection ( mapping , val , mapping . key , true ) ) ;
209+ OnyxUtils . get ( mappingKey ) . then ( ( val ) => OnyxUtils . sendDataToConnection ( mapping , val , mappingKey , true ) ) ;
157210 return ;
158211 }
159212
160213 // If we have a withOnyxInstance that means a React component has subscribed via the withOnyx() HOC and we need to
161214 // group collection key member data into an object.
162215 if ( mapping . withOnyxInstance ) {
163- if ( OnyxUtils . isCollectionKey ( mapping . key ) ) {
216+ if ( OnyxUtils . isCollectionKey ( mappingKey ) ) {
164217 OnyxUtils . getCollectionDataAndSendAsObject ( matchingKeys , mapping ) ;
165218 return ;
166219 }
167220
168221 // If the subscriber is not using a collection key then we just send a single value back to the subscriber
169- OnyxUtils . get ( mapping . key ) . then ( ( val ) => OnyxUtils . sendDataToConnection ( mapping , val , mapping . key , true ) ) ;
222+ OnyxUtils . get ( mappingKey ) . then ( ( val ) => OnyxUtils . sendDataToConnection ( mapping , val , mappingKey , true ) ) ;
170223 return ;
171224 }
172225
@@ -197,6 +250,10 @@ function disconnect(connectionID: number, keyToRemoveFromEvictionBlocklist?: Ony
197250 OnyxUtils . removeFromEvictionBlockList ( keyToRemoveFromEvictionBlocklist , connectionID ) ;
198251 }
199252
253+ if ( callbackToStateMapping [ connectionID ] . dependencyConnections ) {
254+ callbackToStateMapping [ connectionID ] . dependencyConnections . forEach ( ( id : number ) => disconnect ( id ) ) ;
255+ }
256+
200257 delete callbackToStateMapping [ connectionID ] ;
201258}
202259
0 commit comments