Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skippable Collection Member IDs #604

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions API-INTERNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
<dt><a href="#getEvictionBlocklist">getEvictionBlocklist()</a></dt>
<dd><p>Getter - returns the eviction block list.</p>
</dd>
<dt><a href="#getSkippableCollectionMemberIDs">getSkippableCollectionMemberIDs()</a></dt>
<dd><p>Getter - TODO</p>
</dd>
<dt><a href="#setSkippableCollectionMemberIDs">setSkippableCollectionMemberIDs()</a></dt>
<dd><p>Setter - TODO</p>
</dd>
<dt><a href="#initStoreValues">initStoreValues(keys, initialKeyStates, safeEvictionKeys)</a></dt>
<dd><p>Sets the initial values for the Onyx store</p>
</dd>
Expand Down Expand Up @@ -53,7 +59,7 @@ The resulting collection will only contain items that are returned by the select
<dd><p>Checks to see if the subscriber&#39;s supplied key
is associated with a collection of keys.</p>
</dd>
<dt><a href="#splitCollectionMemberKey">splitCollectionMemberKey(key)</a> ⇒</dt>
<dt><a href="#splitCollectionMemberKey">splitCollectionMemberKey(key, collectionKey)</a> ⇒</dt>
<dd><p>Splits a collection member key into the collection key part and the ID part.</p>
</dd>
<dt><a href="#isKeyMatch">isKeyMatch()</a></dt>
Expand Down Expand Up @@ -187,6 +193,18 @@ Getter - returns the deffered init task.
## getEvictionBlocklist()
Getter - returns the eviction block list.

**Kind**: global function
<a name="getSkippableCollectionMemberIDs"></a>

## getSkippableCollectionMemberIDs()
Getter - TODO

**Kind**: global function
<a name="setSkippableCollectionMemberIDs"></a>

## setSkippableCollectionMemberIDs()
Setter - TODO

**Kind**: global function
<a name="initStoreValues"></a>

Expand Down Expand Up @@ -268,7 +286,7 @@ is associated with a collection of keys.
**Kind**: global function
<a name="splitCollectionMemberKey"></a>

## splitCollectionMemberKey(key) ⇒
## splitCollectionMemberKey(key, collectionKey) ⇒
Splits a collection member key into the collection key part and the ID part.

**Kind**: global function
Expand All @@ -278,6 +296,7 @@ or throws an Error if the key is not a collection one.
| Param | Description |
| --- | --- |
| key | The collection member key to split. |
| collectionKey | The collection key of the `key` param that can be passed in advance to optimize the function. |

<a name="isKeyMatch"></a>

Expand Down
113 changes: 97 additions & 16 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function init({
shouldSyncMultipleInstances = Boolean(global.localStorage),
debugSetState = false,
enablePerformanceMetrics = false,
skippableCollectionMemberIDs = [],
}: InitOptions): void {
if (enablePerformanceMetrics) {
GlobalSettings.setPerformanceMetricsEnabled(true);
Expand All @@ -52,6 +53,8 @@ function init({

Storage.init();

OnyxUtils.setSkippableCollectionMemberIDs(new Set(skippableCollectionMemberIDs));

if (shouldSyncMultipleInstances) {
Storage.keepInstancesSync?.((key, value) => {
const prevValue = cache.get(key, false) as OnyxValue<typeof key>;
Expand Down Expand Up @@ -134,6 +137,18 @@ function set<TKey extends OnyxKey>(key: TKey, value: OnyxSetInput<TKey>): Promis
delete OnyxUtils.getMergeQueue()[key];
}

const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs();
if (skippableCollectionMemberIDs.size) {
try {
const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key);
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
return Promise.resolve();
}
} catch (e) {
// Key is not a collection one or something went wrong during split, so we proceed with the function's logic.
}
}

// Onyx.set will ignore `undefined` values as inputs, therefore we can return early.
if (value === undefined) {
return Promise.resolve();
Expand Down Expand Up @@ -196,7 +211,26 @@ function set<TKey extends OnyxKey>(key: TKey, value: OnyxSetInput<TKey>): Promis
* @param data object keyed by ONYXKEYS and the values to set
*/
function multiSet(data: OnyxMultiSetInput): Promise<void> {
const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(data, true);
let newData = data;

const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs();
if (skippableCollectionMemberIDs.size) {
newData = Object.keys(newData).reduce((result: OnyxMultiSetInput, key) => {
try {
const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key);
// eslint-disable-next-line no-param-reassign
result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? newData[key] : null;
fabioh8010 marked this conversation as resolved.
Show resolved Hide resolved
} catch {
// Key is not a collection one or something went wrong during split, so we assign the data to result anyway.
// eslint-disable-next-line no-param-reassign
result[key] = newData[key];
}

return result;
}, {});
}

const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);

const updatePromises = keyValuePairsToSet.map(([key, value]) => {
const prevValue = cache.get(key, false);
Expand All @@ -207,9 +241,9 @@ function multiSet(data: OnyxMultiSetInput): Promise<void> {
});

return Storage.multiSet(keyValuePairsToSet)
.catch((error) => OnyxUtils.evictStorageAndRetry(error, multiSet, data))
.catch((error) => OnyxUtils.evictStorageAndRetry(error, multiSet, newData))
.then(() => {
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, data);
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData);
return Promise.all(updatePromises);
})
.then(() => undefined);
Expand All @@ -232,6 +266,19 @@ function multiSet(data: OnyxMultiSetInput): Promise<void> {
* Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'}
*/
function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>): Promise<void> {
const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs();
if (skippableCollectionMemberIDs.size) {
try {
const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key);
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
// eslint-disable-next-line no-param-reassign
changes = undefined;
}
} catch (e) {
// Key is not a collection one or something went wrong during split, so we proceed with the function's logic.
}
}

const mergeQueue = OnyxUtils.getMergeQueue();
const mergeQueuePromise = OnyxUtils.getMergeQueuePromise();

Expand Down Expand Up @@ -346,19 +393,36 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
return Promise.resolve();
}

const mergedCollection: OnyxInputKeyValueMapping = collection;
let resultCollection: OnyxInputKeyValueMapping = collection;

// Confirm all the collection keys belong to the same parent
const mergedCollectionKeys = Object.keys(mergedCollection);
const mergedCollectionKeys = Object.keys(resultCollection);
if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, mergedCollectionKeys)) {
return Promise.resolve();
}

