Skip to content

Commit d88c9dd

Browse files
committed
Refactor and move getReplacePatches() logic to fastMerge() / mergeObject()
1 parent bd928cd commit d88c9dd

File tree

12 files changed

+166
-79
lines changed

12 files changed

+166
-79
lines changed

lib/Onyx.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
319319
if (!validChanges.length) {
320320
return Promise.resolve();
321321
}
322-
const batchedDeltaChanges = OnyxUtils.batchMergeChanges(validChanges);
322+
const batchedDeltaChanges = OnyxUtils.batchMergeChanges(validChanges).result;
323323

324324
// Case (1): When there is no existing value in storage, we want to set the value instead of merge it.
325325
// Case (2): The presence of a top-level `null` in the merge queue instructs us to drop the whole existing value.
@@ -391,7 +391,11 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
391391
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
392392
* @param collection Object collection keyed by individual collection member keys and values
393393
*/
394-
function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>): Promise<void> {
394+
function mergeCollection<TKey extends CollectionKeyBase, TMap>(
395+
collectionKey: TKey,
396+
collection: OnyxMergeCollectionInput<TKey, TMap>,
397+
mergeReplaceNullPatches?: MixedOperationsQueue['mergeReplaceNullPatches'],
398+
): Promise<void> {
395399
if (!OnyxUtils.isValidNonEmptyCollectionForMerge(collection)) {
396400
Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
397401
return Promise.resolve();
@@ -476,7 +480,7 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
476480
// New keys will be added via multiSet while existing keys will be updated using multiMerge
477481
// This is because setting a key that doesn't exist yet with multiMerge will throw errors
478482
if (keyValuePairsForExistingCollection.length > 0) {
479-
promises.push(Storage.multiMerge(keyValuePairsForExistingCollection));
483+
promises.push(Storage.multiMerge(keyValuePairsForExistingCollection, mergeReplaceNullPatches));
480484
}
481485

482486
if (keyValuePairsForNewCollection.length > 0) {
@@ -773,29 +777,36 @@ function update(data: OnyxUpdate[]): Promise<void> {
773777
const batchedChanges = OnyxUtils.batchMergeChanges(operations);
774778
if (operations[0] === null) {
775779
// eslint-disable-next-line no-param-reassign
776-
queue.set[key] = batchedChanges;
780+
queue.set[key] = batchedChanges.result;
777781
} else {
778782
// eslint-disable-next-line no-param-reassign
779-
queue.merge[key] = batchedChanges;
783+
queue.merge[key] = batchedChanges.result;
784+
if (batchedChanges.replaceNullPatches.length > 0) {
785+
// eslint-disable-next-line no-param-reassign
786+
queue.mergeReplaceNullPatches[key] = batchedChanges.replaceNullPatches;
787+
}
780788
}
781789
return queue;
782790
},
783791
{
784792
merge: {},
793+
mergeReplaceNullPatches: {},
785794
set: {},
786795
},
787796
);
788797

789798
if (!utils.isEmptyObject(batchedCollectionUpdates.merge)) {
790-
promises.push(() => mergeCollection(collectionKey, batchedCollectionUpdates.merge as Collection<CollectionKey, unknown, unknown>));
799+
promises.push(() =>
800+
mergeCollection(collectionKey, batchedCollectionUpdates.merge as Collection<CollectionKey, unknown, unknown>, batchedCollectionUpdates.mergeReplaceNullPatches),
801+
);
791802
}
792803
if (!utils.isEmptyObject(batchedCollectionUpdates.set)) {
793804
promises.push(() => multiSet(batchedCollectionUpdates.set));
794805
}
795806
});
796807

797808
Object.entries(updateQueue).forEach(([key, operations]) => {
798-
const batchedChanges = OnyxUtils.batchMergeChanges(operations);
809+
const batchedChanges = OnyxUtils.batchMergeChanges(operations).result;
799810

800811
if (operations[0] === null) {
801812
promises.push(() => set(key, batchedChanges));

lib/OnyxCache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ class OnyxCache {
164164
throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs');
165165
}
166166

167-
this.storageMap = {...utils.fastMerge(this.storageMap, data, true, false, true)};
167+
this.storageMap = {...utils.fastMerge(this.storageMap, data, true, false, true).result};
168168

169169
Object.entries(data).forEach(([key, value]) => {
170170
this.addKey(key);

lib/OnyxUtils.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
OnyxValue,
2929
Selector,
3030
} from './types';
31+
import type {FastMergeResult} from './utils';
3132
import utils from './utils';
3233
import type {WithOnyxState} from './withOnyx/types';
3334
import type {DeferredTask} from './createDeferredTask';
@@ -1264,29 +1265,42 @@ function applyMerge<TValue extends OnyxInput<OnyxKey> | undefined, TChange exten
12641265

12651266
if (changes.some((change) => change && typeof change === 'object')) {
12661267
// Object values are then merged one after the other
1267-
return changes.reduce((modifiedData, change) => utils.fastMerge(modifiedData, change, true, false, true), (existingValue || {}) as TChange);
1268+
return changes.reduce((modifiedData, change) => utils.fastMerge(modifiedData, change, true, false, true).result, (existingValue || {}) as TChange);
12681269
}
12691270

12701271
// If we have anything else we can't merge it so we'll
12711272
// simply return the last value that was queued
12721273
return lastChange as TChange;
12731274
}
12741275

1275-
function batchMergeChanges<TChange extends OnyxInput<OnyxKey> | undefined>(changes: TChange[]): TChange {
1276+
function batchMergeChanges<TChange extends OnyxInput<OnyxKey> | undefined>(changes: TChange[]): FastMergeResult<TChange> {
12761277
const lastChange = changes?.at(-1);
12771278

12781279
if (Array.isArray(lastChange)) {
1279-
return lastChange;
1280+
return {result: lastChange, replaceNullPatches: []};
12801281
}
12811282

12821283
if (changes.some((change) => change && typeof change === 'object')) {
12831284
// Object values are then merged one after the other
1284-
return changes.reduce((modifiedData, change) => utils.fastMerge(modifiedData, change, false, true, false), {} as TChange);
1285+
return changes.reduce<FastMergeResult<TChange>>(
1286+
(modifiedData, change) => {
1287+
const fastMergeResult = utils.fastMerge(modifiedData.result, change, false, true, false);
1288+
// eslint-disable-next-line no-param-reassign
1289+
modifiedData.result = fastMergeResult.result;
1290+
// eslint-disable-next-line no-param-reassign
1291+
modifiedData.replaceNullPatches = [...modifiedData.replaceNullPatches, ...fastMergeResult.replaceNullPatches];
1292+
return modifiedData;
1293+
},
1294+
{
1295+
result: {} as TChange,
1296+
replaceNullPatches: [],
1297+
},
1298+
);
12851299
}
12861300

12871301
// If we have anything else we can't merge it so we'll
12881302
// simply return the last value that was queued
1289-
return lastChange as TChange;
1303+
return {result: lastChange as TChange, replaceNullPatches: []};
12901304
}
12911305

12921306
/**
@@ -1296,7 +1310,7 @@ function initializeWithDefaultKeyStates(): Promise<void> {
12961310
return Storage.multiGet(Object.keys(defaultKeyStates)).then((pairs) => {
12971311
const existingDataAsObject = Object.fromEntries(pairs);
12981312

1299-
const merged = utils.fastMerge(existingDataAsObject, defaultKeyStates, true, false, false);
1313+
const merged = utils.fastMerge(existingDataAsObject, defaultKeyStates, true, false, false).result;
13001314
cache.merge(merged ?? {});
13011315

13021316
Object.entries(merged ?? {}).forEach(([key, value]) => keyChanged(key, value, existingDataAsObject));

lib/storage/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ const storage: Storage = {
131131
* Multiple merging of existing and new values in a batch
132132
* This function also removes all nested null values from an object.
133133
*/
134-
multiMerge: (pairs) =>
134+
multiMerge: (pairs, mergeReplaceNullPatches) =>
135135
tryOrDegradePerformance(() => {
136-
const promise = provider.multiMerge(pairs);
136+
const promise = provider.multiMerge(pairs, mergeReplaceNullPatches);
137137

138138
if (shouldKeepInstancesSync) {
139139
return promise.then(() => InstanceSync.multiMerge(pairs.map((pair) => pair[0])));

lib/storage/providers/IDBKeyValProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const provider: StorageProvider = {
4949

5050
const upsertMany = pairsWithoutNull.map(([key, value], index) => {
5151
const prev = values[index];
52-
const newValue = utils.fastMerge(prev as Record<string, unknown>, value as Record<string, unknown>, true, false, true);
52+
const newValue = utils.fastMerge(prev as Record<string, unknown>, value as Record<string, unknown>, true, false, true).result;
5353
return promisifyRequest(store.put(newValue, key));
5454
});
5555
return Promise.all(upsertMany);

lib/storage/providers/MemoryOnlyProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const provider: StorageProvider = {
8686
multiMerge(pairs) {
8787
_.forEach(pairs, ([key, value]) => {
8888
const existingValue = store[key] as Record<string, unknown>;
89-
const newValue = utils.fastMerge(existingValue, value as Record<string, unknown>, true, false, true) as OnyxValue<OnyxKey>;
89+
const newValue = utils.fastMerge(existingValue, value as Record<string, unknown>, true, false, true).result as OnyxValue<OnyxKey>;
9090

9191
set(key, newValue);
9292
});

lib/storage/providers/SQLiteProvider.ts

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
* The SQLiteStorage provider stores everything in a key/value store by
33
* converting the value to a JSON string
44
*/
5+
import {getFreeDiskStorage} from 'react-native-device-info';
56
import type {BatchQueryResult, QuickSQLiteConnection, SQLBatchTuple} from 'react-native-quick-sqlite';
67
import {open} from 'react-native-quick-sqlite';
7-
import {getFreeDiskStorage} from 'react-native-device-info';
8-
import type StorageProvider from './types';
8+
import type {FastMergeReplaceNullPatch} from '../../utils';
99
import utils from '../../utils';
10+
import type StorageProvider from './types';
1011
import type {KeyList, KeyValuePairList} from './types';
11-
import type {OnyxKey, OnyxValue} from '../../types';
1212

1313
const DB_NAME = 'OnyxDB';
1414
let db: QuickSQLiteConnection;
@@ -18,47 +18,13 @@ function replacer(key: string, value: unknown) {
1818
return value;
1919
}
2020

21-
type JSONReplacePatch = [string, string[], any];
22-
23-
function getReplacePatches(storageKey: string, value: any): JSONReplacePatch[] {
24-
const patches: JSONReplacePatch[] = [];
25-
26-
// eslint-disable-next-line rulesdir/prefer-early-return
27-
function recurse(obj: any, path: string[] = []) {
28-
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
29-
if (obj.ONYX_INTERNALS__REPLACE_OBJECT_MARK) {
30-
const copy = {...obj};
31-
delete copy.ONYX_INTERNALS__REPLACE_OBJECT_MARK;
32-
33-
patches.push([storageKey, [...path], copy]);
34-
return;
35-
}
36-
37-
// eslint-disable-next-line guard-for-in, no-restricted-syntax
38-
for (const key in obj) {
39-
recurse(obj[key], [...path, key]);
40-
}
41-
}
42-
}
43-
44-
recurse(value);
45-
return patches;
46-
}
47-
48-
function generateJSONReplaceSQLBatch(patches: JSONReplacePatch[]): [string, string[][]] {
49-
const sql = `
50-
UPDATE keyvaluepairs
51-
SET valueJSON = JSON_REPLACE(valueJSON, :jsonPath, JSON(:value))
52-
WHERE record_key = :key;
53-
`;
54-
55-
const queryArguments = patches.map(([key, pathArray, value]) => {
21+
function generateJSONReplaceSQLQueries(key: string, patches: FastMergeReplaceNullPatch[]): string[][] {
22+
const queries = patches.map(([pathArray, value]) => {
5623
const jsonPath = `$.${pathArray.join('.')}`;
57-
// return {key, jsonPath, value: JSON.stringify(value)};
5824
return [jsonPath, JSON.stringify(value), key];
5925
});
6026

61-
return [sql.trim(), queryArguments];
27+
return queries;
6228
}
6329

6430
const provider: StorageProvider = {
@@ -108,7 +74,7 @@ const provider: StorageProvider = {
10874
}
10975
return db.executeBatchAsync([['REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, json(?));', stringifiedPairs]]);
11076
},
111-
multiMerge(pairs) {
77+
multiMerge(pairs, mergeReplaceNullPatches) {
11278
const commands: SQLBatchTuple[] = [];
11379

11480
const patchQuery = `INSERT INTO keyvaluepairs (record_key, valueJSON)
@@ -124,16 +90,20 @@ const provider: StorageProvider = {
12490
const replaceQueryArguments: string[][] = [];
12591

12692
const nonNullishPairs = pairs.filter((pair) => pair[1] !== undefined);
93+
12794
// eslint-disable-next-line @typescript-eslint/prefer-for-of
12895
for (let i = 0; i < nonNullishPairs.length; i++) {
12996
const pair = nonNullishPairs[i];
13097
const value = JSON.stringify(pair[1], replacer);
13198
patchQueryArguments.push([pair[0], value]);
13299

133-
const patches = getReplacePatches(pair[0], pair[1]);
134-
const [sql, args] = generateJSONReplaceSQLBatch(patches);
135-
if (args.length > 0) {
136-
replaceQueryArguments.push(...args);
100+
const patches = mergeReplaceNullPatches?.[pair[0]] ?? [];
101+
if (patches.length > 0) {
102+
const queries = generateJSONReplaceSQLQueries(pair[0], patches);
103+
104+
if (queries.length > 0) {
105+
replaceQueryArguments.push(...queries);
106+
}
137107
}
138108
}
139109

lib/storage/providers/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {BatchQueryResult, QueryResult} from 'react-native-quick-sqlite';
2-
import type {OnyxKey, OnyxValue} from '../../types';
2+
import type {MixedOperationsQueue, OnyxKey, OnyxValue} from '../../types';
33

44
type KeyValuePair = [OnyxKey, OnyxValue<OnyxKey>];
55
type KeyList = OnyxKey[];
@@ -39,7 +39,7 @@ type StorageProvider = {
3939
/**
4040
* Multiple merging of existing and new values in a batch
4141
*/
42-
multiMerge: (pairs: KeyValuePairList) => Promise<BatchQueryResult | IDBValidKey[] | void>;
42+
multiMerge: (pairs: KeyValuePairList, mergeReplaceNullPatches?: MixedOperationsQueue['mergeReplaceNullPatches']) => Promise<BatchQueryResult | IDBValidKey[] | void>;
4343

4444
/**
4545
* Merges an existing value with a new one

lib/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {BuiltIns} from 'type-fest/source/internal';
33
import type OnyxUtils from './OnyxUtils';
44
import type {WithOnyxInstance, WithOnyxState} from './withOnyx/types';
55
import type {OnyxMethod} from './OnyxUtils';
6+
import type {FastMergeReplaceNullPatch} from './utils';
67

78
/**
89
* Utility type that excludes `null` from the type `TValue`.
@@ -490,6 +491,7 @@ type GenericFunction = (...args: any[]) => any;
490491
*/
491492
type MixedOperationsQueue = {
492493
merge: OnyxInputKeyValueMapping;
494+
mergeReplaceNullPatches: {[TKey in OnyxKey]: FastMergeReplaceNullPatch[]};
493495
set: OnyxInputKeyValueMapping;
494496
};
495497

0 commit comments

Comments
 (0)