[WIP][POC] Flip the subscription layer onto OnyxStore (atomic swap) - #834
[WIP][POC] Flip the subscription layer onto OnyxStore (atomic swap)#834fabioh8010 wants to merge 11 commits into
Conversation
Introduce lib/OnyxStore.ts: a single listener registry (keyListeners Map<key, Set<listener>>) with subscribe / notifyKey / notifyCollection / getState / hasListenersForKey / clearAll. Built on the existing structural-sharing cache (cache.getCollectionData frozen snapshots). This module is inert — nothing imports it yet. The subscription and notification paths (Onyx.connect, useOnyx, OnyxUtils.notify*) are wired onto it in a later change. Adding it alone has zero behavioral impact. Includes tests/unit/OnyxStoreTest.ts (20 tests) covering exact-key and collection-snapshot routing, ref-equality member skips, hasListenersForKey, clearAll, and listener error isolation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire useOnyx, Onyx.connect/disconnect, and the OnyxUtils notify path onto the OnyxStore registry. useOnyx is rebuilt on useSyncExternalStore reading the eager, structurally-shared cache via onyxStore.getState — it renders once (no loading->loaded second render) and bails out via === on the stable cached reference. Delete OnyxConnectionManager and OnyxSnapshotCache (and their unit + perf tests). Also remove the now-defunct reuseConnection option (connection pooling is gone). The `loading` status value is retained for now (always 'loaded' in practice) and removed in a follow-up; OnyxStore stays trimmed (no subscribeState) since useOnyxState is a separate proposal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the store-based useOnyx on React's useSyncExternalStoreWithSelector (use-sync-external-store/with-selector) with deepEqual as the equality fn. Its committed-value dedup collapses content-equal selections to a stable reference and survives the selector function's identity churning every render — so consumers can pass inline selectors that close over freshly allocated arrays/objects without stabilizing the inputs (no more useStableArrayReference). No-selector subscriptions keep the Object.is fast-path since the raw cache value is already reference-stable. Adds use-sync-external-store + @types/use-sync-external-store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4d023b4d41
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const isLoading = connectedKeyRef.current !== key && OnyxUtils.hasPendingMergeForKey(key); | ||
| const loadingStatus: FetchStatus = isLoading ? 'loading' : 'loaded'; |
There was a problem hiding this comment.
Re-render when the initial pending merge settles
When this hook mounts or switches keys while a no-op merge is pending—for example, merging data already present in the cache—isLoading makes it return [undefined, {status: 'loading'}], but merge completion does not necessarily notify the store because broadcastUpdate() returns when hasChanged is false. The effect only mutates connectedKeyRef without scheduling a render, so the hook can remain permanently blank and loading. The merge-settlement path needs to trigger a render even when the cached snapshot does not change.
Useful? React with 👍 / 👎.
Details
Related Issues
GH_LINK
Linked E/App PR
Automated Tests
Manual Tests
Author Checklist
### Related Issuessection above### Linked E/App PRsection above, and verified this change against it (E/App CI passed and manual testing completed)TestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Avataris modified, I verified thatAvataris working as expected in all cases)mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari