diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..c09746cb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Tests", + "type": "node", + "request": "launch", + "runtimeExecutable": "yarn", + "args": ["jest", "--runInBand"], + "console": "integratedTerminal", + "cwd": "${workspaceRoot}", + "internalConsoleOptions": "neverOpen" + }, + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a370bec4..8c2f5c59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## v1.3.0-rc.2 (Jul 31, 2024) + +### Fixes: + +- `react`: only rerun `useAtomSelector` effect on cache id change (#110) + +## v1.3.0-rc.1 (Jul 30, 2024) + +### Fixes: + +- `react`: restore `useAtomSelector` mounted state when swapping inline refs (#108) + +## v1.3.0-rc.0 (Jul 30, 2024) + +This version contains huge fixes for `useAtomSelector` that are only fully compatible with React 19 or with React 18 when not using StrictMode. It's recommended to wait for React 19 before upgrading to this version. + +### Fixes: + +- `atoms`, `react`: optimize `useAtomSelector` for React 19 (#106) + ## v1.2.3 (Sep 7, 2024) ### Fixes: diff --git a/jest.setup.ts b/jest.setup.ts index 264828a9..344eef55 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1 +1,36 @@ import '@testing-library/jest-dom/extend-expect' + +let callstacks: Record = {} +let id = 0 + +afterEach(() => { + callstacks = {} + id = 0 +}) + +const generateId = () => { + const stack = + (new Error().stack || '') + .split('\n') + .find(line => /\.test\.tsx:/.test(line)) || '' + + return (callstacks[stack] ||= `:r${id++}:`) +} + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useId: generateId, +})) + +// React's `useId` gives new ids in the same callstack when a component tree is +// destroyed/unmounted. Call this to manually force ids to be recreated in tests +// to mimic React's behavior. +;(globalThis as any).clearUseIdEntry = (idNum: number) => { + const key = Object.keys(callstacks).find( + key => callstacks[key] === `:r${idNum}:` + ) + + if (key) { + delete callstacks[key] + } +} diff --git a/package.json b/package.json index 640242b9..5a024b80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zedux", - "version": "1.2.3", + "version": "1.3.0-rc.2", "description": "A Molecular State Engine for React", "type": "module", "author": "Joshua Claunch (bowheart )", diff --git a/packages/atoms/package.json b/packages/atoms/package.json index ce11047c..9e2466b3 100644 --- a/packages/atoms/package.json +++ b/packages/atoms/package.json @@ -1,6 +1,6 @@ { "name": "@zedux/atoms", - "version": "1.2.3", + "version": "1.3.0-rc.2", "description": "A Molecular State Engine for React", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -10,7 +10,7 @@ "url": "https://github.com/Omnistac/zedux/issues" }, "dependencies": { - "@zedux/core": "^1.2.3" + "@zedux/core": "^1.3.0-rc.2" }, "exports": { ".": { diff --git a/packages/atoms/src/classes/Selectors.ts b/packages/atoms/src/classes/Selectors.ts index f5730b22..565f9f62 100644 --- a/packages/atoms/src/classes/Selectors.ts +++ b/packages/atoms/src/classes/Selectors.ts @@ -48,18 +48,6 @@ export class Selectors { */ public _refBaseKeys = new WeakMap, string>() - /** - * Used to work around React double-renders and double-effects. - */ - public _storage: Record< - string, - { - cache?: SelectorCache - ignorePhase?: number - timeoutId?: ReturnType - } - > = {} - constructor(private readonly ecosystem: Ecosystem) {} public addDependent( @@ -182,9 +170,8 @@ export class Selectors { args as Args, true ) - if (!id) return - return this._items[id] + return id && this._items[id] } /** @@ -369,20 +356,19 @@ export class Selectors { /** * Should only be used internally */ - public _swapRefs( - oldRef: AtomSelectorOrConfig, - newRef: AtomSelectorOrConfig, + public _swapRefs( + oldCache: SelectorCache, + newRef: AtomSelectorOrConfig, args: any[] = [] ) { - const existingCache = this.find(oldRef, args) - const baseKey = this._refBaseKeys.get(oldRef) + const baseKey = this._refBaseKeys.get(oldCache.selectorRef) - if (!existingCache || !baseKey) return + if (!baseKey) return this._refBaseKeys.set(newRef, baseKey) - this._refBaseKeys.delete(oldRef) - existingCache.selectorRef = newRef - this.runSelector(existingCache.id, args) + this._refBaseKeys.delete(oldCache.selectorRef) + oldCache.selectorRef = newRef + this.runSelector(oldCache.id, args, false, true) } /** @@ -395,7 +381,6 @@ export class Selectors { }) this._refBaseKeys = new WeakMap() - this._storage = {} } /** @@ -429,7 +414,8 @@ export class Selectors { private runSelector( id: string, args: Args, - isInitializing?: boolean + isInitializing?: boolean, + skipNotifyingDependents?: boolean ) { const { _evaluationStack, _graph, _mods, modBus } = this.ecosystem _graph.bufferUpdates(id) @@ -451,7 +437,9 @@ export class Selectors { const result = selector(_evaluationStack.atomGetters, ...args) if (!isInitializing && !resultsComparator(result, cache.result as T)) { - _graph.scheduleDependents(id, cache.nextReasons, result, cache.result) + if (!skipNotifyingDependents) { + _graph.scheduleDependents(id, cache.nextReasons, result, cache.result) + } if (_mods.stateChanged) { modBus.dispatch( diff --git a/packages/core/package.json b/packages/core/package.json index 24bdc17e..c7fad2e0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@zedux/core", - "version": "1.2.3", + "version": "1.3.0-rc.2", "description": "A high-level, declarative, composable form of Redux", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/immer/package.json b/packages/immer/package.json index 7b287599..2c4a97b4 100644 --- a/packages/immer/package.json +++ b/packages/immer/package.json @@ -1,6 +1,6 @@ { "name": "@zedux/immer", - "version": "1.2.3", + "version": "1.3.0-rc.2", "description": "Official Immer integration for Zedux's store and atomic APIs", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -10,7 +10,7 @@ "url": "https://github.com/Omnistac/zedux/issues" }, "devDependencies": { - "@zedux/atoms": "^1.2.3", + "@zedux/atoms": "^1.3.0-rc.2", "immer": "^9.0.21" }, "exports": { @@ -41,7 +41,7 @@ ], "license": "MIT", "peerDependencies": { - "@zedux/atoms": "^1.2.3", + "@zedux/atoms": "^1.3.0-rc.2", "immer": ">=9.0.19" }, "repository": { diff --git a/packages/machines/package.json b/packages/machines/package.json index c5b4a824..f4976f1f 100644 --- a/packages/machines/package.json +++ b/packages/machines/package.json @@ -1,6 +1,6 @@ { "name": "@zedux/machines", - "version": "1.2.3", + "version": "1.3.0-rc.2", "description": "Simple native state machine implementation for Zedux atoms", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -10,7 +10,7 @@ "url": "https://github.com/Omnistac/zedux/issues" }, "devDependencies": { - "@zedux/atoms": "^1.2.3" + "@zedux/atoms": "^1.3.0-rc.2" }, "exports": { ".": { @@ -41,7 +41,7 @@ ], "license": "MIT", "peerDependencies": { - "@zedux/atoms": "^1.2.3" + "@zedux/atoms": "^1.3.0-rc.2" }, "repository": { "directory": "packages/machines", diff --git a/packages/machines/test/integrations/state-machines.test.tsx b/packages/machines/test/integrations/state-machines.test.tsx index 06e56eb3..3d775d54 100644 --- a/packages/machines/test/integrations/state-machines.test.tsx +++ b/packages/machines/test/integrations/state-machines.test.tsx @@ -3,7 +3,7 @@ import { InjectMachineStoreParams, MachineState, } from '@zedux/machines' -import { api, atom } from '@zedux/react' +import { api, atom } from '@zedux/atoms' import { ecosystem } from '../../../react/test/utils/ecosystem' const injectMachine = < diff --git a/packages/machines/test/snippets/api.tsx b/packages/machines/test/snippets/api.tsx index ce4fdf96..5c5fd603 100644 --- a/packages/machines/test/snippets/api.tsx +++ b/packages/machines/test/snippets/api.tsx @@ -5,7 +5,7 @@ import { ion, useAtomSelector, useAtomValue, -} from '@zedux/react' +} from '../../../react/src' import { injectMachineStore } from '@zedux/machines' import React, { Suspense, useState } from 'react' diff --git a/packages/react/package.json b/packages/react/package.json index feb10501..a36201c8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@zedux/react", - "version": "1.2.3", + "version": "1.3.0-rc.2", "description": "A Molecular State Engine for React", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -10,14 +10,15 @@ "url": "https://github.com/Omnistac/zedux/issues" }, "dependencies": { - "@zedux/atoms": "^1.2.3" + "@zedux/atoms": "^1.3.0-rc.2" }, "devDependencies": { + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", - "@types/react-dom": "^18.0.11", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@testing-library/react": "^16.0.1", + "@types/react-dom": "^18.3.0", + "react": "^19.0.0-rc-ee1a403a-20240916", + "react-dom": "^19.0.0-rc-ee1a403a-20240916" }, "exports": { ".": { @@ -55,7 +56,7 @@ ], "license": "MIT", "peerDependencies": { - "react": ">=18.0.0" + "react": ">=19.0.0" }, "repository": { "directory": "packages/react", diff --git a/packages/react/src/hooks/useAtomInstance.ts b/packages/react/src/hooks/useAtomInstance.ts index c507ba13..6c95339f 100644 --- a/packages/react/src/hooks/useAtomInstance.ts +++ b/packages/react/src/hooks/useAtomInstance.ts @@ -117,8 +117,12 @@ export const useAtomInstance: { } return () => { - // no need to set the "ref"'s `.mounted` property to false here + // remove the edge immediately - no need for a delay here. When StrictMode + // double-invokes (invokes, then cleans up, then re-invokes) this effect, + // it's expected that any `ttl: 0` atoms get destroyed and recreated - + // that's part of what StrictMode is ensuring ecosystem._graph.removeEdge(dependentKey, instance.id) + // no need to set `render.mounted` to false here } }, [instance.id]) diff --git a/packages/react/src/hooks/useAtomSelector.ts b/packages/react/src/hooks/useAtomSelector.ts index b8b15c6f..2b01b366 100644 --- a/packages/react/src/hooks/useAtomSelector.ts +++ b/packages/react/src/hooks/useAtomSelector.ts @@ -29,10 +29,10 @@ export const useAtomSelector = ( const { _graph, selectors } = ecosystem const dependentKey = useReactComponentId() const [, render] = useState() - const storage = - (render as any).storage || (selectors._storage[dependentKey] ||= {}) - const existingCache = storage.cache as SelectorCache | undefined + const existingCache = (render as any).cache as + | SelectorCache + | undefined const argsChanged = !existingCache || @@ -46,99 +46,75 @@ export const useAtomSelector = ( : haveDepsChanged(existingCache.args, args)) const resolvedArgs = argsChanged ? args : (existingCache.args as Args) - const cache = selectors.getCache(selectorOrConfig, resolvedArgs) - const renderedResult = cache.result - - if (cache !== existingCache) { - if (existingCache) { - // yes, remove this during render - _graph.removeEdge(dependentKey, existingCache.id) - } - storage.cache = cache as SelectorCache - } - - // When an inline selector returns a referentially unstable result every run, - // we have to ignore the subsequent update. Do that using a "state machine" - // that goes from 0 -> 1 -> 2. This machine ensures that the ignored update - // occurs after the component rerenders and the effect reruns after that - // render. This works with strict mode on or off. Use the stable `render` - // function as a "ref" :O - if (storage.ignorePhase === 1) { - storage.ignorePhase = 2 + // if the refs/args don't match, existingCache has refCount: 1, there is no + // cache yet for the new ref, and the new ref has the same name, assume it's + // an inline selector + const isSwappingRefs = + existingCache && + existingCache.selectorRef !== selectorOrConfig && + !argsChanged + ? _graph.nodes[existingCache.id]?.refCount === 1 && + !selectors._refBaseKeys.has(selectorOrConfig) && + selectors._getIdealCacheId(existingCache.selectorRef) === + selectors._getIdealCacheId(selectorOrConfig) + : false + + if (isSwappingRefs) { + // switch `mounted` to false temporarily to prevent circular rerenders + ;(render as any).mounted = false + selectors._swapRefs( + existingCache as SelectorCache, + selectorOrConfig as AtomSelectorOrConfig, + resolvedArgs + ) + ;(render as any).mounted = true } - let cancelCleanup = false + const cache = isSwappingRefs + ? (existingCache as SelectorCache) + : selectors.getCache(selectorOrConfig, resolvedArgs) - useEffect(() => { - cancelCleanup = true - delete selectors._storage[dependentKey] - ;(render as any).storage = storage - - // re-get the cache in case an unmounting component's effect cleanup - // destroyed it before we could add this dependent - const newCache = selectors.getCache(selectorOrConfig, resolvedArgs) - - const cleanup = () => { - if (cancelCleanup) { - cancelCleanup = false - - return - } + const addEdge = () => { + if (!_graph.nodes[cache.id]?.dependents.get(dependentKey)) { + _graph.addEdge(dependentKey, cache.id, OPERATION, External, () => { + if ((render as any).mounted) render({}) + }) + } + } - if (storage.ignorePhase !== 1) { - delete selectors._storage[dependentKey] + // Yes, subscribe during render. This operation is idempotent. + addEdge() - queueMicrotask(() => { - _graph.removeEdge(dependentKey, newCache.id) - }) - } - } + const renderedResult = cache.result + ;(render as any).cache = cache as SelectorCache - // Make this function idempotent to guard against React's double-invocation - if (_graph.nodes[newCache.id]?.dependents.get(dependentKey)) { - return cleanup - } + useEffect(() => { + // Try adding the edge again (will be a no-op unless React's StrictMode ran + // this effect's cleanup unnecessarily) + addEdge() - _graph.addEdge(dependentKey, newCache.id, OPERATION, External, () => - render({}) - ) + // use the referentially stable render function as a ref :O + ;(render as any).mounted = true // an unmounting component's effect cleanup can force-destroy the selector - // or update its dependencies before this component is mounted. If that - // happened, trigger a rerender to recache the selector and/or get its new - // result. On the rerender, ignore changes - if (newCache.result !== renderedResult && !storage.ignorePhase) { - storage.ignorePhase = 1 + // or update the state of its dependencies (causing it to rerun) before we + // set `render.mounted`. If that happened, trigger a rerender to recreate + // the selector and/or get its new state + if (cache.isDestroyed || cache.result !== renderedResult) { render({}) } - if (storage.ignorePhase === 2) { - storage.ignorePhase = 0 + return () => { + // remove the edge immediately - no need for a delay here. When StrictMode + // double-invokes (invokes, then cleans up, then re-invokes) this effect, + // it's expected that selectors and `ttl: 0` atoms with no other + // dependents get destroyed and recreated - that's part of what StrictMode + // is ensuring + _graph.removeEdge(dependentKey, cache.id) + // no need to set `render.mounted` to false here } - - // React StrictMode's double renders can wreak havoc on the selector cache. - // Clean up havoc - if (storage.timeoutId == null) { - const removeCruft = () => { - storage.timeoutId = null - cancelCleanup = false - - Object.values(selectors._storage).forEach(storageItem => { - if (storageItem.cache?.id) { - selectors._destroySelector(storageItem.cache.id) - } - }) - } - - storage.timeoutId = - typeof requestIdleCallback !== 'undefined' - ? requestIdleCallback(removeCruft, { timeout: 500 }) - : setTimeout(removeCruft, 500) - } - - return cleanup - }, [cache]) + }, [cache.id]) return renderedResult as T } diff --git a/packages/react/src/hooks/useReactComponentId.ts b/packages/react/src/hooks/useReactComponentId.ts index 71844cfe..56878f6f 100644 --- a/packages/react/src/hooks/useReactComponentId.ts +++ b/packages/react/src/hooks/useReactComponentId.ts @@ -1,5 +1,35 @@ import React, { useId } from 'react' +type MaybeComponent = + | { + displayName?: string + name?: string + } + | undefined + +type React19 = { + __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?: { + A?: { + getOwner?: () => { + type?: MaybeComponent + } + } + } + + __SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?: { + A?: { + getOwner?: () => { + type?: MaybeComponent + } + } + } +} + +const react19KeyBase = + '_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE' as const +const clientKey = `__CLIENT${react19KeyBase}` as const +const serverKey = `__SERVER${react19KeyBase}` as const + /** * Get a unique id for a Zedux hook call. This id is predictable - it should be * exactly the same every time a given component renders in the same place in @@ -8,18 +38,14 @@ import React, { useId } from 'react' * * This uses the forbidden React internals object. We only use it to get a * dev-friendly name for the React component's node in the atom graph. It's fine - * if React changes their internals - we'll fall back to using a generated node - * name. + * if React changes their internals - we'll fall back to the string "rc" ("React + * Component"). We have no need to "warn users they cannot upgrade" 'cause they + * can at the cost of some DX. */ export const useReactComponentId = () => { - const component: - | { - displayName?: string - name?: string - } - | undefined = (React as any) - .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner - ?.current?.type + const component: MaybeComponent = ( + (React as React19)[clientKey] || (React as React19)[serverKey] + )?.A?.getOwner?.()?.type const name = component?.displayName || component?.name || 'rc' diff --git a/packages/react/test/integrations/ecosystem.test.tsx b/packages/react/test/integrations/ecosystem.test.tsx index 2787e3cf..9859e270 100644 --- a/packages/react/test/integrations/ecosystem.test.tsx +++ b/packages/react/test/integrations/ecosystem.test.tsx @@ -11,7 +11,7 @@ import { getEcosystem, } from '@zedux/react' import React, { useState } from 'react' -import { act } from 'react-dom/test-utils' +import { act } from '@testing-library/react' import { ecosystem } from '../utils/ecosystem' import { renderInEcosystem } from '../utils/renderInEcosystem' import { fireEvent, render } from '@testing-library/react' diff --git a/packages/react/test/integrations/react-context.test.tsx b/packages/react/test/integrations/react-context.test.tsx index 85664107..a20de820 100644 --- a/packages/react/test/integrations/react-context.test.tsx +++ b/packages/react/test/integrations/react-context.test.tsx @@ -119,7 +119,6 @@ describe('React context', () => { render() { if (this.state.error) { - // You can render any custom fallback UI return
{this.state.error}
} @@ -136,7 +135,7 @@ describe('React context', () => { const div = await findByTestId('1') expect(div.innerHTML).toMatch(/AtomProvider.*requires.*prop/i) - expect(spy).toHaveBeenCalledTimes(3) + expect(spy).toHaveBeenCalledTimes(1) spy.mockReset() }) @@ -209,7 +208,6 @@ describe('React context', () => { test('useAtomContext() throws an error if 2nd param is true and no instance was provided', async () => { jest.useFakeTimers() - const mock = mockConsole('error') const atom1 = atom('1', (id: string) => id) function Test() { @@ -222,11 +220,5 @@ describe('React context', () => { const pattern = /no atom instance was provided/i expect(() => renderInEcosystem()).toThrowError(pattern) - expect(mock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - message: expect.stringMatching(pattern), - }) - ) }) }) diff --git a/packages/react/test/integrations/selection.test.tsx b/packages/react/test/integrations/selection.test.tsx index cdce800d..ddb267b2 100644 --- a/packages/react/test/integrations/selection.test.tsx +++ b/packages/react/test/integrations/selection.test.tsx @@ -150,6 +150,11 @@ describe('selection', () => { expect(selector3).toHaveBeenCalledTimes(1) expect((await findByTestId('text')).innerHTML).toBe('c1') + // reset the 3 useId calls in ResurrectingComponent's useAtomSelectors + ;(globalThis as any).clearUseIdEntry(1) + ;(globalThis as any).clearUseIdEntry(2) + ;(globalThis as any).clearUseIdEntry(3) + act(() => { fireEvent.click(button) jest.runAllTimers() diff --git a/packages/react/test/integrations/suspense.test.tsx b/packages/react/test/integrations/suspense.test.tsx index 55b42376..e36d01f7 100644 --- a/packages/react/test/integrations/suspense.test.tsx +++ b/packages/react/test/integrations/suspense.test.tsx @@ -93,12 +93,15 @@ describe('suspense', () => { const div2 = await findByTestId('error') expect(div2.innerHTML).toBe('b') - expect(mock).toHaveBeenCalledTimes(3) - expect(mock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - message: expect.stringMatching(/uncaught 'b'/i), - }) + expect(mock).toHaveBeenCalledTimes(1) + expect(mock).toHaveBeenCalledWith( + expect.any(String), + 'b', + expect.stringContaining( + 'The above error occurred in the component' + ), + expect.any(String), + expect.any(String) ) }) @@ -132,12 +135,15 @@ describe('suspense', () => { const div2 = await findByTestId('error') expect(div2.innerHTML).toBe('b') - expect(mock).toHaveBeenCalledTimes(3) - expect(mock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - message: expect.stringMatching(/uncaught 'b'/i), - }) + expect(mock).toHaveBeenCalledTimes(1) + expect(mock).toHaveBeenCalledWith( + expect.any(String), + 'b', + expect.stringContaining( + 'The above error occurred in the component' + ), + expect.any(String), + expect.any(String) ) }) diff --git a/packages/react/test/units/__snapshots__/useAtomSelector.test.tsx.snap b/packages/react/test/units/__snapshots__/useAtomSelector.test.tsx.snap index 6b174fce..2ee7291c 100644 --- a/packages/react/test/units/__snapshots__/useAtomSelector.test.tsx.snap +++ b/packages/react/test/units/__snapshots__/useAtomSelector.test.tsx.snap @@ -21,7 +21,7 @@ exports[`useAtomSelector inline selector that returns a different object referen "1" => true, }, "dependents": Map { - "Test-:rb:" => { + "Test-:r0:" => { "callback": [Function], "createdAt": 123456789, "flags": 2, @@ -40,7 +40,7 @@ exports[`useAtomSelector inline selector that returns a different object referen "1": { "dependencies": Map {}, "dependents": Map { - "@@selector-unnamed-1" => { + "@@selector-unnamed-0" => { "callback": undefined, "createdAt": 123456789, "flags": 0, @@ -51,16 +51,17 @@ exports[`useAtomSelector inline selector that returns a different object referen "refCount": 1, "weight": 1, }, - "@@selector-unnamed-1": { + "@@selector-unnamed-0": { "dependencies": Map { "1" => true, }, "dependents": Map { - "Test-:rb:" => { + "Test-:r0:" => { "callback": [Function], "createdAt": 123456789, "flags": 2, "operation": "useAtomSelector", + "task": undefined, }, }, "isSelector": true, @@ -91,7 +92,7 @@ exports[`useAtomSelector inline selector that returns a different object referen "1" => true, }, "dependents": Map { - "Test-:rd:" => { + "Test-:r0:" => { "callback": [Function], "createdAt": 123456789, "flags": 2, @@ -110,7 +111,7 @@ exports[`useAtomSelector inline selector that returns a different object referen "1": { "dependencies": Map {}, "dependents": Map { - "@@selector-unnamed-3" => { + "@@selector-unnamed-1" => { "callback": undefined, "createdAt": 123456789, "flags": 0, @@ -121,16 +122,17 @@ exports[`useAtomSelector inline selector that returns a different object referen "refCount": 1, "weight": 1, }, - "@@selector-unnamed-3": { + "@@selector-unnamed-1": { "dependencies": Map { "1" => true, }, "dependents": Map { - "Test-:rd:" => { + "Test-:r0:" => { "callback": [Function], "createdAt": 123456789, "flags": 2, "operation": "useAtomSelector", + "task": undefined, }, }, "isSelector": true, diff --git a/packages/react/test/units/useAtomSelector.test.tsx b/packages/react/test/units/useAtomSelector.test.tsx index 808cecef..16cc66d3 100644 --- a/packages/react/test/units/useAtomSelector.test.tsx +++ b/packages/react/test/units/useAtomSelector.test.tsx @@ -168,7 +168,7 @@ describe('useAtomSelector', () => { }) }) - test('useAtomSelector creates/uses a different cache when selector goes from object form to function form', async () => { + test('useAtomSelector reuses the same cache when selector goes from object form to function form', async () => { jest.useFakeTimers() const selector1 = jest.fn(() => 1) @@ -208,7 +208,7 @@ describe('useAtomSelector', () => { expect(div.innerHTML).toBe('1') expect(selector1).toHaveBeenCalledTimes(2) expect(ecosystem.selectors.dehydrate()).toEqual({ - '@@selector-mockConstructor-1': 1, + '@@selector-mockConstructor-0': 1, }) }) @@ -305,7 +305,7 @@ describe('useAtomSelector', () => { expect(div.innerHTML).toBe('0') expect(selector1).toHaveBeenCalledTimes(2) expect(ecosystem.selectors.dehydrate()).toEqual({ - '@@selector-mockConstructor-1-[0]': 0, + '@@selector-mockConstructor-0-[0]': 0, }) }) @@ -356,7 +356,7 @@ describe('useAtomSelector', () => { expect(div.innerHTML).toBe('1') expect(selector1).toHaveBeenCalledTimes(2) expect(ecosystem.selectors.dehydrate()).toEqual({ - '@@selector-mockConstructor-2': 1, + '@@selector-mockConstructor-0': 1, }) expect(renders).toBe(2) }) @@ -440,8 +440,6 @@ describe('useAtomSelector', () => { const button = await findByTestId('button') const div = await findByTestId('text') - jest.runAllTimers() - expect(ecosystem.selectors.findAll()).toMatchInlineSnapshot(` { "@@selector-unnamed-1": SelectorCache { @@ -465,9 +463,9 @@ describe('useAtomSelector', () => { expect(ecosystem.selectors.findAll()).toMatchInlineSnapshot(` { - "@@selector-unnamed-3": SelectorCache { + "@@selector-unnamed-1": SelectorCache { "args": [], - "id": "@@selector-unnamed-3", + "id": "@@selector-unnamed-1", "nextReasons": [], "prevReasons": [], "result": 1, @@ -533,10 +531,8 @@ describe('useAtomSelector', () => { const div = await findByTestId('text') - jest.runAllTimers() - expect(div.innerHTML).toBe('1') - expect(renders).toBe(2) // 2 rerenders + 2 for strict mode + expect(renders).toBe(4) // 2 rerenders + 2 for strict mode expect(ecosystem._graph.nodes).toMatchSnapshot() act(() => { @@ -545,7 +541,102 @@ describe('useAtomSelector', () => { }) expect(div.innerHTML).toBe('2') - expect(renders).toBe(4) // 4 rerenders + 4 for strict mode + expect(renders).toBe(6) // 3 rerenders + 3 for strict mode expect(ecosystem._graph.nodes).toMatchSnapshot() }) + + test('inline selector stays subscribed after being swapped out', async () => { + jest.useFakeTimers() + const atom1 = atom('1', () => ({ val: 1 })) + const selector = ({ get }: AtomGetters) => get(atom1) + + function Test() { + const { val } = useAtomSelector({ + resultsComparator: (a, b) => a.val === b.val, + selector, + }) + + return ( + <> +
{val}
+ + ) + } + + const { findByTestId } = renderInEcosystem(, { + useStrictMode: true, + }) + + const div = await findByTestId('text') + + expect(div.innerHTML).toBe('1') + + act(() => { + ecosystem.getInstance(atom1).setState({ val: 2 }) + jest.runAllTimers() + }) + + expect(div.innerHTML).toBe('2') + + act(() => { + ecosystem.getInstance(atom1).setState({ val: 3 }) + jest.runAllTimers() + }) + + expect(div.innerHTML).toBe('3') + + act(() => { + ecosystem.getInstance(atom1).setState({ val: 4 }) + jest.runAllTimers() + }) + + expect(div.innerHTML).toBe('4') + }) + + test('when a selector is destroyed, other selectors that pass different params to the same ref retain their ids', async () => { + jest.useFakeTimers() + const atom1 = atom('1', () => '1') + const selector1 = ({ get }: AtomGetters, str: string) => get(atom1) + str + + function Test() { + const a = useAtomSelector(selector1, 'a') + const b = useAtomSelector(selector1, 'b') + const c = useAtomSelector(selector1, 'c') + + return ( +
+ {a} + {b} + {c} +
+ ) + } + + const { findByTestId } = renderInEcosystem() + + const div = await findByTestId('text') + + expect(div.innerHTML).toBe('1a1b1c') + expect(ecosystem.selectors.dehydrate()).toMatchInlineSnapshot(` + { + "@@selector-selector1-0-["a"]": "1a", + "@@selector-selector1-0-["b"]": "1b", + "@@selector-selector1-0-["c"]": "1c", + } + `) + + act(() => { + ecosystem.selectors.destroyCache(selector1, ['a'], true) + jest.runAllTimers() + }) + + expect(div.innerHTML).toBe('1a1b1c') + expect(ecosystem.selectors.dehydrate()).toMatchInlineSnapshot(` + { + "@@selector-selector1-0-["a"]": "1a", + "@@selector-selector1-0-["b"]": "1b", + "@@selector-selector1-0-["c"]": "1c", + } + `) + }) }) diff --git a/packages/react/test/utils/ecosystem.ts b/packages/react/test/utils/ecosystem.ts index 4ba4903e..b2f08fae 100644 --- a/packages/react/test/utils/ecosystem.ts +++ b/packages/react/test/utils/ecosystem.ts @@ -1,5 +1,5 @@ import { act } from '@testing-library/react' -import { createEcosystem } from '@zedux/react' +import { createEcosystem } from '@zedux/atoms' export const ecosystem = createEcosystem({ id: 'test' }) diff --git a/yarn.lock b/yarn.lock index 60e68451..9c7cd55c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,11 +23,12 @@ "@babel/highlight" "^7.18.6" "@babel/code-frame@^7.10.4": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" - integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== dependencies: - "@babel/highlight" "^7.16.7" + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0": version "7.16.0" @@ -268,16 +269,16 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== -"@babel/helper-validator-identifier@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" - integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== - "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" @@ -315,15 +316,6 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/highlight@^7.16.7": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" - integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -333,6 +325,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.16.5": version "7.16.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.6.tgz#8f194828193e8fa79166f34a4b4e52f3e769a314" @@ -1193,15 +1195,15 @@ dependencies: "@sinonjs/commons" "^2.0.0" -"@testing-library/dom@^9.0.0": - version "9.2.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.2.0.tgz#0e1f45e956f2a16f471559c06edd8827c4832f04" - integrity sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA== +"@testing-library/dom@^10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" + integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" "@types/aria-query" "^5.0.1" - aria-query "^5.0.0" + aria-query "5.3.0" chalk "^4.1.0" dom-accessibility-api "^0.5.9" lz-string "^1.5.0" @@ -1222,14 +1224,12 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c" - integrity sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg== +"@testing-library/react@^16.0.1": + version "16.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.1.tgz#29c0ee878d672703f5e7579f239005e4e0faa875" + integrity sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^9.0.0" - "@types/react-dom" "^18.0.0" "@tootallnate/once@2": version "2.0.0" @@ -1272,9 +1272,9 @@ integrity sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA== "@types/aria-query@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" - integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== "@types/babel__core@^7.1.14": version "7.1.17" @@ -1390,13 +1390,6 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== -"@types/react-dom@^18.0.0": - version "18.0.1" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.1.tgz#cb3cc10ea91141b12c71001fede1017acfbce4db" - integrity sha512-jCwTXvHtRLiyVvKm9aEdHXs8rflVOGd5Sl913JZrPshfXjn8NYsTNZOz70bCsA31IR0TOqwi3ad+X4tSCBoMTw== - dependencies: - "@types/react" "*" - "@types/react-dom@^18.0.11": version "18.0.11" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" @@ -1715,6 +1708,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + aria-query@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" @@ -2014,7 +2014,7 @@ chalk@4.1.0, chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2344,6 +2344,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -2385,11 +2390,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.6: version "0.5.13" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz#102ee5f25eacce09bdf1cfa5a298f86da473be4b" integrity sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw== +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -4274,7 +4284,7 @@ log-symbols@^5.1.0: chalk "^5.0.0" is-unicode-supported "^1.1.0" -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -4973,13 +4983,12 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg== -react-dom@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== +react-dom@^19.0.0-rc-ee1a403a-20240916: + version "19.0.0-rc-fb9a90fa48-20240614" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0-rc-fb9a90fa48-20240614.tgz#8dce9ed0650096d65437e4bce790628e483831a2" + integrity sha512-PoEsPe32F7KPLYOBvZfjylEI1B67N44PwY3lyvpmBkhlluLnLz0jH8q2Wg9YidAi6z0k3iUnNRm5x10wurzt9Q== dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.0" + scheduler "0.25.0-rc-fb9a90fa48-20240614" react-is@^16.13.1: version "16.13.1" @@ -5001,12 +5010,10 @@ react-refresh@^0.14.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== -react@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== - dependencies: - loose-envify "^1.1.0" +react@^19.0.0-rc-ee1a403a-20240916: + version "19.0.0-rc-fb9a90fa48-20240614" + resolved "https://registry.yarnpkg.com/react/-/react-19.0.0-rc-fb9a90fa48-20240614.tgz#90eb43a0b005e8cc3cbf0d801c14816d01df1b08" + integrity sha512-nvE3Gy+IOIfH/DXhkyxFVQSrITarFcQz4+shzC/McxQXEUSonpw2oDy/Wi9hdDtV3hlP12VYuDL95iiBREedNQ== readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.2" @@ -5204,12 +5211,10 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== - dependencies: - loose-envify "^1.1.0" +scheduler@0.25.0-rc-fb9a90fa48-20240614: + version "0.25.0-rc-fb9a90fa48-20240614" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-fb9a90fa48-20240614.tgz#9ee11063b7c0f47aef3fea53d9f1be3f13794dce" + integrity sha512-HHqQ/SqbeiDfXXVKgNxTpbQTD4n7IUb4hZATvHjp03jr3TF7igehCyHdOjeYTrzIseLO93cTTfSb5f4qWcirMQ== semver@7.3.4: version "7.3.4"