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

[DRAFT] Prototype use of autotracking to optimize subscription behavior #2047

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
6 changes: 3 additions & 3 deletions jest.config.js
Expand Up @@ -51,8 +51,8 @@ const nextEntryConfig = {
module.exports = {
projects: [
tsStandardConfig,
rnConfig,
standardReact17Config,
nextEntryConfig,
//rnConfig,
//standardReact17Config,
//nextEntryConfig,
],
}
2 changes: 2 additions & 0 deletions src/components/Context.ts
Expand Up @@ -3,6 +3,7 @@ import type { Context } from 'react'
import type { Action, AnyAction, Store } from 'redux'
import type { Subscription } from '../utils/Subscription'
import type { CheckFrequency } from '../hooks/useSelector'
import type { Node } from '../utils/autotracking/tracking'

export interface ReactReduxContextValue<
SS = any,
Expand All @@ -13,6 +14,7 @@ export interface ReactReduxContextValue<
getServerState?: () => SS
stabilityCheck: CheckFrequency
noopCheck: CheckFrequency
trackingNode: Node<Record<string, unknown>>
}

const ContextKey = Symbol.for(`react-redux-context`)
Expand Down
14 changes: 11 additions & 3 deletions src/components/Provider.tsx
Expand Up @@ -6,6 +6,7 @@ import { createSubscription } from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import type { Action, AnyAction, Store } from 'redux'
import type { CheckFrequency } from '../hooks/useSelector'
import { createNode, updateNode } from '../utils/autotracking/proxy'

