🚨 Latest Research:Tanstack npm Packages Compromised in Ongoing Mini Shai-Hulud Supply-Chain Attack.Learn More
Socket
Book a DemoSign in
Socket

react-native-onyx

Package Overview
Dependencies
Maintainers
1
Versions
347
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-native-onyx - npm Package Compare versions

Comparing version
3.0.65
to
3.0.66
+17
-4
dist/OnyxCache.d.ts

@@ -20,4 +20,2 @@ import type { ValueOf } from 'type-fest';

private storageMap;
/** Cache of complete collection data objects for O(1) retrieval */
private collectionData;
/**

@@ -32,2 +30,6 @@ * Captured pending tasks for already running storage methods

private recentlyAccessedKeys;
/** Frozen collection snapshots for structural sharing */
private collectionSnapshots;
/** Collections whose snapshots need rebuilding (lazy — rebuilt on next read) */
private dirtyCollections;
constructor();

@@ -92,3 +94,3 @@ /** Get all the storage keys */

captureTask(taskName: CacheTask, promise: Promise<OnyxValue<OnyxKey>>): Promise<OnyxValue<OnyxKey>>;
/** Check if the value has changed */
/** Check if the value has changed. Uses reference equality as a fast path, falls back to deep equality. */
hasValueChanged(key: OnyxKey, value: OnyxValue<OnyxKey>): boolean;

@@ -133,4 +135,15 @@ /**

/**
* Get all data for a collection key
* Rebuilds the frozen collection snapshot from current storageMap references.
* Uses the indexed collection->members map for O(collectionMembers) instead of O(totalKeys).
* Returns the previous snapshot reference when all member references are identical,
* preventing unnecessary re-renders in useSyncExternalStore.
*
* @param collectionKey - The collection key to rebuild
*/
private rebuildCollectionSnapshot;
/**
* Get all data for a collection key.
* Returns a frozen snapshot with structural sharing — safe to return by reference.
* Lazily rebuilds the snapshot if the collection was modified since the last read.
*/
getCollectionData(collectionKey: OnyxKey): Record<OnyxKey, OnyxValue<OnyxKey>> | undefined;

@@ -137,0 +150,0 @@ }

+125
-39

@@ -11,2 +11,8 @@ "use strict";

const OnyxKeys_1 = __importDefault(require("./OnyxKeys"));
/**
* Stable frozen empty object used as the canonical value for empty collections.
* Returning the same reference avoids unnecessary re-renders in useSyncExternalStore,
* which relies on === equality to detect changes.
*/
const FROZEN_EMPTY_COLLECTION = Object.freeze({});
// Task constants

@@ -32,6 +38,7 @@ const TASK = {

this.storageMap = {};
this.collectionData = {};
this.pendingPromises = new Map();
this.collectionSnapshots = new Map();
this.dirtyCollections = new Set();
// bind all public methods to prevent problems with `this`
(0, bindAll_1.default)(this, 'getAllKeys', 'get', 'hasCacheForKey', 'addKey', 'addNullishStorageKey', 'hasNullishStorageKey', 'clearNullishStorageKeys', 'set', 'drop', 'merge', 'hasPendingTask', 'getTaskPromise', 'captureTask', 'setAllKeys', 'setEvictionAllowList', 'isEvictableKey', 'removeLastAccessedKey', 'addLastAccessedKey', 'addEvictableKeysToRecentlyAccessedList', 'getKeyForEviction', 'setCollectionKeys', 'getCollectionData', 'hasValueChanged');
(0, bindAll_1.default)(this, 'getAllKeys', 'get', 'hasCacheForKey', 'addKey', 'addNullishStorageKey', 'hasNullishStorageKey', 'clearNullishStorageKeys', 'set', 'drop', 'merge', 'hasPendingTask', 'getTaskPromise', 'captureTask', 'setAllKeys', 'setEvictionAllowList', 'isEvictableKey', 'removeLastAccessedKey', 'addLastAccessedKey', 'addEvictableKeysToRecentlyAccessedList', 'getKeyForEviction', 'setCollectionKeys', 'hasValueChanged', 'getCollectionData');
}

