@@ -100,7 +100,9 @@ let lastSubscriptionID = 0;
100100// Connections can be made before `Onyx.init`. They would wait for this task before resolving
101101const deferredInitTask = createDeferredTask ( ) ;
102102
103- // Holds a set of collection member IDs which updates will be ignored when using Onyx methods.
103+ // Collection member IDs that Onyx should silently ignore across all operations — reads, writes, cache, and subscriber
104+ // notifications. This is used to filter out keys formed from invalid/default IDs (e.g. "-1", "0",
105+ // "undefined", "null", "NaN") that can appear when an ID variable is accidentally coerced to string.
104106let skippableCollectionMemberIDs = new Set < string > ( ) ;
105107// Holds a set of keys that should always be merged into snapshot entries.
106108let snapshotMergeKeys = new Set < string > ( ) ;
@@ -1111,19 +1113,76 @@ function mergeInternal<TValue extends OnyxInput<OnyxKey> | undefined, TChange ex
11111113 * Merge user provided default key value pairs.
11121114 */
11131115function initializeWithDefaultKeyStates ( ) : Promise < void > {
1114- // Filter out RAM-only keys from storage reads as they may have stale persisted data
1115- // from before the key was migrated to RAM-only.
1116- const keysToFetch = Object . keys ( defaultKeyStates ) . filter ( ( key ) => ! isRamOnlyKey ( key ) ) ;
1117- return Storage . multiGet ( keysToFetch ) . then ( ( pairs ) => {
1118- const existingDataAsObject = Object . fromEntries ( pairs ) as Record < string , unknown > ;
1119-
1120- const merged = utils . fastMerge ( existingDataAsObject , defaultKeyStates , {
1121- shouldRemoveNestedNulls : true ,
1122- } ) . result ;
1123- cache . merge ( merged ?? { } ) ;
1124-
1125- for ( const [ key , value ] of Object . entries ( merged ?? { } ) ) keyChanged ( key , value ) ;
1126- } ) ;
1116+ // Eagerly load the entire database into cache in a single batch read.
1117+ // This is faster than lazy-loading individual keys because:
1118+ // 1. One DB transaction instead of hundreds
1119+ // 2. All subsequent reads are synchronous cache hits
1120+ return Storage . getAll ( )
1121+ . then ( ( pairs ) => {
1122+ const allDataFromStorage : Record < string , unknown > = { } ;
1123+ for ( const [ key , value ] of pairs ) {
1124+ // RAM-only keys should not be cached from storage as they may have stale persisted data
1125+ // from before the key was migrated to RAM-only.
1126+ if ( isRamOnlyKey ( key ) ) {
1127+ continue ;
1128+ }
1129+
1130+ // Skip collection members that are marked as skippable
1131+ if ( skippableCollectionMemberIDs . size && getCollectionKey ( key ) ) {
1132+ const [ , collectionMemberID ] = splitCollectionMemberKey ( key ) ;
1133+
1134+ if ( skippableCollectionMemberIDs . has ( collectionMemberID ) ) {
1135+ continue ;
1136+ }
1137+ }
1138+
1139+ allDataFromStorage [ key ] = value ;
1140+ }
1141+
1142+ // Load all storage data into cache silently (no subscriber notifications)
1143+ cache . setAllKeys ( Object . keys ( allDataFromStorage ) ) ;
1144+ cache . merge ( allDataFromStorage ) ;
1145+
1146+ // For keys that have a developer-defined default (via `initialKeyStates`), merge the
1147+ // persisted value with the default so new properties added in code updates are applied
1148+ // without wiping user data that already exists in storage.
1149+ const defaultKeysFromStorage = Object . keys ( defaultKeyStates ) . reduce ( ( obj : Record < string , unknown > , key ) => {
1150+ if ( key in allDataFromStorage ) {
1151+ // eslint-disable-next-line no-param-reassign
1152+ obj [ key ] = allDataFromStorage [ key ] ;
1153+ }
1154+ return obj ;
1155+ } , { } ) ;
1156+
1157+ const merged = utils . fastMerge ( defaultKeysFromStorage , defaultKeyStates , {
1158+ shouldRemoveNestedNulls : true ,
1159+ } ) . result ;
1160+ cache . merge ( merged ?? { } ) ;
1161+
1162+ // Notify subscribers about default key states so that any subscriber that connected
1163+ // before init (e.g. during module load) receives the merged default values immediately
1164+ for ( const [ key , value ] of Object . entries ( merged ?? { } ) ) {
1165+ keyChanged ( key , value ) ;
1166+ }
1167+ } )
1168+ . catch ( ( error ) => {
1169+ Logger . logAlert ( `Failed to load data from storage during init. The app will boot with default key states only. Error: ${ error } ` ) ;
1170+
1171+ // Populate the key index so getAllKeys() returns correct results for default keys.
1172+ // Without this, subscribers that check getAllKeys() would see an empty set even
1173+ // though we have default values in cache.
1174+ cache . setAllKeys ( Object . keys ( defaultKeyStates ) ) ;
1175+
1176+ // Boot with defaults so the app renders instead of deadlocking.
1177+ // Users will get a fresh-install experience but the app won't be bricked.
1178+ cache . merge ( defaultKeyStates ) ;
1179+
1180+ // Notify subscribers about default key states so that any subscriber that connected
1181+ // before init (e.g. during module load) receives the merged default values immediately
1182+ for ( const [ key , value ] of Object . entries ( defaultKeyStates ) ) {
1183+ keyChanged ( key , value ) ;
1184+ }
1185+ } ) ;
11271186}
11281187
11291188/**
0 commit comments