export interface ProviderProps<A extends Action = AnyAction, S = unknown> {
/**
Expand Down Expand Up @@ -42,14 +43,21 @@ function Provider<A extends Action = AnyAction, S = unknown>({
stabilityCheck = 'once',
noopCheck = 'once',
}: ProviderProps<A, S>) {
const contextValue = React.useMemo(() => {
const subscription = createSubscription(store)
const contextValue: ReactReduxContextValue = React.useMemo(() => {
const trackingNode = createNode(store.getState() as any)
//console.log('Created tracking node: ', trackingNode)
const subscription = createSubscription(
store as any,
undefined,
trackingNode
)
return {
store,
store: store as any,
subscription,
getServerState: serverState ? () => serverState : undefined,
stabilityCheck,
noopCheck,
trackingNode,
}
}, [store, serverState, stabilityCheck, noopCheck])

Expand Down
51 changes: 48 additions & 3 deletions src/hooks/useSelector.ts
@@ -1,4 +1,4 @@
import { useCallback, useDebugValue, useRef } from 'react'
import { useCallback, useDebugValue, useMemo, useRef } from 'react'

import {
createReduxContextHook,
Expand All @@ -8,6 +8,9 @@ import { ReactReduxContext } from '../components/Context'
import type { EqualityFn, NoInfer } from '../types'
import type { uSESWS } from '../utils/useSyncExternalStore'
import { notInitialized } from '../utils/useSyncExternalStore'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import { createCache } from '../utils/autotracking/autotracking'
import { CacheWrapper } from '../utils/Subscription'

export type CheckFrequency = 'never' | 'once' | 'always'

Expand Down Expand Up @@ -80,6 +83,7 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector {
getServerState,
stabilityCheck: globalStabilityCheck,
noopCheck: globalNoopCheck,
trackingNode,
} = useReduxContext()!

const firstRun = useRef(true)
Expand Down Expand Up @@ -136,11 +140,52 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector {
[selector, globalStabilityCheck, stabilityCheck]
)

const latestWrappedSelectorRef = useRef(wrappedSelector)

// console.log(
// 'Writing latest selector. Same reference? ',
// wrappedSelector === latestWrappedSelectorRef.current
// )
latestWrappedSelectorRef.current = wrappedSelector

const cache = useMemo(() => {
//console.log('Recreating cache')
const cache = createCache(() => {
// console.log('Wrapper cache called: ', store.getState())
//return latestWrappedSelectorRef.current(trackingNode.proxy as TState)
return wrappedSelector(trackingNode.proxy as TState)
})
return cache
}, [trackingNode, wrappedSelector])

const cacheWrapper = useRef({ cache } as CacheWrapper)

useIsomorphicLayoutEffect(() => {
cacheWrapper.current.cache = cache
})

const subscribeToStore = useMemo(() => {
const subscribeToStore = (onStoreChange: () => void) => {
const wrappedOnStoreChange = () => {
// console.log('wrappedOnStoreChange')
return onStoreChange()
}
console.log('Subscribing to store with tracking')
return subscription.addNestedSub(wrappedOnStoreChange, {
trigger: 'tracked',
cache: cacheWrapper.current,
})
}
return subscribeToStore
}, [subscription])

const selectedState = useSyncExternalStoreWithSelector(
subscription.addNestedSub,
//subscription.addNestedSub,
subscribeToStore,
store.getState,
//() => trackingNode.proxy as TState,
getServerState || store.getState,
wrappedSelector,
cache.getValue,
equalityFn
)

Expand Down
88 changes: 78 additions & 10 deletions src/utils/Subscription.ts
@@ -1,15 +1,30 @@
import type { Store } from 'redux'
import { getBatch } from './batch'
import type { Node } from './autotracking/tracking'

import {
createCache,
TrackingCache,
$REVISION,
} from './autotracking/autotracking'
import { updateNode } from './autotracking/proxy'

// encapsulates the subscription logic for connecting a component to the redux store, as
// well as nesting subscriptions of descendant components, so that we can ensure the
// ancestor components re-render before descendants

type VoidFunc = () => void

export interface CacheWrapper {
cache: TrackingCache
}

type Listener = {
callback: VoidFunc
next: Listener | null
prev: Listener | null
trigger: 'always' | 'tracked'
selectorCache?: CacheWrapper
}

function createListenerCollection() {
Expand All @@ -24,10 +39,29 @@ function createListenerCollection() {
},

notify() {
console.log('Notifying subscribers')
batch(() => {
let listener = first
while (listener) {
listener.callback()
console.log('Listener: ', listener)
if (listener.trigger == 'tracked') {
if (listener.selectorCache!.cache.needsRecalculation()) {
console.log('Calling subscriber due to recalc need')
console.log(
'Calling subscriber due to recalc. Revision before: ',
$REVISION
)
listener.callback()
console.log('Revision after: ', $REVISION)
} else {
console.log(
'Skipping subscriber, no recalc: ',
listener.selectorCache
)
}
} else {
listener.callback()
}
listener = listener.next
}
})
Expand All @@ -43,13 +77,29 @@ function createListenerCollection() {
return listeners
},

subscribe(callback: () => void) {
subscribe(
callback: () => void,
options: AddNestedSubOptions = { trigger: 'always' }
) {
let isSubscribed = true

console.log('Adding listener: ', options.trigger)

let listener: Listener = (last = {
callback,
next: null,
prev: last,
trigger: options.trigger,
selectorCache:
options.trigger === 'tracked' ? options.cache! : undefined,
// subscriberCache:
// options.trigger === 'tracked'
// ? createCache(() => {
// console.log('Calling subscriberCache')
// listener.selectorCache!.get()
// callback()
// })
// : undefined,
})

if (listener.prev) {
Expand Down Expand Up @@ -79,13 +129,18 @@ function createListenerCollection() {

type ListenerCollection = ReturnType<typeof createListenerCollection>

interface AddNestedSubOptions {
trigger: 'always' | 'tracked'
cache?: CacheWrapper
}

export interface Subscription {
addNestedSub: (listener: VoidFunc) => VoidFunc
addNestedSub: (listener: VoidFunc, options?: AddNestedSubOptions) => VoidFunc
notifyNestedSubs: VoidFunc
handleChangeWrapper: VoidFunc
isSubscribed: () => boolean
onStateChange?: VoidFunc | null
trySubscribe: VoidFunc
trySubscribe: (options?: AddNestedSubOptions) => void
tryUnsubscribe: VoidFunc
getListeners: () => ListenerCollection
}
Expand All @@ -95,16 +150,28 @@ const nullListeners = {
get: () => [],
} as unknown as ListenerCollection

export function createSubscription(store: any, parentSub?: Subscription) {
export function createSubscription(
store: Store,
parentSub?: Subscription,
trackingNode?: Node<any>
) {
let unsubscribe: VoidFunc | undefined
let listeners: ListenerCollection = nullListeners

function addNestedSub(listener: () => void) {
trySubscribe()
return listeners.subscribe(listener)
function addNestedSub(
listener: () => void,
options: AddNestedSubOptions = { trigger: 'always' }
) {
console.log('addNestedSub: ', options)
trySubscribe(options)
return listeners.subscribe(listener, options)
}

function notifyNestedSubs() {
if (store && trackingNode) {
console.log('Updating node in notifyNestedSubs')
updateNode(trackingNode, store.getState())
}
listeners.notify()
}

Expand All @@ -118,10 +185,11 @@ export function createSubscription(store: any, parentSub?: Subscription) {
return Boolean(unsubscribe)
}

function trySubscribe() {
function trySubscribe(options: AddNestedSubOptions = { trigger: 'always' }) {
if (!unsubscribe) {
console.log('trySubscribe, parentSub: ', parentSub)
unsubscribe = parentSub
? parentSub.addNestedSub(handleChangeWrapper)
? parentSub.addNestedSub(handleChangeWrapper, options)
: store.subscribe(handleChangeWrapper)

listeners = createListenerCollection()
Expand Down