@@ -96,7 +103,7 @@ /** Get all the storage keys */

const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
const oldValue = this.storageMap[key];
if (value === null || value === undefined) {
delete this.storageMap[key];
// Remove from collection data cache if it's a collection member
if (collectionKey && this.collectionData[collectionKey]) {
delete this.collectionData[collectionKey][key];
if (collectionKey && oldValue !== undefined) {
this.dirtyCollections.add(collectionKey);
}

@@ -106,8 +113,4 @@ return undefined;

this.storageMap[key] = value;
// Update collection data cache if this is a collection member
if (collectionKey) {
if (!this.collectionData[collectionKey]) {
this.collectionData[collectionKey] = {};
}
this.collectionData[collectionKey][key] = value;
if (collectionKey && oldValue !== value) {
this.dirtyCollections.add(collectionKey);
}

@@ -119,10 +122,9 @@ return value;

delete this.storageMap[key];
// Remove from collection data cache if this is a collection member
const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
if (collectionKey && this.collectionData[collectionKey]) {
delete this.collectionData[collectionKey][key];
if (collectionKey) {
this.dirtyCollections.add(collectionKey);
}
// If this is a collection key, clear its data
// If this is a collection key, clear its snapshot
if (OnyxKeys_1.default.isCollectionKey(key)) {
delete this.collectionData[key];
this.collectionSnapshots.delete(key);
}

@@ -140,14 +142,16 @@ this.storageKeys.delete(key);

}
this.storageMap = Object.assign({}, utils_1.default.fastMerge(this.storageMap, data, {
shouldRemoveNestedNulls: true,
objectRemovalMode: 'replace',
}).result);
const affectedCollections = new Set();
for (const [key, value] of Object.entries(data)) {
this.addKey(key);
const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
if (value === null || value === undefined) {
if (value === undefined) {
this.addNullishStorageKey(key);
// Remove from collection data cache if it's a collection member
if (collectionKey && this.collectionData[collectionKey]) {
delete this.collectionData[collectionKey][key];
// undefined means "no change" — skip storageMap modification
continue;
}
if (value === null) {
this.addNullishStorageKey(key);
delete this.storageMap[key];
if (collectionKey) {
affectedCollections.add(collectionKey);
}

@@ -157,11 +161,23 @@ }

this.nullishStorageKeys.delete(key);
// Update collection data cache if this is a collection member
// Per-key merge instead of spreading the entire storageMap
const existing = this.storageMap[key];
const merged = utils_1.default.fastMerge(existing, value, {
shouldRemoveNestedNulls: true,
objectRemovalMode: 'replace',
}).result;
// fastMerge is reference-stable: returns the original target when
// nothing changed, so a simple === check detects no-ops.
if (merged === existing) {
continue;
}
this.storageMap[key] = merged;
if (collectionKey) {
if (!this.collectionData[collectionKey]) {
this.collectionData[collectionKey] = {};
}
this.collectionData[collectionKey][key] = this.storageMap[key];
affectedCollections.add(collectionKey);
}
}
}
// Mark affected collections as dirty — snapshots will be lazily rebuilt on next read
for (const collectionKey of affectedCollections) {
this.dirtyCollections.add(collectionKey);
}
}

@@ -196,5 +212,8 @@ /**

}
/** Check if the value has changed */
/** Check if the value has changed. Uses reference equality as a fast path, falls back to deep equality. */
hasValueChanged(key, value) {
const currentValue = this.get(key);
const currentValue = this.storageMap[key];
if (currentValue === value) {
return false;
}
return !(0, fast_equals_1.deepEqual)(currentValue, value);

@@ -268,20 +287,87 @@ }

OnyxKeys_1.default.setCollectionKeys(collectionKeys);
// Initialize collection data for existing collection keys
// Initialize frozen snapshots for collection keys
for (const collectionKey of collectionKeys) {
if (this.collectionData[collectionKey]) {
if (!this.collectionSnapshots.has(collectionKey)) {
this.collectionSnapshots.set(collectionKey, Object.freeze({}));
}
}
}
/**
* Rebuilds the frozen collection snapshot from current storageMap references.
* Uses the indexed collection->members map for O(collectionMembers) instead of O(totalKeys).
* Returns the previous snapshot reference when all member references are identical,
* preventing unnecessary re-renders in useSyncExternalStore.
*
* @param collectionKey - The collection key to rebuild
*/
rebuildCollectionSnapshot(collectionKey) {
const previousSnapshot = this.collectionSnapshots.get(collectionKey);
const members = {};
let hasMemberChanges = false;
// Use the indexed forward lookup for O(collectionMembers) iteration.
// Falls back to scanning all storageKeys if the index isn't populated yet.
const memberKeys = OnyxKeys_1.default.getMembersOfCollection(collectionKey);
const keysToScan = memberKeys !== null && memberKeys !== void 0 ? memberKeys : this.storageKeys;
const needsPrefixCheck = !memberKeys;
for (const key of keysToScan) {
// When using the fallback path (scanning all storageKeys instead of the indexed
// forward lookup), skip keys that don't belong to this collection.
if (needsPrefixCheck && OnyxKeys_1.default.getCollectionKey(key) !== collectionKey) {
continue;
}
this.collectionData[collectionKey] = {};
const val = this.storageMap[key];
// Skip null/undefined values — they represent deleted or unset keys
// and should not be included in the frozen collection snapshot.
if (val !== undefined && val !== null) {
members[key] = val;
// Check if this member's reference changed from the old snapshot
if (!hasMemberChanges && (!previousSnapshot || previousSnapshot[key] !== val)) {
hasMemberChanges = true;
}
}
}
// Check if any members were removed from the previous snapshot.
// We can't rely on count comparison alone — if one key is removed and another added,
// the counts match but the snapshot content is different.
if (!hasMemberChanges && previousSnapshot) {
// eslint-disable-next-line no-restricted-syntax
for (const key in previousSnapshot) {
if (!(key in members)) {
hasMemberChanges = true;
break;
}
}
}
// If nothing actually changed, reuse the old snapshot reference.
// This is critical: useSyncExternalStore uses === to detect changes,
// so returning the same reference prevents unnecessary re-renders.
if (!hasMemberChanges && previousSnapshot) {
return;
}
Object.freeze(members);
this.collectionSnapshots.set(collectionKey, members);
}
/**
* Get all data for a collection key
* Get all data for a collection key.
* Returns a frozen snapshot with structural sharing — safe to return by reference.
* Lazily rebuilds the snapshot if the collection was modified since the last read.
*/
getCollectionData(collectionKey) {
const cachedCollection = this.collectionData[collectionKey];
if (!cachedCollection || Object.keys(cachedCollection).length === 0) {
if (this.dirtyCollections.has(collectionKey)) {
this.rebuildCollectionSnapshot(collectionKey);
this.dirtyCollections.delete(collectionKey);
}
const snapshot = this.collectionSnapshots.get(collectionKey);
if (utils_1.default.isEmptyObject(snapshot)) {
// We check storageKeys.size (not collection-specific keys) to distinguish
// "init complete, this collection is genuinely empty" from "init not done yet."
// During init, setAllKeys loads ALL keys at once — so if any key exists,
// the full storage picture is loaded and an empty collection is truly empty.
// Returning undefined before init prevents subscribers from seeing a false empty state.
if (this.storageKeys.size > 0) {
return FROZEN_EMPTY_COLLECTION;
}
return undefined;
}
// Return a shallow copy to ensure React detects changes when items are added/removed
return Object.assign({}, cachedCollection);
return snapshot;
}

@@ -288,0 +374,0 @@ }

@@ -112,3 +112,13 @@ "use strict";

function isEmptyObject(obj) {
return typeof obj === 'object' && Object.keys(obj || {}).length === 0;
if (typeof obj !== 'object') {
return false;
}
// Use for-in loop to avoid an unnecessary array allocation from Object.keys()
// eslint-disable-next-line no-restricted-syntax
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
return false;
}
}
return true;
}

@@ -115,0 +125,0 @@ /**

{
"name": "react-native-onyx",
"version": "3.0.65",
"version": "3.0.66",
"author": "Expensify, Inc.",

@@ -5,0 +5,0 @@ "homepage": "https://expensify.com",