const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs();
if (skippableCollectionMemberIDs.size) {
resultCollection = Object.keys(resultCollection).reduce((result: OnyxInputKeyValueMapping, key) => {
try {
const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key, collectionKey);
// eslint-disable-next-line no-param-reassign
result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null;
} catch {
// Something went wrong during split, so we assign the data to result anyway.
// eslint-disable-next-line no-param-reassign
result[key] = resultCollection[key];
}

return result;
}, {});
}

return OnyxUtils.getAllKeys()
.then((persistedKeys) => {
// Split to keys that exist in storage and keys that don't
const keys = mergedCollectionKeys.filter((key) => {
if (mergedCollection[key] === null) {
if (resultCollection[key] === null) {
OnyxUtils.remove(key);
return false;
}
Expand All @@ -370,13 +434,13 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
const cachedCollectionForExistingKeys = OnyxUtils.getCachedCollection(collectionKey, existingKeys);

const existingKeyCollection = existingKeys.reduce((obj: OnyxInputKeyValueMapping, key) => {
const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(mergedCollection[key], cachedCollectionForExistingKeys[key]);
const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]);
if (!isCompatible) {
Logger.logAlert(logMessages.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType));
return obj;
}
// eslint-disable-next-line no-param-reassign
obj[key] = mergedCollection[key];
obj[key] = resultCollection[key];
return obj;
}, {}) as Record<OnyxKey, OnyxInput<TKey>>;

Expand All @@ -385,7 +449,7 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
if (persistedKeys.has(key)) {
return;
}
newCollection[key] = mergedCollection[key];
newCollection[key] = resultCollection[key];
});

// When (multi-)merging the values with the existing values in storage,
Expand Down Expand Up @@ -424,9 +488,9 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
});

return Promise.all(promises)
.catch((error) => OnyxUtils.evictStorageAndRetry(error, mergeCollection, collectionKey, mergedCollection))
.catch((error) => OnyxUtils.evictStorageAndRetry(error, mergeCollection, collectionKey, resultCollection))
.then(() => {
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE_COLLECTION, undefined, mergedCollection);
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE_COLLECTION, undefined, resultCollection);
return promiseUpdate;
});
})
Expand Down Expand Up @@ -730,8 +794,6 @@ function update(data: OnyxUpdate[]): Promise<void> {
.then(() => undefined);
}

type BaseCollection<TMap> = Record<string, TMap | null>;

