Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,24 @@ function subscribeToKey<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKe
callbackToStateMapping[subscriptionID] = mapping as CallbackToStateMapping<OnyxKey>;
callbackToStateMapping[subscriptionID].subscriptionID = subscriptionID;

// If the subscriber is attempting to connect to a collection member whose ID is skippable (e.g. "undefined", "null", etc.)
// we suppress wiring the subscription fully to avoid unnecessary callback emissions such as for "report_undefined".
// We still return a valid subscriptionID so callers can disconnect safely.
try {
const skippableIDs = getSkippableCollectionMemberIDs();
if (skippableIDs.size) {
const [, collectionMemberID] = OnyxKeys.splitCollectionMemberKey(mapping.key);
if (skippableIDs.has(collectionMemberID)) {
// Clean up the provisional mapping to avoid retaining unused subscribers.
cache.addNullishStorageKey(mapping.key);
delete callbackToStateMapping[subscriptionID];
return subscriptionID;
}
}
} catch (e) {
// Not a collection member key, proceed as usual.
}

// When keyChanged is called, a key is passed and the method looks through all the Subscribers in callbackToStateMapping for the matching key to get the subscriptionID
// to avoid having to loop through all the Subscribers all the time (even when just one connection belongs to one key),
// We create a mapping from key to lists of subscriptionIDs to access the specific list of subscriptionIDs.
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/onyxUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,63 @@ describe('OnyxUtils', () => {

afterEach(() => jest.clearAllMocks());

describe('skippable member subscriptions', () => {
const BASE = ONYXKEYS.COLLECTION.TEST_KEY;

beforeEach(() => {
// Enable skipping of undefined member IDs for these tests
OnyxUtils.setSkippableCollectionMemberIDs(new Set(['undefined']));
});

afterEach(() => {
// Restore to no skippable IDs to avoid affecting other tests
OnyxUtils.setSkippableCollectionMemberIDs(new Set());
});

it('does not emit initial callback for report_undefined member', async () => {
const key = `${BASE}undefined`;
const callback = jest.fn();
Onyx.connect({key, callback});

// Flush async subscription flow
await act(async () => waitForPromisesToResolve());

// No initial data should be sent for a skippable member
expect(callback).not.toHaveBeenCalled();
});

it('still emits for valid member keys', async () => {
const key = `${BASE}123`;
await Onyx.set(key, {id: 123});

const callback = jest.fn();
Onyx.connect({key, callback});
await act(async () => waitForPromisesToResolve());
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith({id: 123}, key);
});

it('omits skippable members from base collection', async () => {
const undefinedKey = `${BASE}undefined`;
const validKey = `${BASE}1`;

await Onyx.set(undefinedKey, {bad: true});
await Onyx.set(validKey, {ok: true});

let received: Record<string, unknown> | undefined;
Onyx.connect({
key: BASE,
waitForCollectionCallback: true,
callback: (value) => {
received = value as Record<string, unknown>;
},
});
await act(async () => waitForPromisesToResolve());
expect(received).toEqual({[validKey]: {ok: true}});
expect(Object.keys(received ?? {})).not.toContain(undefinedKey);
});
});

describe('partialSetCollection', () => {
beforeEach(() => {
Onyx.clear();
Expand Down
Loading