/**
* Sets a collection by replacing all existing collection members with new values.
* Any existing collection members not included in the new data will be removed.
Expand All @@ -745,16 +807,35 @@ type BaseCollection<TMap> = Record<string, TMap | null>;
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
* @param collection Object collection keyed by individual collection member keys and values
*/
function setCollection<TKey extends CollectionKeyBase, TMap extends string>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TMap>): Promise<void> {
const newCollectionKeys = Object.keys(collection);
function setCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>): Promise<void> {
let resultCollection: OnyxInputKeyValueMapping = collection;

// Confirm all the collection keys belong to the same parent
const newCollectionKeys = Object.keys(resultCollection);
if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, newCollectionKeys)) {
Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`);
return Promise.resolve();
}

const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs();
if (skippableCollectionMemberIDs.size) {
resultCollection = Object.keys(resultCollection).reduce((result: OnyxInputKeyValueMapping, key) => {
try {
const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key, collectionKey);
// eslint-disable-next-line no-param-reassign
result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null;
} catch {
// Something went wrong during split, so we assign the data to result anyway.
// eslint-disable-next-line no-param-reassign
result[key] = resultCollection[key];
}

return result;
}, {});
}

return OnyxUtils.getAllKeys().then((persistedKeys) => {
const mutableCollection: BaseCollection<TMap> = {...collection};
const mutableCollection: OnyxInputKeyValueMapping = {...resultCollection};

persistedKeys.forEach((key) => {
if (!key.startsWith(collectionKey)) {
Expand Down
57 changes: 55 additions & 2 deletions lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ let lastSubscriptionID = 0;
// Connections can be made before `Onyx.init`. They would wait for this task before resolving
const deferredInitTask = createDeferredTask();

let skippableCollectionMemberIDs = new Set<string>();

function getSnapshotKey(): OnyxKey | null {
return snapshotKey;
}
Expand Down Expand Up @@ -127,6 +129,20 @@ function getEvictionBlocklist(): Record<OnyxKey, string[] | undefined> {
return evictionBlocklist;
}

/**
* Getter - TODO
*/
function getSkippableCollectionMemberIDs(): Set<string> {
return skippableCollectionMemberIDs;
}

/**
* Setter - TODO
*/
function setSkippableCollectionMemberIDs(ids: Set<string>): void {
skippableCollectionMemberIDs = ids;
}

/**
* Sets the initial values for the Onyx store
*
Expand Down Expand Up @@ -257,6 +273,18 @@ function get<TKey extends OnyxKey, TValue extends OnyxValue<TKey>>(key: TKey): P
// Otherwise retrieve the value from storage and capture a promise to aid concurrent usages
const promise = Storage.getItem(key)
.then((val) => {
if (skippableCollectionMemberIDs.size) {
try {
const [, collectionMemberID] = splitCollectionMemberKey(key);
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
// eslint-disable-next-line no-param-reassign
val = undefined as OnyxValue<TKey>;
}
} catch (e) {
// Key is not a collection one or something went wrong during split, so we proceed with the function's logic.
}
}

if (val === undefined) {
cache.addNullishStorageKey(key);
return undefined;
Expand Down Expand Up @@ -335,6 +363,17 @@ function multiGet<TKey extends OnyxKey>(keys: CollectionKeyBase[]): Promise<Map<
// temp object is used to merge the missing data into the cache
const temp: OnyxCollection<KeyValueMapping[TKey]> = {};
values.forEach(([key, value]) => {
if (skippableCollectionMemberIDs.size) {
try {
const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key);
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
return;
}
} catch (e) {
// Key is not a collection one or something went wrong during split, so we proceed with the function's logic.
}
}

dataMap.set(key, value as OnyxValue<TKey>);
temp[key] = value as OnyxValue<TKey>;
});
Expand Down Expand Up @@ -419,11 +458,23 @@ function isCollectionMemberKey<TCollectionKey extends CollectionKeyBase>(collect
/**
* Splits a collection member key into the collection key part and the ID part.
* @param key - The collection member key to split.
* @param collectionKey - The collection key of the `key` param that can be passed in advance to optimize the function.
* @returns A tuple where the first element is the collection part and the second element is the ID part,
* or throws an Error if the key is not a collection one.
*/
function splitCollectionMemberKey<TKey extends CollectionKey, CollectionKeyType = TKey extends `${infer Prefix}_${string}` ? `${Prefix}_` : never>(key: TKey): [CollectionKeyType, string] {
const collectionKey = getCollectionKey(key);
function splitCollectionMemberKey<TKey extends CollectionKey, CollectionKeyType = TKey extends `${infer Prefix}_${string}` ? `${Prefix}_` : never>(
key: TKey,
collectionKey?: string,
): [CollectionKeyType, string] {
if (collectionKey && !isCollectionMemberKey(collectionKey, key, collectionKey.length)) {
throw new Error(`Invalid '${collectionKey}' collection key provided, it isn't compatible with '${key}' key.`);
}

if (!collectionKey) {
// eslint-disable-next-line no-param-reassign
collectionKey = getCollectionKey(key);
}

return [collectionKey as CollectionKeyType, key.slice(collectionKey.length)];
}

Expand Down Expand Up @@ -1418,6 +1469,8 @@ const OnyxUtils = {
subscribeToKey,
unsubscribeFromKey,
getEvictionBlocklist,
getSkippableCollectionMemberIDs,
setSkippableCollectionMemberIDs,
};

GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {
Expand Down
6 changes: 6 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ type InitOptions = {
* @default false
*/
enablePerformanceMetrics?: boolean;

/**
* Array of collection member IDs which updates will be ignored when using Onyx methods.
* Additionally, any subscribers from these keys to won't receive any data from Onyx.
*/
skippableCollectionMemberIDs?: string[];
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Loading
Loading