Repository: facebookexperimental/Recoil Branch: main Commit: c1b97f3a0117 Files: 345 Total size: 1.5 MB Directory structure: gitextract_bj3knz1n/ ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github/ │ └── workflows/ │ ├── nightly.yml │ └── nodejs.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .watchmanconfig ├── CHANGELOG-recoil-relay.md ├── CHANGELOG-recoil-sync.md ├── CHANGELOG-recoil.md ├── CHANGELOG-refine.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README-recoil-relay.md ├── README-recoil-sync.md ├── README-refine.md ├── README.md ├── babel.config.json ├── eslint-rules/ │ └── no-fb-only.js ├── flow-typed/ │ ├── jest.js │ ├── npm/ │ │ ├── ReactDOM_vx.x.x.js │ │ ├── ReactTestUtils_vx.x.x.js │ │ ├── d3-array_vx.x.x.js │ │ ├── d3-collection_vx.x.x.js │ │ ├── d3-interpolate_vx.x.x.js │ │ ├── d3-scale_vx.x.x.js │ │ ├── d3-selection_vx.x.x.js │ │ ├── d3-transition_vx.x.x.js │ │ ├── d3_vx.x.x.js │ │ ├── hamt_plus_vx.x.x.js │ │ ├── immutable_vx.x.x.js │ │ ├── jest_v26.x.x.js │ │ ├── jsondiffpatch-for-react_vx.x.x.js │ │ ├── nullthrows_vx.x.x.js │ │ └── react-dom_v18.x.x.js │ ├── package.json.js │ └── public-stubs.js ├── jest.config.js ├── package.json ├── packages/ │ ├── recoil/ │ │ ├── Recoil_index.js │ │ ├── adt/ │ │ │ ├── Recoil_ArrayKeyedMap.js │ │ │ ├── Recoil_Loadable.js │ │ │ ├── Recoil_PersistentMap.js │ │ │ ├── Recoil_Queue.js │ │ │ ├── Recoil_Wrapper.js │ │ │ └── __tests__/ │ │ │ ├── Recoil_ArrayKeyedMap-test.js │ │ │ └── Recoil_Loadable-test.js │ │ ├── caches/ │ │ │ ├── Recoil_CacheImplementationType.js │ │ │ ├── Recoil_CachePolicy.js │ │ │ ├── Recoil_LRUCache.js │ │ │ ├── Recoil_MapCache.js │ │ │ ├── Recoil_TreeCache.js │ │ │ ├── Recoil_TreeCacheImplementationType.js │ │ │ ├── Recoil_cacheFromPolicy.js │ │ │ ├── Recoil_treeCacheFromPolicy.js │ │ │ ├── Recoil_treeCacheLRU.js │ │ │ └── __tests__/ │ │ │ ├── Recoil_LRUCache-test.js │ │ │ ├── Recoil_MapCache-test.js │ │ │ ├── Recoil_TreeCache-test.js │ │ │ ├── Recoil_cacheFromPolicy-test.js │ │ │ ├── Recoil_treeCacheFromPolicy-test.js │ │ │ └── Recoil_treeCacheLRU-test.js │ │ ├── contrib/ │ │ │ ├── devtools_connector/ │ │ │ │ └── RecoilDevTools_Connector.react.js │ │ │ └── uri_persistence/ │ │ │ ├── Recoil_Link.js │ │ │ └── __tests__/ │ │ │ └── Recoil_Link-test.js │ │ ├── core/ │ │ │ ├── Recoil_AtomicUpdates.js │ │ │ ├── Recoil_Batching.js │ │ │ ├── Recoil_FunctionalCore.js │ │ │ ├── Recoil_Graph.js │ │ │ ├── Recoil_GraphTypes.js │ │ │ ├── Recoil_Keys.js │ │ │ ├── Recoil_Node.js │ │ │ ├── Recoil_ReactMode.js │ │ │ ├── Recoil_RecoilRoot.js │ │ │ ├── Recoil_RecoilValue.js │ │ │ ├── Recoil_RecoilValueInterface.js │ │ │ ├── Recoil_RetainedBy.js │ │ │ ├── Recoil_Retention.js │ │ │ ├── Recoil_RetentionZone.js │ │ │ ├── Recoil_Snapshot.js │ │ │ ├── Recoil_SnapshotCache.js │ │ │ ├── Recoil_State.js │ │ │ └── __tests__/ │ │ │ ├── Recoil_RecoilRoot-test.js │ │ │ ├── Recoil_RecoilValueInterface-test.js │ │ │ ├── Recoil_Retention-test.js │ │ │ ├── Recoil_Snapshot-test.js │ │ │ ├── Recoil_batcher-test.js │ │ │ ├── Recoil_core-test.js │ │ │ ├── Recoil_perf-test.js │ │ │ └── Recoil_useRecoilStoreID-test.js │ │ ├── hooks/ │ │ │ ├── Recoil_Hooks.js │ │ │ ├── Recoil_SnapshotHooks.js │ │ │ ├── Recoil_useGetRecoilValueInfo.js │ │ │ ├── Recoil_useRecoilBridgeAcrossReactRoots.js │ │ │ ├── Recoil_useRecoilCallback.js │ │ │ ├── Recoil_useRecoilRefresher.js │ │ │ ├── Recoil_useRecoilTransaction.js │ │ │ ├── Recoil_useRetain.js │ │ │ └── __tests__/ │ │ │ ├── Recoil_Hooks_TRANSITION_SUPPORT_UNSTABLE-test.js │ │ │ ├── Recoil_PublicHooks-test.js │ │ │ ├── Recoil_React-test.js │ │ │ ├── Recoil_useGetRecoilValueInfo-test.js │ │ │ ├── Recoil_useGotoRecoilSnapshot-test.js │ │ │ ├── Recoil_useRecoilBridgeAcrossReactRoots-test.js │ │ │ ├── Recoil_useRecoilCallback-test.js │ │ │ ├── Recoil_useRecoilInterface-test.js │ │ │ ├── Recoil_useRecoilRefresher-test.js │ │ │ ├── Recoil_useRecoilSnapshot-test.js │ │ │ ├── Recoil_useRecoilStateReset-test.js │ │ │ ├── Recoil_useRecoilTransaction-test.js │ │ │ ├── Recoil_useRecoilTransactionObserver-test.js │ │ │ ├── Recoil_useRecoilValueLoadable-test.js │ │ │ ├── Recoil_useTransactionObservation_DEPRECATED-test.js │ │ │ └── Recoil_useTransition-test.js │ │ ├── package-for-release.json │ │ ├── package.json │ │ └── recoil_values/ │ │ ├── Recoil_WaitFor.js │ │ ├── Recoil_WaitFor.js.flow │ │ ├── Recoil_atom.js │ │ ├── Recoil_atomFamily.js │ │ ├── Recoil_callbackTypes.js │ │ ├── Recoil_constSelector.js │ │ ├── Recoil_errorSelector.js │ │ ├── Recoil_readOnlySelector.js │ │ ├── Recoil_selector.js │ │ ├── Recoil_selectorFamily.js │ │ ├── __flowtests__/ │ │ │ └── Recoil_WaitFor-flowtest.js │ │ └── __tests__/ │ │ ├── Recoil_WaitFor-test.js │ │ ├── Recoil_atom-test.js │ │ ├── Recoil_atomFamily-test.js │ │ ├── Recoil_atomWithFallback-test.js │ │ ├── Recoil_constSelector-test.js │ │ ├── Recoil_errorSelector-test.js │ │ ├── Recoil_selector-test.js │ │ ├── Recoil_selectorFamily-test.js │ │ └── Recoil_selectorHooks-test.js │ ├── recoil-relay/ │ │ ├── RecoilRelay_Environments.js │ │ ├── RecoilRelay_graphQLMutationEffect.js │ │ ├── RecoilRelay_graphQLQueryEffect.js │ │ ├── RecoilRelay_graphQLSelector.js │ │ ├── RecoilRelay_graphQLSelectorFamily.js │ │ ├── RecoilRelay_graphQLSubscriptionEffect.js │ │ ├── RecoilRelay_index.js │ │ ├── __test_utils__/ │ │ │ └── RecoilRelay_mockRelayEnvironment.js │ │ ├── __tests__/ │ │ │ ├── RecoilRelay_RecoilRelayEnvironment-test.js │ │ │ ├── RecoilRelay_graphQLMutationEffect-test.js │ │ │ ├── RecoilRelay_graphQLQueryEffect-test.js │ │ │ ├── RecoilRelay_graphQLSelector-test.js │ │ │ ├── RecoilRelay_graphQLSelectorFamily-test.js │ │ │ ├── RecoilRelay_graphQLSubscriptionEffect-test.js │ │ │ └── mock-graphql/ │ │ │ ├── RecoilRelay_MockQueries.js │ │ │ └── schema.graphql │ │ ├── package-for-release.json │ │ └── package.json │ ├── recoil-sync/ │ │ ├── RecoilSync.js │ │ ├── RecoilSync_URL.js │ │ ├── RecoilSync_URLJSON.js │ │ ├── RecoilSync_URLTransit.js │ │ ├── RecoilSync_index.js │ │ ├── __test_utils__/ │ │ │ └── RecoilSync_MockURLSerialization.js │ │ ├── __tests__/ │ │ │ ├── RecoilSync-test.js │ │ │ ├── RecoilSync_URL-test.js │ │ │ ├── RecoilSync_URLCompound-test.js │ │ │ ├── RecoilSync_URLInterface-test.js │ │ │ ├── RecoilSync_URLJSON-test.js │ │ │ ├── RecoilSync_URLListen-test.js │ │ │ ├── RecoilSync_URLPush-test.js │ │ │ ├── RecoilSync_URLTransit-test.js │ │ │ └── RecoilSync_URLTransitJSON-test.js │ │ ├── package-for-release.json │ │ └── package.json │ ├── refine/ │ │ ├── Refine_API.js │ │ ├── Refine_Checkers.js │ │ ├── Refine_ContainerCheckers.js │ │ ├── Refine_JSON.js │ │ ├── Refine_PrimitiveCheckers.js │ │ ├── Refine_UtilityCheckers.js │ │ ├── Refine_index.js │ │ ├── __tests__/ │ │ │ ├── Refine-test.js │ │ │ ├── Refine_Containers-test.js │ │ │ ├── Refine_JSON-test.js │ │ │ ├── Refine_Primitives-test.js │ │ │ └── Refine_Utilities-test.js │ │ ├── package-for-release.json │ │ └── package.json │ └── shared/ │ ├── __test_utils__/ │ │ ├── Recoil_ReactRenderModes.js │ │ └── Recoil_TestingUtils.js │ ├── package.json │ ├── polyfill/ │ │ ├── ReactBatchedUpdates.js │ │ ├── ReactBatchedUpdates.native.js │ │ ├── err.js │ │ ├── expectationViolation.js │ │ ├── invariant.js │ │ ├── recoverableViolation.js │ │ └── sprintf.js │ └── util/ │ ├── Recoil_CopyOnWrite.js │ ├── Recoil_Environment.js │ ├── Recoil_Memoize.js │ ├── Recoil_PerformanceTimings.js │ ├── Recoil_ReactBatchedUpdates.js │ ├── Recoil_RecoilEnv.js │ ├── Recoil_concatIterables.js │ ├── Recoil_deepFreezeValue.js │ ├── Recoil_differenceSets.js │ ├── Recoil_err.js │ ├── Recoil_expectationViolation.js │ ├── Recoil_filterIterable.js │ ├── Recoil_filterMap.js │ ├── Recoil_filterSet.js │ ├── Recoil_gkx.js │ ├── Recoil_invariant.js │ ├── Recoil_isNode.js │ ├── Recoil_isPromise.js │ ├── Recoil_lazyProxy.js │ ├── Recoil_mapIterable.js │ ├── Recoil_mapMap.js │ ├── Recoil_mergeMaps.js │ ├── Recoil_nullthrows.js │ ├── Recoil_recoverableViolation.js │ ├── Recoil_shallowArrayEqual.js │ ├── Recoil_someSet.js │ ├── Recoil_stableStringify.js │ ├── Recoil_unionSets.js │ ├── Recoil_useComponentName.js │ ├── Recoil_usePrevious.js │ ├── Recoil_useRefInitOnce.js │ └── __tests__/ │ ├── Recoil_Memoize-test.js │ ├── Recoil_RecoilEnv-test.js │ ├── Recoil_deepFreezeValue-test.js │ ├── Recoil_lazyProxy-test.js │ └── Recoil_stableStringify-test.js ├── packages-ext/ │ ├── recoil-devtools/ │ │ ├── .babelrc │ │ ├── .eslintignore │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── flow.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── constants/ │ │ │ │ └── Constants.js │ │ │ ├── manifest.json │ │ │ ├── pages/ │ │ │ │ ├── Background/ │ │ │ │ │ ├── Background.js │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── Background.test.js │ │ │ │ │ └── index.html │ │ │ │ ├── Content/ │ │ │ │ │ ├── ContentScript.js │ │ │ │ │ └── __tests__/ │ │ │ │ │ └── ContentScript.test.js │ │ │ │ ├── Devtools/ │ │ │ │ │ ├── DevtoolsScript.js │ │ │ │ │ └── index.html │ │ │ │ ├── Page/ │ │ │ │ │ └── PageScript.js │ │ │ │ └── Popup/ │ │ │ │ ├── ConnectionContext.js │ │ │ │ ├── Devpanel.html │ │ │ │ ├── Items/ │ │ │ │ │ ├── CollapsibleItem.js │ │ │ │ │ ├── DiffItem.js │ │ │ │ │ ├── Item.js │ │ │ │ │ ├── ItemDependencies.js │ │ │ │ │ ├── ItemDescription.js │ │ │ │ │ ├── ItemLabel.js │ │ │ │ │ ├── ItemMoreItems.js │ │ │ │ │ ├── ItemValue.js │ │ │ │ │ ├── NodeName.js │ │ │ │ │ └── index.js │ │ │ │ ├── PopupApp.js │ │ │ │ ├── PopupComponent.js │ │ │ │ ├── PopupDependencyGraph.js │ │ │ │ ├── PopupDiff.js │ │ │ │ ├── PopupHeader.js │ │ │ │ ├── PopupMainContent.js │ │ │ │ ├── PopupScript.js │ │ │ │ ├── PopupSidebar.js │ │ │ │ ├── PopupSidebarTransaction.js │ │ │ │ ├── PopupSnapshot.js │ │ │ │ ├── Snapshot/ │ │ │ │ │ ├── AtomList.js │ │ │ │ │ ├── SearchContext.js │ │ │ │ │ ├── SelectorList.js │ │ │ │ │ ├── SnapshotSearch.js │ │ │ │ │ └── snapshotHooks.js │ │ │ │ ├── Tabs.js │ │ │ │ ├── index.html │ │ │ │ └── useSelectionHooks.js │ │ │ ├── types/ │ │ │ │ └── DevtoolsTypes.js │ │ │ └── utils/ │ │ │ ├── Connection.js │ │ │ ├── EvictableList.js │ │ │ ├── GraphUtils.js │ │ │ ├── Logger.js │ │ │ ├── ObjectEntries.js │ │ │ ├── ObjectValues.js │ │ │ ├── Serialization.js │ │ │ ├── Store.js │ │ │ ├── TXHashtable.js │ │ │ ├── __tests__/ │ │ │ │ ├── EvictableListTest.js │ │ │ │ ├── Recoil_DevTools_GraphUtils.test.js │ │ │ │ ├── Recoil_DevTools_TXHashtable.test.js │ │ │ │ └── SerializationTest.js │ │ │ ├── debounce.js │ │ │ ├── getStyle.js │ │ │ └── sankey/ │ │ │ ├── CV2_D3.js │ │ │ ├── CV2_memoize.js │ │ │ ├── Sankey.js │ │ │ ├── SankeyGraph.js │ │ │ ├── SankeyGraphLayout.js │ │ │ ├── compactArray.js │ │ │ └── isImmutable.js │ │ ├── utils/ │ │ │ ├── build.js │ │ │ ├── env.js │ │ │ └── webserver.js │ │ └── webpack.config.js │ └── todo-example/ │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public/ │ │ ├── index.html │ │ ├── manifest.json │ │ └── robots.txt │ └── src/ │ ├── App.css │ ├── App.js │ ├── components/ │ │ └── Todo/ │ │ ├── TodoItem.jsx │ │ ├── TodoItemCreator.jsx │ │ ├── TodoList.jsx │ │ ├── TodoListFilters.jsx │ │ ├── TodoListStats.jsx │ │ ├── Todo_state.js │ │ └── Todo_types.js │ ├── index.css │ └── index.js ├── relay.config.js ├── rollup.config.js ├── scripts/ │ ├── build.mjs │ ├── deploy_nightly_build.js │ ├── pack.mjs │ ├── project-root-dir.mjs │ ├── rollup-configs.mjs │ └── utils.mjs ├── setupJestMock.js └── typescript/ ├── index.d.ts ├── recoil-relay-test.ts ├── recoil-relay.d.ts ├── recoil-sync-test.ts ├── recoil-sync.d.ts ├── recoil-test.ts ├── recoil.d.ts ├── refine-test.ts ├── refine.d.ts ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ node_modules yarn.lock docs cjs/ es/ native/ recoil_devtools_ext/ ================================================ FILE: .eslintrc.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ const rulesDirPlugin = require('eslint-plugin-rulesdir'); const path = require('path'); const OFF = 0; const WARNING = 1; const ERROR = 2; rulesDirPlugin.RULES_DIR = path.resolve(__dirname, './eslint-rules'); module.exports = { env: { browser: true, es2020: true, }, extends: ['plugin:react/recommended'], parser: 'hermes-eslint', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 11, sourceType: 'module', }, plugins: ['flowtype', 'react', 'jest', 'fb-www', 'rulesdir', 'relay'], rules: { strict: 0, 'jsx-a11y/href-no-hash': OFF, 'react/jsx-key': OFF, 'react/prop-types': OFF, 'rulesdir/no-fb-only': OFF, }, settings: { react: { version: 'detect', }, }, }; ================================================ FILE: .flowconfig ================================================ [ignore] .*/__tests__.* .*/node_modules.* /build/.* /cjs/.* # Disable for now as the NPM dependencies don't seem to be working... /packages/recoil-sync/.* /packages/recoil-relay/.* /packages-ext/todo-example/.* /packages-ext/recoil-devtools/.* [include] [libs] node_modules/flow-interfaces-chrome/interfaces [options] module.system=node suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FlowFixMeEmpty suppress_type=$FlowOSSFixMe module.name_mapper='React' -> '/node_modules/react' module.name_mapper='ReactDOMLegacy_DEPRECATED' -> '/node_modules/react-dom' module.name_mapper='ReactNative' -> '/node_modules/react-native' module.name_mapper='ReactTestUtils' -> '/node_modules/react-dom/test-utils' module.name_mapper='Recoil' -> '/packages/recoil' module.name_mapper='recoil' -> '/packages/recoil' module.name_mapper='recoil-sync' -> '/packages/recoil-syync' module.name_mapper='recoil-relay' -> '/packages/recoil-relay' module.name_mapper='refine' -> '/packages/refine' module.name_mapper='recoil-shared' -> '/packages/shared' module.name_mapper='relay-runtime' -> '/node_modules/relay-runtime/index.js.flow' module.name_mapper='react-relay' -> '/node_modules/react-relay' module.name_mapper='relay-test-utils' -> '/node_modules/relay-test-utils' exact_by_default=true babel_loose_array_spread=true enums=true [strict] deprecated-type sketchy-null unclear-type unsafe-getters-setters untyped-import untyped-type-import [lints] all=warn [version] ^0.207.0 ================================================ FILE: .github/workflows/nightly.yml ================================================ name: Nightly Push on: schedule: - cron: '0 8 * * *' # every day at 8 am UTC / 1 am PDT / 4 am EDT jobs: deploy-nightly: if: github.repository_owner == 'facebookexperimental' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 # config GIT so it can do git push - uses: fregante/setup-git-user@v1 - name: Use Node.js 16.x uses: actions/setup-node@v1 with: node-version: 16.x - run: npm i -g yarn - run: yarn install --frozen-lockfile - run: yarn deploy-nightly ================================================ FILE: .github/workflows/nodejs.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: node-version: [16.x, 18.x] os: [ubuntu-latest, windows-latest, macOS-latest] react: ['17.0.2', '18.1.0'] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm i -g yarn - run: cd packages-ext/recoil-devtools && yarn install --frozen-lockfile - run: yarn install --frozen-lockfile - name: Install React (Unix/Mac) run: > sed -i'' -e 's/\("react\(-dom\)\{0,1\}\(-test-renderer\)\{0,1\}": "\).*"/\1${{ matrix.react }}"/g' package.json if: matrix.os != 'windows-latest' - name: Install React (Windows) run: > sed -i 's/\(""react\(-dom\)\\{0,1\\}\(-test-renderer\)\\{0,1\\}"": ""\).*""/\1${{ matrix.react }}""/g' package.json if: matrix.os == 'windows-latest' - run: yarn install - run: yarn flow - run: yarn test:typescript - run: yarn test - run: yarn lint - run: yarn build - run: yarn run pack - name: Create package uses: actions/upload-artifact@v3 with: name: package path: "build/recoil/*.tgz" ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # build outputs build cjs es native umd /index.d.ts nightly-build-files # Mac .DS_Store # NPM Package lock since we use yarn.lock package-lock.json # Generated documentation from the docs branch /docs # Generated files for recoil-relay unit tests packages/recoil-relay/__tests__/mock-graphql/__generated__ ================================================ FILE: .prettierignore ================================================ node_modules/ build/ cjs/ es/ umd/ flow-typed/ recoil_devtools_ext/ yarn.lock script/project-root-dir.js *.md ================================================ FILE: .prettierrc ================================================ { "arrowParens": "avoid", "singleQuote": true, "trailingComma": "all", "bracketSpacing": false, "bracketSameLine": true, "overrides": [ { "files": ["*.js"], "options": { "parser": "flow" } }, { "files": ["*.ts"], "options": { "parser": "typescript" } } ] } ================================================ FILE: .watchmanconfig ================================================ {} ================================================ FILE: CHANGELOG-recoil-relay.md ================================================ # Change Log ## UPCOMING **_Add new changes here as they land_** ## 0.1.0 (2022-06-2) Initial open source release ================================================ FILE: CHANGELOG-recoil-sync.md ================================================ # Change Log ## UPCOMING **_Add new changes here as they land_** - Migrate from deprecated `substr()` to `substring()` for browser support (#2089) ## 0.2.0 (2022-10-06) - Export `updateItems()` for the `listen` prop callback in `` in addition to `updateItem()` and `updateAllKnownItems()`. (#2017, #2035) - Removing parameter from URL will reset atoms when using location `queryParams` with a `param`. This is also a slight breaking change when an atom might sync with multiple URL params. (#1900, #1976) - Add dev warning if unstable `` `handlers` prop is detected. (#2044) ## 0.1.1 (2022-08-18) - Use `@recoiljs/refine` version 0.1.1 with fixes. ## 0.1.0 (2022-06-21) Initial open source release ================================================ FILE: CHANGELOG-recoil.md ================================================ # Change Log ## UPCOMING **_Add new changes here as they land_** ## 0.7.7 (2023-03-01) - Fix potential unhandled promise rejection in `useRecoilCallback()` (#2075) - Add OSS support for GateKeeper feature toggling via `RecoilEnv.RECOIL_GKS_ENABLED` (#2078) - Fix resolving suspense of async selectors used with SSR (#2073, #1960) - Fix SSR with some versions of React DOM used with Next.JS 13 (#2082, #2086) ## 0.7.6 (2022-10-06) - Workaround for React 18 environments with nested renderers that don't support `useSyncExternalStore()` (#2001, #2010) - Expose flag to disable "duplicate atom key" checking / logging, as it was too noisy in environments such as NextJS. (#733, #2020, #2046) - Import `RecoilEnv` from the recoil package, and set `RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = false` in code to disable the checking and logging. - We also support `process.env.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false` in NodeJS environments such as NextJs - Caution: This does infact disable all checks for duplicate atom keys, so use with caution! ## 0.7.5 (2022-08-11) - Fix useRecoilSnapshot() with React's Fast Refresh during development (#1891) - Fix useRecoilSnapshot() and recoil-sync with changed browser behavior starting with Chrome v104 (#1943, #1936) ## 0.7.4 (2022-06-21) - Fix missing flow types (#1857) - Cleanup memory leak when using atoms with selector defaults. (#1821, #1840, #1844) ## 0.7.3 (2022-06-01) - Atom effects can initialize or set atoms to wrapped values (#1681) - Add `parentStoreID_UNSTABLE` to atom effects which is the ID of the parent store it cloned from, such as the host `` store for `useRecoilCallback()` snapshots. (#1744) - Enable atoms and selectors to be used in family parameters (#1740) ## 0.7.2 (2022-04-13) - Selector cache lookup optimizations (#1720, #1736) - Allow async selectors to re-evaluate when async dependencies are discovered with stale state (#1736) ## 0.7.1 (2022-04-12) ### Typing - Add explicit `children` prop to `` and `useRecoilBridgeAcrossReactRoots_UNSTABLE()` for TypeScript for `@types/react` with React 18 (#1718, #1717, #1726, #1731) - Update typing for family parameters to better support Map, Set, and classes with `toJSON()`. (#1709, #1703) ### Fixes - Avoid dev-mode console error with React 18 when using shared async selectors across multiple ``'s. (#1712) - Cleanup potential memory leak when using async selectors (#1714) - Fix potentially hung async selectors when shared across multiple roots that depend on atoms initialized with promises that don't resolve (#1714) ## 0.7 (2022-03-31) ### New Features - The `default` value is now optional for `atom()` and `atomFamily()`. If not provided the atom will initialize to a pending state. (#1639) - Add `getStoreID()` method to `Snapshot` (#1612) - Publish `RecoilLoadable.loading()` factory for making an async `Loadable` which never resolves. (#1641) ### Breaking Changes - Selector's `get()` and Atom's `default` can now accept a `Loadable` to put the node in that state. If you wish to store a `Loadable`, `Promise`, or `RecoilValue` directly you can wrap it with `selector.value()` or `atom.value()`. (#1640) - `useRecoilCallback()` now provides a snapshot for the latest state instead of the latest rendered state, which had bugs (#1610, #1604) ### Improvements / Optimizations - Automatically retain snapshots for the duration of async callbacks. (#1632) - Optimization for more selector dependencies. 2x improvement with 100 dependencies, 4x with 1,000, and now able to support 10,000+. (#1651, #1515, #914) - Better error reporting when selectors provide inconsistent results (#1696) ### Fixes - Avoid spurious console errors from effects when calling `setSelf()` from `onSet()` handlers. (#1589, #1582) - Freezing user values in dev mode now works in JS environments without the `Window` interface. (#1571) ## 0.6.1 (2022-01-29) - Fix postInstall script (#1577) ## 0.6 (2022-01-28) - React 18 - Leverage new React 18 APIs for improved safety and optimizations. (#1488) - Fixes for `` (#1473, #1444, #1509). - Experimental support for `useTransition()` using hooks with `_TRANSITION_SUPPORT_UNSTABLE` suffix. (#1572, #1560) - Recoil updates now re-render earlier: - Recoil and React state changes from the same batch now stay in sync. (#1076) - Renders now occur before transaction observers instead of after. ### New Features - Add `refresh()` to the `useRecoilCallback()` interface for refreshing selector caches. (#1413) - Callbacks from selector's `getCallback()` can now mutate, refresh, and transact Recoil state, in addition to reading it, for parity with `useRecoilCallback()`. (#1498) - Recoil StoreID's for `` and `Snapshot` stores accessible via `useRecoilStoreID()` hook (#1417) or `storeID` parameter for atom effects (#1414). - `RecoilLoadable.all()` and `RecoilLoadable.of()` now accept either literal values, async Promises, or Loadables. (#1455, #1442) - Add `.isRetained()` method for Snapshots and check if snapshot is already released when using `.retain()` (#1546) ### Other Fixes and Optimizations - Reduce overhead of snapshot cloning - Only clone the current snapshot for callbacks if the callback actually uses it. (#1501) - Cache the cloned snapshots from callbacks unless there was a state change. (#1533) - Fix transitive selector refresh for some cases (#1409) - Fix some corner cases with async selectors and multiple stores (#1568) - Atom Effects - Run atom effects when atoms are initialized from a set during a transaction from `useRecoilTransaction_UNSTABLE()` (#1466, #1569) - Atom effects are cleaned up when initialized by a Snapshot which is released. (#1511, #1532) - Unsubscribe `onSet()` handlers in atom effects when atoms are cleaned up. (#1509) - Call `onSet()` when atoms are initialized with `` (#1519, #1511) - Avoid extra re-renders in some cases when a component uses a different atom/selector. (#825) - `` will only call `initializeState()` once during the initial render. (#1372) - Lazily compute the properties of `useGetRecoilValueInfo_UNSTABLE()` and `Snapshot#getInfo_UNSTABLE()` results (#1549) - Memoize the results of lazy proxies. (#1548) ### Breaking Changes - Rename atom effects from `effects_UNSTABLE` to just `effects`, as the interface is mostly stabilizing. (#1520) - Atom effect initialization takes precedence over initialization with ``. (#1509) - `useGetRecoilValueInfo_UNSTABLE()` and `Snapshot#getInfo_UNSTABLE()` always report the node `type`. (#1547) ## 0.5.2 (2021-11-07) - Fix TypeScript exports (#1397) ## 0.5.1 (2021-11-05) - Fix TypeScript exports (#1389) ## 0.5.0 (2021-11-03) - Added `useRecoilRefresher_UNSTABLE()` hook which forces a selector to re-run it's `get()`, and is a no-op for an atom. (#972, #1294, #1302) - Atom effect improvements: - Add `getLoadable()`, `getPromise()`, and `getInfo_UNSTABLE()` to Atom Effects interface for reading other atoms. (#1205, #1210) - Add `isReset` parameter to `onSet()` callback to know if the atom is being reset or not. (#1358, #1345) - `Loadable` improvements: - Publish `RecoilLoadable` interface with factories and type checking for Loadables. (#1263, #1264, #1312) - Ability to map Loadables with other Loadables. (#1180) - Re-implement Loadable as classes. (#1315) - Improved dev-mode checks: - Atoms freeze default, initialized, and async values. Selectors should not freeze upstream dependencies. (#1261, #1259) - Perform runtime check that required options are provided when creating atoms and selectors. (#1324) - Fix user-thrown promises in selectors for some cases. - Allow class instances in family parameters for Flow ## 0.4.1 (2021-08-26) - Performance optimizations to suppress re-rendering components: - When subscribed selectors evaluate to the same value. (#749, #952) - On initial render when not using React Concurrent Mode (#820) - When selector async deps resolve, but React re-renders before chained promises have executed. - Fixed #1072 where in some cases selectors with async deps would not update in response to state updates ## 0.4 (2021-07-30) ### New Features - Selector cache configuration: introduced `cachePolicy_UNSTABLE` option for selectors and selector families. This option allows you to control the behavior of how the selector evicts entries from its internal cache. - Improved `useRecoilTransaction_UNSTABLE()` hook for transactions with multiple atoms (#1085) ### Fixes and Optimizations - Fix TypeScript typing for `selectorFamily()`, `getCallback()`, `useGetRecoilValueInfo()`, and `Snapshot#getNodes()` (#1060, #1116, #1123) - Allow mutable values in selectors to be used with waitFor\*() helpers (#1074, #1096) - Atom Effects fixes: - Fix onSet() handler to get the proper new value when an atom is reset or has an async default Promise that resolves (#1059, #1050, #738) (Slightly breaking change) - Fix support for multiple Atom effects cleanup handlers (#1125) - Fix selector subscriptions when atoms with effects are initialized via a `Snapshot` (#1135, #1107) - Optimization for async selectors when dependencies resolve to cached values (#1037) - Remove unnecessary warning message (#1034, #1062) ## 0.3.1 (2021-5-18) - Fix TypeScript exports ## 0.3.0 (2021-5-14) For supporting garbage collection in the future there is a slight breaking change that `Snapshot`'s will only be valid for the duration of the callback or render. A new `retain()` API can be used to persist them longer. This is not enforced yet, but Recoil will now provide a warning in dev-mode if a `Snapshot` is used past its lifetime. (#1006) ### New Features / Improvements - Add `override` prop to `` (#973) - Add `getCallback()` to selector evaluation interface (#989) - Improved TypeScript and Flow typing for `Loadable`s (#966, #1022) ### Performance Optimizations - Improve scalability (time and memory) of Atom families by cleaning up a legacy feature. ### Bug Fixes - Throwing an error in an async selector should be properly caught by ``'s (#998, #1017) - Fix for Atom Effects `onSet()` should not be called when triggered from `setSelf()` initializing with a Promise or from the same `onSet()` handler. (#974, #979, #953, #986) - Improved support for Safari (#967, #609) - Objects stored in selectors are properly frozen in dev mode (#911) ## 0.2.0 (2021-3-18) ### Major improvements - More reliable async selectors - Improved performance using HAMT data structures (b7d1cfddec66dae). ### Other improvements - Changed semantics of waitForAny() such that it will always return loadables unless everything is loading. This better aligns behaviour of waitForAny() and waitForNone() - Added a waitForAllSettled helper analogous to Promise.allSettled. (4c95591) - Friendly error message for misuse of useRecoilCallback (#870) - Friendly error message if you try to use an async function as a selector setter, which is not supported. (#777) - Improved React Native support. (#748, #702) - Added useGetRecoilValueInfo_UNSTABLE() hook for dev tools. (#713, #714) ### Bug fixes - Selectors now treat any non-Promise that is thrown as an error, rather than only instances of Error. (f0e66f727) - A child of RecoilRoot could sometimes have its state updated after being unmounted. (#917) - The error message for missing RecoilRoot wasn't displayed on React experimental releases. (#712) - IE 11 compatibility (#894, d27c800d8) - Errors shouldn't be frozen (#852) - Atom effects could fail to initialize atoms in certain cases (#775). - Async selectors would fail with multiple React roots (a618a3). ## 0.1.3 (2021-3-2) - Fixed peer dependencies ## 0.1.2 (2020-10-30) - Fix TypeScript exports ## 0.1.1 (2020-10-29) - Performance Improvements - Experimental React Native support - Experimental Atom Effects - Experimental Snapshot construction ## 0.0.13 (2020-09-16) - Fix for bug affecting SSR ## 0.0.12 (2020-09-15) - Fix for bug affecting SSR on development builds ## 0.0.11 (2020-09-15) - Experimental React Concurrent Mode Support - Performance - Flow Types - ES, CommonJS, and UMD packages - Synchronization Across React Roots - Preliminary Developer Tools API - Test Infrastructure Fixes ## 0.0.10 (2020-06-18) ### Bug Fix - Fix exports for snapshot hooks ## 0.0.9 (2020-06-17) ### Features - TypeScript support now rolled into Recoil repository and package. - Recoil Snapshot API for observing and managing global Recoil state. ### Improvements - Throw error with meaningful message if user doesn't use an atom or selector with most Recoil hooks (#205) - Thanks @alexandrzavalii - Improve testing (#321, #318, #294, #262, #295) - Thanks @aaronabramov, @Komalov, @mondaychen, @drarmstr, and @tyler-mitchell - Improve open-source build (#249, #203, #33) - Thanks to @tony-go, @acutmore, and @jaredpalmer ### Bug Fixes - Some fixes for Server Side Rendering, though we do not officially support it yet. (#233, #220, #284) - Thanks @fyber-LJX, @Chrischuck, and @aulneau - Fix selectors recording dependency subscriptions in some cases (#296) - Thanks @drarmstr - Fix updaters in `useRecoilCallback()` getting current state (#260) - Thanks @drarmstr - Fix error messages when throwing certain errors in the open-source build. (#199) - Thanks @jonthomp - Reduce Flow errors for open-source builds (#308) - Thanks @Komalov ## 0.0.8 (2020-05-30) ### Bug Fixes - Build system and repository syncing fixed. - Fixed a bug where atoms that stored self-referential structures would cause an infinite loop. (#153) - Fixed bugs affecting Server-Side Rendering. (#53) ### Features - TypeScript support is now available via DefinitelyTyped. - `atomFamily` and `selectorFamily`: these provide a standard way to create atoms and selectors using memoized functions. Compared with doing this yourself, in the future these will help with memory management. - `noWait`, `waitForNone`, `waitForAny`, `waitForAll`: helpers for concurrency and other advanced logic in async selectors. - `constSelector` and `errorSelector`: selectors that always evaluate to a constant or always throw an error. - `readOnlySelector`: wraps a read-write atom or selector in a read-only interface, for when you need type covariance. ================================================ FILE: CHANGELOG-refine.md ================================================ # Change Log ## UPCOMING **_Add new changes here as they land_** ## 0.1.1 (2022-08-17) - Rename `boolean()` export to `bool()` since `boolean` is a reserved word (#1922, #1962, #1971) - Remove reference to `native` directory in `package.json` to cleanup errors for `react-native`. (#1931) - Export `Path` class for custom checkers. (#1950, #1956) - Extend the failure message of `union()` and `or()` with each type. (#1961) ## 0.1.0 (2022-06-21) Initial open source release ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. This Code of Conduct also applies outside the project spaces when there is a reasonable belief that an individual's behavior may have a negative impact on the project or its community. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Recoil We want to make contributing to this project as easy and transparent as possible. ## Our Development Process Some people will be working directly on GitHub. These changes will be public from the beginning. Other changesets will come via a bridge with Facebook's internal source control. This is a necessity as it allows engineers at Facebook outside of the core team to move fast and contribute from an environment they are comfortable in. ## The `main` Branch is Unsafe We will do our best to keep main in good shape, with tests passing at all times. But we will sometimes make API changes that your application might not be compatible with. We will do our best to communicate these changes and always version appropriately so you can lock into a specific version if need be. ## Pull Requests We actively welcome your pull requests. 1. Fork the repo and create your branch from `main`. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. 4. Ensure the test suite passes. 5. Make sure your code lints and is formatted with `prettier`. Run `yarn format` to run `prettier` on all files. 6. If you haven't already, complete the Contributor License Agreement ("CLA"). ## Getting in Touch Please file issues liberally. That's the easiest way to contact us in a way that ensures everyone working on Recoil can see it. We are eager for your questions, input, and to hear about your experience. ## Contributor License Agreement ("CLA") In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: ## Issues We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. ## License By contributing to Recoil, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Meta Platforms, Inc. and affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README-recoil-relay.md ================================================ # Recoil Relay · [![NPM Version](https://img.shields.io/npm/v/recoil-relay)](https://www.npmjs.com/package/recoil-relay) [![Node.js CI](https://github.com/facebookexperimental/Recoil/workflows/Node.js%20CI/badge.svg)](https://github.com/facebookexperimental/Recoil/actions) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebookexperimental/Recoil/blob/main/LICENSE) [![Follow on Twitter](https://img.shields.io/twitter/follow/recoiljs?label=Follow%20Recoil&style=social)](https://twitter.com/recoiljs) The `recoil-relay` library helps [Recoil](https://recoiljs.org) perform type safe and efficient queries using [GraphQL](https://graphql.org/) with the [Relay](https://relay.dev) library. Please see the [**Recoil Relay GraphQL Documentation**](https://recoiljs.org/docs/recoil-relay/introduction) `recoil-relay` provides `graphQLSelector()` and `graphQLSelectorFamily()` selectors which can easily query with GraphQL. The queries are synced with the Recoil data-flow graph so downstream selectors can derive state from them, they can depend on upstream Recoil state, and they are automatically subscribed to any changes in the graph from Relay. Everything stays in sync automatically. ## Example After setting up your Relay environment adding a GraphQL query is as simple as defining a [GraphQL selector](https://recoiljs.org/docs/recoil-relay/graphql-selectors). ```jsx const userNameQuery = graphQLSelector({ key: 'UserName', environment: myEnvironment, query: graphql` query UserQuery($id: ID!) { user(id: $id) { name } } `, variables: ({get}) => ({id: get(currentIDAtom)}), mapResponse: data => data.user?.name, }); ``` Then use it like any other Recoil [selector](https://recoiljs.org/docs/api-reference/core/selector): ```jsx function MyComponent() { const userName = useRecoilValue(userNameQuery); return {userName}; } ``` ## Installation Please see the [Recoil installation guide](https://recoiljs.org/docs/introduction/installation) for installing Recoil and the [Relay documentation](https://relay.dev/docs/getting-started/installation-and-setup/) for installing and setting up the Relay library, GraphQL compiler, Babel plugin, and ESLint plugin. Then add `recoil-relay` as a dependency. ## Contributing Development of Recoil happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Recoil. - [Code of Conduct](./CODE_OF_CONDUCT.md) - [Contributing Guide](./CONTRIBUTING.md) ### License Recoil is [MIT licensed](./LICENSE). ================================================ FILE: README-recoil-sync.md ================================================ # Recoil Sync · [![NPM Version](https://img.shields.io/npm/v/recoil-sync)](https://www.npmjs.com/package/recoil-sync) [![Node.js CI](https://github.com/facebookexperimental/Recoil/workflows/Node.js%20CI/badge.svg)](https://github.com/facebookexperimental/Recoil/actions) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebookexperimental/Recoil/blob/main/LICENSE) [![Follow on Twitter](https://img.shields.io/twitter/follow/recoiljs?label=Follow%20Recoil&style=social)](https://twitter.com/recoiljs) The `recoil-sync` package provides an add-on library to help synchronize [Recoil](https://recoiljs.org/) state with external systems. Please see the [**Recoil Sync Documentation**](https://recoiljs.org/docs/recoil-sync/introduction) In Recoil, simple [asynchronous data queries](https://recoiljs.org/docs/guides/asynchronous-data-queries) can be implemented via selectors or `useEffect()` and [atom effects](https://recoiljs.org/docs/guides/atom-effects) can be used for bi-directional syncing of individual atoms. The `recoil-sync` add-on package provides some additional functionality: * **Batching Atomic Transactions** - Updates for multiple atoms can be batched together as a single transaction with the external system. This can be important if an atomic transaction is required for consistent state of related atoms. * **Abstract and Flexible** - This API allows users to specify what atoms to sync separately from describing the mechanism of how to sync. This allows components to use atoms and sync with different systems in different environments without changing their implementation. For example, a component may use atoms that persist to the URL when used in a stand-alone tool while persisting to a custom user database when embedded in another tool. * **Validation and Backward Compatibility** - When dealing with state from external sources it is important to validate the input. When state is persisted beyond the lifetime of an app it can also be important to consider backward compatibility of previous versions of state. `recoil-sync` and [`refine`](https://recoiljs.org/docs/refine/introduction) help provide this functionality. * **Complex Mapping of Atoms to External Storage** - There may not be a one-to-one mapping between atoms and external storage items. Atoms may migrate to use newer versions of items, may pull props from multiple items, just a piece of some compound state, or other complex mappings. * **Sync with React Hooks or Props** - This library enables syncing atoms with React hooks or props that are not accessible from atom effects. The `recoil-sync` library also provides built-in implementations for external stores, such as [syncing with the browser URL](https://recoiljs.org/docs/recoil-sync/url-persistence). --- The basic idea is that a [`syncEffect()`](https://recoiljs.org/docs/recoil-sync/sync-effect) can be added to each atom that you wish to sync, and then a [``](https://recoiljs.org/docs/recoil-sync/api/RecoilSync) is added inside your `` to specify how to sync those atoms. You can use built-in stores such as [``](https://recoiljs.org/docs/recoil-sync/url-persistence), [make your own](https://recoiljs.org/docs/recoil-sync/implement-store), or sync different groups of atoms with different stores. ## Example ### URL Persistence Here is a simple example [syncing an atom with the browser URL](https://recoiljs.org/docs/recoil-sync/url-persistence): ```jsx const currentUserState = atom({ key: 'CurrentUser', default: 0, effects: [ syncEffect({ refine: number() }), ], }); ``` Then, at the root of your application, simply include `` to sync all of those tagged atoms with the URL ```jsx function MyApp() { return ( ... ) } ``` That's it! Now this atom will initialize its state based on the URL during initial load, any state mutations will update the URL, and changes in the URL (such as the back button) will update the atom. See more examples in the [Sync Effect](https://recoiljs.org/docs/recoil-sync/sync-effect), [Store Implementation](https://recoiljs.org/docs/recoil-sync/implement-store), and [URL Persistence](https://recoiljs.org/docs/recoil-sync/url-persistence) guides. ## Installation Please see the [Recoil installation guide](https://recoiljs.org/docs/introduction/installation) and add `recoil-sync` as an additional dependency. `recoil-sync` also includes the [`refine`](https://recoiljs.org/docs/refine/introduction) library. ## Contributing Development of Recoil happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Recoil. - [Code of Conduct](./CODE_OF_CONDUCT.md) - [Contributing Guide](./CONTRIBUTING.md) ### License Recoil is [MIT licensed](./LICENSE). ================================================ FILE: README-refine.md ================================================ # Refine · [![NPM Version](https://img.shields.io/npm/v/@recoiljs/refine)](https://www.npmjs.com/package/@recoiljs/refine) [![Node.js CI](https://github.com/facebookexperimental/Recoil/workflows/Node.js%20CI/badge.svg)](https://github.com/facebookexperimental/Recoil/actions) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebookexperimental/Recoil/blob/main/LICENSE) [![Follow on Twitter](https://img.shields.io/twitter/follow/recoiljs?label=Follow%20Recoil&style=social)](https://twitter.com/recoiljs) **Refine** is a type-refinement / validator combinator library for mixed / unknown values in Flow or TypeScript. Refine is currently released as [@recoiljs/refine](https://www.npmjs.com/package/@recoiljs/refine) on NPM. Please see the [**Refine Documentation**](https://recoiljs.org/docs/refine/Introduction). To get started learning about Refine, check out the documentation on the core concepts of [Utilities](https://recoiljs.org/docs/refine/api/Utilities) and [Checkers](https://recoiljs.org/docs/refine/api/Checkers). ## Why would I want to use Refine? - Refine is useful when your code encounters `unknown` TypeScript type or `mixed` Flow type values and you need to [assert those values have a specific static type](https://recoiljs.org/docs/refine/Introduction#type-refinement-example). - Refine provides an API for building type-refinement helper functions which can validate that an unknown value conforms to an expected type. - Refine can validate input values and [upgrade from previous versions](https://recoiljs.org/docs/refine/Introduction#backward-compatible-example). ## Type Refinement Example Coerce unknown types to a strongly typed variable. [`assertion()`](https://recoiljs.org/docs/refine/api/Utilities#assertion) will throw if the input doesn't match the expected type while [`coercion()`](https://recoiljs.org/docs/refine/api/Utilities#coercion) will return `null`. ```jsx const myObjectChecker = object({ numberProperty: number(), stringProperty: optional(string()), arrayProperty: array(number()), }); const myObjectAssertion = assertion(myObjectChecker); const myObject: CheckerReturnType = myObjectAssertion({ numberProperty: 123, stringProperty: 'hello', arrayProperty: [1, 2, 3], }); ``` ## Backward Compatible Example Using [`match()`](https://recoiljs.org/docs/refine/api/Advanced_Checkers#match) and [`asType()`](https://recoiljs.org/docs/refine/api/Advanced_Checkers#asType) you can upgrade from previous types to the latest version. ```jsx const myChecker: Checker<{str: string}> = match( object({str: string()}), asType(string(), str => ({str: str})), asType(number(), num => ({str: String(num)})), ); const obj1: {str: string} = coercion(myChecker({str: 'hello'})); const obj2: {str: string} = coercion(myChecker('hello')); const obj3: {str: string} = coercion(myChecker(123)); ``` ## JSON Parser Example Refine wraps `JSON` to provide a built-in strongly typed parser. ```jsx const myParser = jsonParser(array(object({num: number()}))); const result = myParser('[{"num": 1}, {"num": 2}]'); if (result != null) { // we can now access values in num typesafe way assert(result[0].num === 1); } else { // value failed to match parser spec } ``` ## Usage in Recoil Sync The **Recoil Sync** library leverages **Refine** for type refinement, input validation, and upgrading types for backward compatibility. See the [`recoil-sync` docs](https://recoiljs.org/docs/recoil-sync/introduction) for more details. ## Installation Refine is currently bundled as part of the [Recoil Sync](https://recoiljs.org/docs/recoil-sync/introduction) package. ## Contributing Development of Recoil happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Recoil. - [Code of Conduct](./CODE_OF_CONDUCT.md) - [Contributing Guide](./CONTRIBUTING.md) ### License Recoil is [MIT licensed](./LICENSE). ================================================ FILE: README.md ================================================ # Recoil · [![NPM Version](https://img.shields.io/npm/v/recoil)](https://www.npmjs.com/package/recoil) [![Node.js CI](https://github.com/facebookexperimental/Recoil/workflows/Node.js%20CI/badge.svg)](https://github.com/facebookexperimental/Recoil/actions) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebookexperimental/Recoil/blob/main/LICENSE) [![Follow on Twitter](https://img.shields.io/twitter/follow/recoiljs?label=Follow%20Recoil&style=social)](https://twitter.com/recoiljs) Recoil is an experimental state management framework for React. Website: https://recoiljs.org ## Documentation Documentation: https://recoiljs.org/docs/introduction/core-concepts API Reference: https://recoiljs.org/docs/api-reference/core/RecoilRoot Tutorials: https://recoiljs.org/resources ## Installation The Recoil package lives in [npm](https://www.npmjs.com/get-npm). Please see the [installation guide](https://recoiljs.org/docs/introduction/installation) To install the latest stable version, run the following command: ```shell npm install recoil ``` Or if you're using [yarn](https://classic.yarnpkg.com/en/docs/install/): ```shell yarn add recoil ``` Or if you're using [bower](https://bower.io/#install-bower): ```shell bower install --save recoil ``` ## Contributing Development of Recoil happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Recoil. - [Code of Conduct](./CODE_OF_CONDUCT.md) - [Contributing Guide](./CONTRIBUTING.md) ### License Recoil is [MIT licensed](./LICENSE). ================================================ FILE: babel.config.json ================================================ { "presets": ["@babel/react", "@babel/flow"], "plugins": [ [ "module-resolver", { "root": ["./"], "alias": { "React": "react", "ReactDOMLegacy_DEPRECATED": "react-dom", "ReactNative": "react-native", "ReactTestUtils": "react-dom/test-utils" } } ], "relay", "babel-preset-fbjs/plugins/dev-expression", "@babel/plugin-proposal-class-properties", "@babel/proposal-nullish-coalescing-operator", "@babel/proposal-optional-chaining", "@babel/transform-flow-strip-types" ] } ================================================ FILE: eslint-rules/no-fb-only.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ // Simple eslint rule that warns on usage of `@fb-only` style comments. // These comments introduce a split point where code behaves differently // depending on whether it's run at facebook or in OSS and it's very difficult // to debug, especially if the person working on that code does not have access // to facebook's internal code. To minimize that complexity all of these split // points should be defined in one place and dependency injected in Recoil // at runtime. const regexps = [ [/^.*(@fb-only).*$/, '@fb-only'], [/^.*(@oss-only).*$/, '@oss-only'], ]; // If a file paths matches any of provided regular expressions it will be // excluded from this lint rule. const excludePaths = [/.*eslint-rules.*/]; module.exports = { meta: { docs: { description: 'disallow @fb-only style comments', }, }, create(context) { return { // Only match the top level `Program` AST node that represents the entire file. Program(node) { const lines = context.getSourceCode().text.split('\n'); const filename = context.getFilename(); if (excludePaths.some(r => r.test(filename))) { return; } lines.forEach((line, lineNumber) => { for (const [regexp, descriptor] of regexps) { const match = line.match(regexp); if (match) { context.report({ message: `Usage of "${descriptor}". Please consider dependency injecting this condition ` + `instead. See "${__filename}" for more details`, loc: { start: {line: lineNumber + 1, column: 0}, end: {line: lineNumber + 1, column: line.length - 1}, }, }); } } }); }, }; }, }; ================================================ FILE: flow-typed/jest.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ type JestMockFn, TReturn> = { (...args: TArguments): TReturn, /** * An object for introspecting mock calls */ mock: { /** * An array that represents all calls that have been made into this mock * function. Each call is represented by an array of arguments that were * passed during the call. */ calls: Array, /** * An array that contains all the object instances that have been * instantiated from this mock function. */ instances: Array, /** * An array that contains all the object results that have been * returned by this mock function call */ results: Array<{ isThrow: boolean, value: TReturn }>, }, /** * Resets all information stored in the mockFn.mock.calls and * mockFn.mock.instances arrays. Often this is useful when you want to clean * up a mock's usage data between two assertions. */ mockClear(): void, /** * Resets all information stored in the mock. This is useful when you want to * completely restore a mock back to its initial state. */ mockReset(): void, /** * Removes the mock and restores the initial implementation. This is useful * when you want to mock functions in certain test cases and restore the * original implementation in others. Beware that mockFn.mockRestore only * works when mock was created with jest.spyOn. Thus you have to take care of * restoration yourself when manually assigning jest.fn(). */ mockRestore(): void, /** * Accepts a function that should be used as the implementation of the mock. * The mock itself will still record all calls that go into and instances * that come from itself -- the only difference is that the implementation * will also be executed when the mock is called. */ mockImplementation( fn: (...args: TArguments) => TReturn ): JestMockFn, /** * Accepts a function that will be used as an implementation of the mock for * one call to the mocked function. Can be chained so that multiple function * calls produce different results. */ mockImplementationOnce( fn: (...args: TArguments) => TReturn ): JestMockFn, /** * Accepts a string to use in test result output in place of "jest.fn()" to * indicate which mock function is being referenced. */ mockName(name: string): JestMockFn, /** * Just a simple sugar function for returning `this` */ mockReturnThis(): void, /** * Accepts a value that will be returned whenever the mock function is called. */ mockReturnValue(value: TReturn): JestMockFn, /** * Sugar for only returning a value once inside your mock */ mockReturnValueOnce(value: TReturn): JestMockFn, /** * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) */ mockResolvedValue(value: TReturn): JestMockFn>, /** * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) */ mockResolvedValueOnce( value: TReturn ): JestMockFn>, /** * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) */ mockRejectedValue(value: TReturn): JestMockFn>, /** * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) */ mockRejectedValueOnce(value: TReturn): JestMockFn>, }; type JestAsymmetricEqualityType = { /** * A custom Jasmine equality tester */ asymmetricMatch(value: mixed): boolean, }; type JestCallsType = { allArgs(): mixed, all(): mixed, any(): boolean, count(): number, first(): mixed, mostRecent(): mixed, reset(): void, }; type JestClockType = { install(): void, mockDate(date: Date): void, tick(milliseconds?: number): void, uninstall(): void, }; type JestMatcherResult = { message?: string | (() => string), pass: boolean, }; type JestMatcher = ( actual: any, expected: any ) => JestMatcherResult | Promise; type JestPromiseType = { /** * Use rejects to unwrap the reason of a rejected promise so any other * matcher can be chained. If the promise is fulfilled the assertion fails. */ // eslint-disable-next-line no-use-before-define rejects: JestExpectType, /** * Use resolves to unwrap the value of a fulfilled promise so any other * matcher can be chained. If the promise is rejected the assertion fails. */ // eslint-disable-next-line no-use-before-define resolves: JestExpectType, }; /** * Jest allows functions and classes to be used as test names in test() and * describe() */ type JestTestName = string | Function; /** * Plugin: jest-styled-components */ type JestStyledComponentsMatcherValue = | string | JestAsymmetricEqualityType | RegExp | typeof undefined; type JestStyledComponentsMatcherOptions = { media?: string, modifier?: string, supports?: string, }; type JestStyledComponentsMatchersType = { toHaveStyleRule( property: string, value: JestStyledComponentsMatcherValue, options?: JestStyledComponentsMatcherOptions ): void, }; /** * Plugin: jest-enzyme */ type EnzymeMatchersType = { // 5.x toBeEmpty(): void, toBePresent(): void, // 6.x toBeChecked(): void, toBeDisabled(): void, toBeEmptyRender(): void, toContainMatchingElement(selector: string): void, toContainMatchingElements(n: number, selector: string): void, toContainExactlyOneMatchingElement(selector: string): void, toContainReact(element: React$Element): void, toExist(): void, toHaveClassName(className: string): void, toHaveHTML(html: string): void, toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: {}) => void), toHaveRef(refName: string): void, toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: {}) => void), toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: {}) => void), toHaveTagName(tagName: string): void, toHaveText(text: string): void, toHaveValue(value: any): void, toIncludeText(text: string): void, toMatchElement( element: React$Element, options?: {| ignoreProps?: boolean, verbose?: boolean |} ): void, toMatchSelector(selector: string): void, // 7.x toHaveDisplayName(name: string): void, }; // DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers type DomTestingLibraryType = { toBeDisabled(): void, toBeEmpty(): void, toBeInTheDocument(): void, toBeVisible(): void, toContainElement(element: HTMLElement | null): void, toContainHTML(htmlText: string): void, toHaveAttribute(name: string, expectedValue?: string): void, toHaveClass(...classNames: string[]): void, toHaveFocus(): void, toHaveFormValues(expectedValues: { [name: string]: any }): void, toHaveStyle(css: string): void, toHaveTextContent( content: string | RegExp, options?: { normalizeWhitespace: boolean } ): void, toBeInTheDOM(): void, }; // Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers type JestJQueryMatchersType = { toExist(): void, toHaveLength(len: number): void, toHaveId(id: string): void, toHaveClass(className: string): void, toHaveTag(tag: string): void, toHaveAttr(key: string, val?: any): void, toHaveProp(key: string, val?: any): void, toHaveText(text: string | RegExp): void, toHaveData(key: string, val?: any): void, toHaveValue(val: any): void, toHaveCss(css: { [key: string]: any }): void, toBeChecked(): void, toBeDisabled(): void, toBeEmpty(): void, toBeHidden(): void, toBeSelected(): void, toBeVisible(): void, toBeFocused(): void, toBeInDom(): void, toBeMatchedBy(sel: string): void, toHaveDescendant(sel: string): void, toHaveDescendantWithText(sel: string, text: string | RegExp): void, }; // Jest Extended Matchers: https://github.com/jest-community/jest-extended type JestExtendedMatchersType = { /** * Note: Currently unimplemented * Passing assertion * * @param {String} message */ // pass(message: string): void; /** * Note: Currently unimplemented * Failing assertion * * @param {String} message */ // fail(message: string): void; /** * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. */ toBeEmpty(): void, /** * Use .toBeOneOf when checking if a value is a member of a given Array. * @param {Array.<*>} members */ toBeOneOf(members: any[]): void, /** * Use `.toBeNil` when checking a value is `null` or `undefined`. */ toBeNil(): void, /** * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. * @param {Function} predicate */ toSatisfy(predicate: (n: any) => boolean): void, /** * Use `.toBeArray` when checking if a value is an `Array`. */ toBeArray(): void, /** * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. * @param {Number} x */ toBeArrayOfSize(x: number): void, /** * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. * @param {Array.<*>} members */ toIncludeAllMembers(members: any[]): void, /** * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. * @param {Array.<*>} members */ toIncludeAnyMembers(members: any[]): void, /** * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. * @param {Function} predicate */ toSatisfyAll(predicate: (n: any) => boolean): void, /** * Use `.toBeBoolean` when checking if a value is a `Boolean`. */ toBeBoolean(): void, /** * Use `.toBeTrue` when checking a value is equal (===) to `true`. */ toBeTrue(): void, /** * Use `.toBeFalse` when checking a value is equal (===) to `false`. */ toBeFalse(): void, /** * Use .toBeDate when checking if a value is a Date. */ toBeDate(): void, /** * Use `.toBeFunction` when checking if a value is a `Function`. */ toBeFunction(): void, /** * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. * * Note: Required Jest version >22 * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same * * @param {Mock} mock */ toHaveBeenCalledBefore(mock: JestMockFn): void, /** * Use `.toBeNumber` when checking if a value is a `Number`. */ toBeNumber(): void, /** * Use `.toBeNaN` when checking a value is `NaN`. */ toBeNaN(): void, /** * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. */ toBeFinite(): void, /** * Use `.toBePositive` when checking if a value is a positive `Number`. */ toBePositive(): void, /** * Use `.toBeNegative` when checking if a value is a negative `Number`. */ toBeNegative(): void, /** * Use `.toBeEven` when checking if a value is an even `Number`. */ toBeEven(): void, /** * Use `.toBeOdd` when checking if a value is an odd `Number`. */ toBeOdd(): void, /** * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). * * @param {Number} start * @param {Number} end */ toBeWithin(start: number, end: number): void, /** * Use `.toBeObject` when checking if a value is an `Object`. */ toBeObject(): void, /** * Use `.toContainKey` when checking if an object contains the provided key. * * @param {String} key */ toContainKey(key: string): void, /** * Use `.toContainKeys` when checking if an object has all of the provided keys. * * @param {Array.} keys */ toContainKeys(keys: string[]): void, /** * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. * * @param {Array.} keys */ toContainAllKeys(keys: string[]): void, /** * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. * * @param {Array.} keys */ toContainAnyKeys(keys: string[]): void, /** * Use `.toContainValue` when checking if an object contains the provided value. * * @param {*} value */ toContainValue(value: any): void, /** * Use `.toContainValues` when checking if an object contains all of the provided values. * * @param {Array.<*>} values */ toContainValues(values: any[]): void, /** * Use `.toContainAllValues` when checking if an object only contains all of the provided values. * * @param {Array.<*>} values */ toContainAllValues(values: any[]): void, /** * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. * * @param {Array.<*>} values */ toContainAnyValues(values: any[]): void, /** * Use `.toContainEntry` when checking if an object contains the provided entry. * * @param {Array.} entry */ toContainEntry(entry: [string, string]): void, /** * Use `.toContainEntries` when checking if an object contains all of the provided entries. * * @param {Array.>} entries */ toContainEntries(entries: [string, string][]): void, /** * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. * * @param {Array.>} entries */ toContainAllEntries(entries: [string, string][]): void, /** * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. * * @param {Array.>} entries */ toContainAnyEntries(entries: [string, string][]): void, /** * Use `.toBeExtensible` when checking if an object is extensible. */ toBeExtensible(): void, /** * Use `.toBeFrozen` when checking if an object is frozen. */ toBeFrozen(): void, /** * Use `.toBeSealed` when checking if an object is sealed. */ toBeSealed(): void, /** * Use `.toBeString` when checking if a value is a `String`. */ toBeString(): void, /** * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. * * @param {String} string */ toEqualCaseInsensitive(string: string): void, /** * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. * * @param {String} prefix */ toStartWith(prefix: string): void, /** * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. * * @param {String} suffix */ toEndWith(suffix: string): void, /** * Use `.toInclude` when checking if a `String` includes the given `String` substring. * * @param {String} substring */ toInclude(substring: string): void, /** * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. * * @param {String} substring * @param {Number} times */ toIncludeRepeated(substring: string, times: number): void, /** * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. * * @param {Array.} substring */ toIncludeMultiple(substring: string[]): void, }; interface JestExpectType { not: JestExpectType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & JestStyledComponentsMatchersType & JestExtendedMatchersType; /** * If you have a mock function, you can use .lastCalledWith to test what * arguments it was last called with. */ lastCalledWith(...args: Array): void; /** * toBe just checks that a value is what you expect. It uses === to check * strict equality. */ toBe(value: any): void; /** * Use .toBeCalledWith to ensure that a mock function was called with * specific arguments. */ toBeCalledWith(...args: Array): void; /** * Using exact equality with floating point numbers is a bad idea. Rounding * means that intuitive things fail. */ toBeCloseTo(num: number, delta: any): void; /** * Use .toBeDefined to check that a variable is not undefined. */ toBeDefined(): void; /** * Use .toBeFalsy when you don't care what a value is, you just want to * ensure a value is false in a boolean context. */ toBeFalsy(): void; /** * To compare floating point numbers, you can use toBeGreaterThan. */ toBeGreaterThan(number: number): void; /** * To compare floating point numbers, you can use toBeGreaterThanOrEqual. */ toBeGreaterThanOrEqual(number: number): void; /** * To compare floating point numbers, you can use toBeLessThan. */ toBeLessThan(number: number): void; /** * To compare floating point numbers, you can use toBeLessThanOrEqual. */ toBeLessThanOrEqual(number: number): void; /** * Use .toBeInstanceOf(Class) to check that an object is an instance of a * class. */ toBeInstanceOf(cls: Class): void; /** * .toBeNull() is the same as .toBe(null) but the error messages are a bit * nicer. */ toBeNull(): void; /** * Use .toBeTruthy when you don't care what a value is, you just want to * ensure a value is true in a boolean context. */ toBeTruthy(): void; /** * Use .toBeUndefined to check that a variable is undefined. */ toBeUndefined(): void; /** * Use .toContain when you want to check that an item is in a list. For * testing the items in the list, this uses ===, a strict equality check. */ toContain(item: any): void; /** * Use .toContainEqual when you want to check that an item is in a list. For * testing the items in the list, this matcher recursively checks the * equality of all fields, rather than checking for object identity. */ toContainEqual(item: any): void; /** * Use .toEqual when you want to check that two objects have the same value. * This matcher recursively checks the equality of all fields, rather than * checking for object identity. */ toEqual(value: any): void; /** * Use .toHaveBeenCalled to ensure that a mock function got called. */ toHaveBeenCalled(): void; toBeCalled(): void; /** * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact * number of times. */ toHaveBeenCalledTimes(number: number): void; toBeCalledTimes(number: number): void; /** * */ toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void; nthCalledWith(nthCall: number, ...args: Array): void; /** * */ toHaveReturned(): void; toReturn(): void; /** * */ toHaveReturnedTimes(number: number): void; toReturnTimes(number: number): void; /** * */ toHaveReturnedWith(value: any): void; toReturnWith(value: any): void; /** * */ toHaveLastReturnedWith(value: any): void; lastReturnedWith(value: any): void; /** * */ toHaveNthReturnedWith(nthCall: number, value: any): void; nthReturnedWith(nthCall: number, value: any): void; /** * Use .toHaveBeenCalledWith to ensure that a mock function was called with * specific arguments. */ toHaveBeenCalledWith(...args: Array): void; toBeCalledWith(...args: Array): void; /** * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called * with specific arguments. */ toHaveBeenLastCalledWith(...args: Array): void; lastCalledWith(...args: Array): void; /** * Check that an object has a .length property and it is set to a certain * numeric value. */ toHaveLength(number: number): void; /** * */ toHaveProperty(propPath: string, value?: any): void; /** * Use .toMatch to check that a string matches a regular expression or string. */ toMatch(regexpOrString: RegExp | string): void; /** * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. */ toMatchObject(object: Object | Array): void; /** * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. */ toStrictEqual(value: any): void; /** * This ensures that an Object matches the most recent snapshot. */ toMatchSnapshot(propertyMatchers?: any, name?: string): void; /** * This ensures that an Object matches the most recent snapshot. */ toMatchSnapshot(name: string): void; toMatchInlineSnapshot(snapshot?: string): void; toMatchInlineSnapshot(propertyMatchers?: any, snapshot?: string): void; /** * Use .toThrow to test that a function throws when it is called. * If you want to test that a specific error gets thrown, you can provide an * argument to toThrow. The argument can be a string for the error message, * a class for the error, or a regex that should match the error. * * Alias: .toThrowError */ toThrow(message?: string | Error | Class | RegExp): void; toThrowError(message?: string | Error | Class | RegExp): void; /** * Use .toThrowErrorMatchingSnapshot to test that a function throws a error * matching the most recent snapshot when it is called. */ toThrowErrorMatchingSnapshot(): void; toThrowErrorMatchingInlineSnapshot(snapshot?: string): void; } type JestObjectType = { /** * Disables automatic mocking in the module loader. * * After this method is called, all `require()`s will return the real * versions of each module (rather than a mocked version). */ disableAutomock(): JestObjectType, /** * An un-hoisted version of disableAutomock */ autoMockOff(): JestObjectType, /** * Enables automatic mocking in the module loader. */ enableAutomock(): JestObjectType, /** * An un-hoisted version of enableAutomock */ autoMockOn(): JestObjectType, /** * Clears the mock.calls and mock.instances properties of all mocks. * Equivalent to calling .mockClear() on every mocked function. */ clearAllMocks(): JestObjectType, /** * Resets the state of all mocks. Equivalent to calling .mockReset() on every * mocked function. */ resetAllMocks(): JestObjectType, /** * Restores all mocks back to their original value. */ restoreAllMocks(): JestObjectType, /** * Removes any pending timers from the timer system. */ clearAllTimers(): void, /** * Returns the number of fake timers still left to run. */ getTimerCount(): number, /** * The same as `mock` but not moved to the top of the expectation by * babel-jest. */ doMock(moduleName: string, moduleFactory?: any): JestObjectType, /** * The same as `unmock` but not moved to the top of the expectation by * babel-jest. */ dontMock(moduleName: string): JestObjectType, /** * Returns a new, unused mock function. Optionally takes a mock * implementation. */ fn, TReturn>( implementation?: (...args: TArguments) => TReturn ): JestMockFn, /** * Determines if the given function is a mocked function. */ isMockFunction(fn: Function): boolean, /** * Given the name of a module, use the automatic mocking system to generate a * mocked version of the module for you. */ genMockFromModule(moduleName: string): any, /** * Mocks a module with an auto-mocked version when it is being required. * * The second argument can be used to specify an explicit module factory that * is being run instead of using Jest's automocking feature. * * The third argument can be used to create virtual mocks -- mocks of modules * that don't exist anywhere in the system. */ mock( moduleName: string, moduleFactory?: any, options?: Object ): JestObjectType, /** * Returns the actual module instead of a mock, bypassing all checks on * whether the module should receive a mock implementation or not. */ requireActual(moduleName: string): any, /** * Returns a mock module instead of the actual module, bypassing all checks * on whether the module should be required normally or not. */ requireMock(moduleName: string): any, /** * Resets the module registry - the cache of all required modules. This is * useful to isolate modules where local state might conflict between tests. */ resetModules(): JestObjectType, /** * Creates a sandbox registry for the modules that are loaded inside the * callback function. This is useful to isolate specific modules for every * test so that local module state doesn't conflict between tests. */ isolateModules(fn: () => void): JestObjectType, /** * Exhausts the micro-task queue (usually interfaced in node via * process.nextTick). */ runAllTicks(): void, /** * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), * setInterval(), and setImmediate()). */ runAllTimers(): void, /** * Exhausts all tasks queued by setImmediate(). */ runAllImmediates(): void, /** * Executes only the macro task queue (i.e. all tasks queued by setTimeout() * or setInterval() and setImmediate()). */ advanceTimersByTime(msToRun: number): void, /** * Executes only the macro task queue (i.e. all tasks queued by setTimeout() * or setInterval() and setImmediate()). * * Renamed to `advanceTimersByTime`. */ runTimersToTime(msToRun: number): void, /** * Executes only the macro-tasks that are currently pending (i.e., only the * tasks that have been queued by setTimeout() or setInterval() up to this * point) */ runOnlyPendingTimers(): void, /** * Explicitly supplies the mock object that the module system should return * for the specified module. Note: It is recommended to use jest.mock() * instead. */ setMock(moduleName: string, moduleExports: any): JestObjectType, /** * Indicates that the module system should never return a mocked version of * the specified module from require() (e.g. that it should always return the * real module). */ unmock(moduleName: string): JestObjectType, /** * Instructs Jest to use fake versions of the standard timer functions * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, * setImmediate and clearImmediate). */ useFakeTimers(): JestObjectType, /** * Instructs Jest to use the real versions of the standard timer functions. */ useRealTimers(): JestObjectType, /** * Creates a mock function similar to jest.fn but also tracks calls to * object[methodName]. */ spyOn( object: Object, methodName: string, accessType?: 'get' | 'set' ): JestMockFn, /** * Set the default timeout interval for tests and before/after hooks in milliseconds. * Note: The default timeout interval is 5 seconds if this method is not called. */ setTimeout(timeout: number): JestObjectType, }; type JestSpyType = { calls: JestCallsType, }; /** Runs this function after every test inside this context */ declare function afterEach( fn: (done: () => void) => ?Promise, timeout?: number ): void; /** Runs this function before every test inside this context */ declare function beforeEach( fn: (done: () => void) => ?Promise, timeout?: number ): void; /** Runs this function after all tests have finished inside this context */ declare function afterAll( fn: (done: () => void) => ?Promise, timeout?: number ): void; /** Runs this function before any tests have started inside this context */ declare function beforeAll( fn: (done: () => void) => ?Promise, timeout?: number ): void; /** A context for grouping tests together */ declare var describe: { /** * Creates a block that groups together several related tests in one "test suite" */ (name: JestTestName, fn: () => void): void, /** * Only run this describe block */ only(name: JestTestName, fn: () => void): void, /** * Skip running this describe block */ skip(name: JestTestName, fn: () => void): void, /** * each runs this test against array of argument arrays per each run * * @param {table} table of Test */ each( ...table: Array | mixed> | [Array, string] ): ( name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number ) => void, }; /** An individual test unit */ declare var it: { /** * An individual test unit * * @param {JestTestName} Name of Test * @param {Function} Test * @param {number} Timeout for the test, in milliseconds. */ ( name: JestTestName, fn?: (done: () => void) => ?Promise, timeout?: number ): void, /** * Only run this test * * @param {JestTestName} Name of Test * @param {Function} Test * @param {number} Timeout for the test, in milliseconds. */ only( name: JestTestName, fn?: (done: () => void) => ?Promise, timeout?: number ): { each( ...table: Array | mixed> | [Array, string] ): ( name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number ) => void, }, /** * Skip running this test * * @param {JestTestName} Name of Test * @param {Function} Test * @param {number} Timeout for the test, in milliseconds. */ skip( name: JestTestName, fn?: (done: () => void) => ?Promise, timeout?: number ): void, /** * Highlight planned tests in the summary output * * @param {String} Name of Test to do */ todo(name: string): void, /** * Run the test concurrently * * @param {JestTestName} Name of Test * @param {Function} Test * @param {number} Timeout for the test, in milliseconds. */ concurrent( name: JestTestName, fn?: (done: () => void) => ?Promise, timeout?: number ): void, /** * each runs this test against array of argument arrays per each run * * @param {table} table of Test */ each( ...table: Array | mixed> | [Array, string] ): ( name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number ) => void, }; declare function fit( name: JestTestName, fn: (done: () => void) => ?Promise, timeout?: number ): void; /** An individual test unit */ declare var test: typeof it; /** A disabled group of tests */ declare var xdescribe: typeof describe; /** A focused group of tests */ declare var fdescribe: typeof describe; /** A disabled individual test */ declare var xit: typeof it; /** A disabled individual test */ declare var xtest: typeof it; type JestPrettyFormatColors = { comment: { close: string, open: string }, content: { close: string, open: string }, prop: { close: string, open: string }, tag: { close: string, open: string }, value: { close: string, open: string }, }; type JestPrettyFormatIndent = (string) => string; // eslint-disable-next-line no-unused-vars type JestPrettyFormatRefs = Array; type JestPrettyFormatPrint = (any) => string; // eslint-disable-next-line no-unused-vars type JestPrettyFormatStringOrNull = string | null; type JestPrettyFormatOptions = {| callToJSON: boolean, edgeSpacing: string, escapeRegex: boolean, highlight: boolean, indent: number, maxDepth: number, min: boolean, // eslint-disable-next-line no-use-before-define plugins: JestPrettyFormatPlugins, printFunctionName: boolean, spacing: string, theme: {| comment: string, content: string, prop: string, tag: string, value: string, |}, |}; type JestPrettyFormatPlugin = { print: ( val: any, serialize: JestPrettyFormatPrint, indent: JestPrettyFormatIndent, opts: JestPrettyFormatOptions, colors: JestPrettyFormatColors ) => string, test: (any) => boolean, }; type JestPrettyFormatPlugins = Array; /** The expect function is used every time you want to test a value */ declare var expect: { /** The object that you want to make assertions against */ ( value: any ): JestExpectType & JestPromiseType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & JestStyledComponentsMatchersType & JestExtendedMatchersType, /** Add additional Jasmine matchers to Jest's roster */ extend(matchers: { [name: string]: JestMatcher }): void, /** Add a module that formats application-specific data structures. */ addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, assertions(expectedAssertions: number): void, hasAssertions(): void, any(value: mixed): JestAsymmetricEqualityType, anything(): any, arrayContaining(value: Array): Array, objectContaining(value: Object): Object, /** Matches any received string that contains the exact expected string. */ stringContaining(value: string): string, stringMatching(value: string | RegExp): string, not: { arrayContaining: (value: $ReadOnlyArray) => Array, objectContaining: (value: {}) => Object, stringContaining: (value: string) => string, stringMatching: (value: string | RegExp) => string, }, }; // TODO handle return type // http://jasmine.github.io/2.4/introduction.html#section-Spies declare function spyOn(value: mixed, method: string): Object; /** Holds all functions related to manipulating test runner */ declare var jest: JestObjectType; /** * The global Jasmine object, this is generally not exposed as the public API, * using features inside here could break in later versions of Jest. */ declare var jasmine: { DEFAULT_TIMEOUT_INTERVAL: number, any(value: mixed): JestAsymmetricEqualityType, anything(): any, arrayContaining(value: Array): Array, clock(): JestClockType, createSpy(name: string): JestSpyType, createSpyObj( baseName: string, methodNames: Array ): { [methodName: string]: JestSpyType }, objectContaining(value: Object): Object, stringMatching(value: string): string, }; ================================================ FILE: flow-typed/npm/ReactDOM_vx.x.x.js ================================================ // flow-typed signature: 3d9217fcc3103eff665390686051441d // flow-typed version: <>/ReactDOM_v17.0.0/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'ReactDOM' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'ReactDOMLegacy_DEPRECATED' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/ReactTestUtils_vx.x.x.js ================================================ // flow-typed signature: 957098ea98fd4cfa09140997329ef825 // flow-typed version: <>/ReactTestUtils_v17.0.0/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'ReactTestUtils' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'ReactTestUtils' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/d3-array_vx.x.x.js ================================================ // flow-typed signature: 07fe700325d6800ee40ab2fb92dbdb62 // flow-typed version: <>/d3-array_v2.7.1/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'd3-array' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'd3-array' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/d3-collection_vx.x.x.js ================================================ // flow-typed signature: 907d29f8d3e51e4214741d633c400932 // flow-typed version: <>/d3-collection_v1.0.7/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'd3-collection' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'd3-collection' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/d3-interpolate_vx.x.x.js ================================================ // flow-typed signature: 2135ae476a3ade7a473e844376538a26 // flow-typed version: <>/d3-interpolate_v2.0.1/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'd3-interpolate' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'd3-interpolate' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/d3-scale_vx.x.x.js ================================================ // flow-typed signature: c5e0917633fcc4b3962290b8ac57a263 // flow-typed version: <>/d3-scale_v3.2.2/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'd3-scale' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'd3-scale' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/d3-selection_vx.x.x.js ================================================ // flow-typed signature: 6b30cbe8947503cf5b22702a7931f20a // flow-typed version: <>/d3-selection_v2.0.0/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'd3-selection' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'd3-selection' { declare module.exports: any; } /** * We include stubs for each file inside this npm package in case you need to * require those files directly. Feel free to delete any files that aren't * needed. */ declare module 'd3-selection/dist/d3-selection' { declare module.exports: any; } declare module 'd3-selection/dist/d3-selection.min' { declare module.exports: any; } declare module 'd3-selection/src/array' { declare module.exports: any; } declare module 'd3-selection/src/constant' { declare module.exports: any; } declare module 'd3-selection/src/create' { declare module.exports: any; } declare module 'd3-selection/src/creator' { declare module.exports: any; } declare module 'd3-selection/src/identity' { declare module.exports: any; } declare module 'd3-selection/src' { declare module.exports: any; } declare module 'd3-selection/src/local' { declare module.exports: any; } declare module 'd3-selection/src/matcher' { declare module.exports: any; } declare module 'd3-selection/src/namespace' { declare module.exports: any; } declare module 'd3-selection/src/namespaces' { declare module.exports: any; } declare module 'd3-selection/src/pointer' { declare module.exports: any; } declare module 'd3-selection/src/pointers' { declare module.exports: any; } declare module 'd3-selection/src/select' { declare module.exports: any; } declare module 'd3-selection/src/selectAll' { declare module.exports: any; } declare module 'd3-selection/src/selection/append' { declare module.exports: any; } declare module 'd3-selection/src/selection/attr' { declare module.exports: any; } declare module 'd3-selection/src/selection/call' { declare module.exports: any; } declare module 'd3-selection/src/selection/classed' { declare module.exports: any; } declare module 'd3-selection/src/selection/clone' { declare module.exports: any; } declare module 'd3-selection/src/selection/data' { declare module.exports: any; } declare module 'd3-selection/src/selection/datum' { declare module.exports: any; } declare module 'd3-selection/src/selection/dispatch' { declare module.exports: any; } declare module 'd3-selection/src/selection/each' { declare module.exports: any; } declare module 'd3-selection/src/selection/empty' { declare module.exports: any; } declare module 'd3-selection/src/selection/enter' { declare module.exports: any; } declare module 'd3-selection/src/selection/exit' { declare module.exports: any; } declare module 'd3-selection/src/selection/filter' { declare module.exports: any; } declare module 'd3-selection/src/selection/html' { declare module.exports: any; } declare module 'd3-selection/src/selection' { declare module.exports: any; } declare module 'd3-selection/src/selection/insert' { declare module.exports: any; } declare module 'd3-selection/src/selection/iterator' { declare module.exports: any; } declare module 'd3-selection/src/selection/join' { declare module.exports: any; } declare module 'd3-selection/src/selection/lower' { declare module.exports: any; } declare module 'd3-selection/src/selection/merge' { declare module.exports: any; } declare module 'd3-selection/src/selection/node' { declare module.exports: any; } declare module 'd3-selection/src/selection/nodes' { declare module.exports: any; } declare module 'd3-selection/src/selection/on' { declare module.exports: any; } declare module 'd3-selection/src/selection/order' { declare module.exports: any; } declare module 'd3-selection/src/selection/property' { declare module.exports: any; } declare module 'd3-selection/src/selection/raise' { declare module.exports: any; } declare module 'd3-selection/src/selection/remove' { declare module.exports: any; } declare module 'd3-selection/src/selection/select' { declare module.exports: any; } declare module 'd3-selection/src/selection/selectAll' { declare module.exports: any; } declare module 'd3-selection/src/selection/selectChild' { declare module.exports: any; } declare module 'd3-selection/src/selection/selectChildren' { declare module.exports: any; } declare module 'd3-selection/src/selection/size' { declare module.exports: any; } declare module 'd3-selection/src/selection/sort' { declare module.exports: any; } declare module 'd3-selection/src/selection/sparse' { declare module.exports: any; } declare module 'd3-selection/src/selection/style' { declare module.exports: any; } declare module 'd3-selection/src/selection/text' { declare module.exports: any; } declare module 'd3-selection/src/selector' { declare module.exports: any; } declare module 'd3-selection/src/selectorAll' { declare module.exports: any; } declare module 'd3-selection/src/sourceEvent' { declare module.exports: any; } declare module 'd3-selection/src/window' { declare module.exports: any; } // Filename aliases declare module 'd3-selection/dist/d3-selection.js' { declare module.exports: $Exports<'d3-selection/dist/d3-selection'>; } declare module 'd3-selection/dist/d3-selection.min.js' { declare module.exports: $Exports<'d3-selection/dist/d3-selection.min'>; } declare module 'd3-selection/src/array.js' { declare module.exports: $Exports<'d3-selection/src/array'>; } declare module 'd3-selection/src/constant.js' { declare module.exports: $Exports<'d3-selection/src/constant'>; } declare module 'd3-selection/src/create.js' { declare module.exports: $Exports<'d3-selection/src/create'>; } declare module 'd3-selection/src/creator.js' { declare module.exports: $Exports<'d3-selection/src/creator'>; } declare module 'd3-selection/src/identity.js' { declare module.exports: $Exports<'d3-selection/src/identity'>; } declare module 'd3-selection/src/index' { declare module.exports: $Exports<'d3-selection/src'>; } declare module 'd3-selection/src/index.js' { declare module.exports: $Exports<'d3-selection/src'>; } declare module 'd3-selection/src/local.js' { declare module.exports: $Exports<'d3-selection/src/local'>; } declare module 'd3-selection/src/matcher.js' { declare module.exports: $Exports<'d3-selection/src/matcher'>; } declare module 'd3-selection/src/namespace.js' { declare module.exports: $Exports<'d3-selection/src/namespace'>; } declare module 'd3-selection/src/namespaces.js' { declare module.exports: $Exports<'d3-selection/src/namespaces'>; } declare module 'd3-selection/src/pointer.js' { declare module.exports: $Exports<'d3-selection/src/pointer'>; } declare module 'd3-selection/src/pointers.js' { declare module.exports: $Exports<'d3-selection/src/pointers'>; } declare module 'd3-selection/src/select.js' { declare module.exports: $Exports<'d3-selection/src/select'>; } declare module 'd3-selection/src/selectAll.js' { declare module.exports: $Exports<'d3-selection/src/selectAll'>; } declare module 'd3-selection/src/selection/append.js' { declare module.exports: $Exports<'d3-selection/src/selection/append'>; } declare module 'd3-selection/src/selection/attr.js' { declare module.exports: $Exports<'d3-selection/src/selection/attr'>; } declare module 'd3-selection/src/selection/call.js' { declare module.exports: $Exports<'d3-selection/src/selection/call'>; } declare module 'd3-selection/src/selection/classed.js' { declare module.exports: $Exports<'d3-selection/src/selection/classed'>; } declare module 'd3-selection/src/selection/clone.js' { declare module.exports: $Exports<'d3-selection/src/selection/clone'>; } declare module 'd3-selection/src/selection/data.js' { declare module.exports: $Exports<'d3-selection/src/selection/data'>; } declare module 'd3-selection/src/selection/datum.js' { declare module.exports: $Exports<'d3-selection/src/selection/datum'>; } declare module 'd3-selection/src/selection/dispatch.js' { declare module.exports: $Exports<'d3-selection/src/selection/dispatch'>; } declare module 'd3-selection/src/selection/each.js' { declare module.exports: $Exports<'d3-selection/src/selection/each'>; } declare module 'd3-selection/src/selection/empty.js' { declare module.exports: $Exports<'d3-selection/src/selection/empty'>; } declare module 'd3-selection/src/selection/enter.js' { declare module.exports: $Exports<'d3-selection/src/selection/enter'>; } declare module 'd3-selection/src/selection/exit.js' { declare module.exports: $Exports<'d3-selection/src/selection/exit'>; } declare module 'd3-selection/src/selection/filter.js' { declare module.exports: $Exports<'d3-selection/src/selection/filter'>; } declare module 'd3-selection/src/selection/html.js' { declare module.exports: $Exports<'d3-selection/src/selection/html'>; } declare module 'd3-selection/src/selection/index' { declare module.exports: $Exports<'d3-selection/src/selection'>; } declare module 'd3-selection/src/selection/index.js' { declare module.exports: $Exports<'d3-selection/src/selection'>; } declare module 'd3-selection/src/selection/insert.js' { declare module.exports: $Exports<'d3-selection/src/selection/insert'>; } declare module 'd3-selection/src/selection/iterator.js' { declare module.exports: $Exports<'d3-selection/src/selection/iterator'>; } declare module 'd3-selection/src/selection/join.js' { declare module.exports: $Exports<'d3-selection/src/selection/join'>; } declare module 'd3-selection/src/selection/lower.js' { declare module.exports: $Exports<'d3-selection/src/selection/lower'>; } declare module 'd3-selection/src/selection/merge.js' { declare module.exports: $Exports<'d3-selection/src/selection/merge'>; } declare module 'd3-selection/src/selection/node.js' { declare module.exports: $Exports<'d3-selection/src/selection/node'>; } declare module 'd3-selection/src/selection/nodes.js' { declare module.exports: $Exports<'d3-selection/src/selection/nodes'>; } declare module 'd3-selection/src/selection/on.js' { declare module.exports: $Exports<'d3-selection/src/selection/on'>; } declare module 'd3-selection/src/selection/order.js' { declare module.exports: $Exports<'d3-selection/src/selection/order'>; } declare module 'd3-selection/src/selection/property.js' { declare module.exports: $Exports<'d3-selection/src/selection/property'>; } declare module 'd3-selection/src/selection/raise.js' { declare module.exports: $Exports<'d3-selection/src/selection/raise'>; } declare module 'd3-selection/src/selection/remove.js' { declare module.exports: $Exports<'d3-selection/src/selection/remove'>; } declare module 'd3-selection/src/selection/select.js' { declare module.exports: $Exports<'d3-selection/src/selection/select'>; } declare module 'd3-selection/src/selection/selectAll.js' { declare module.exports: $Exports<'d3-selection/src/selection/selectAll'>; } declare module 'd3-selection/src/selection/selectChild.js' { declare module.exports: $Exports<'d3-selection/src/selection/selectChild'>; } declare module 'd3-selection/src/selection/selectChildren.js' { declare module.exports: $Exports<'d3-selection/src/selection/selectChildren'>; } declare module 'd3-selection/src/selection/size.js' { declare module.exports: $Exports<'d3-selection/src/selection/size'>; } declare module 'd3-selection/src/selection/sort.js' { declare module.exports: $Exports<'d3-selection/src/selection/sort'>; } declare module 'd3-selection/src/selection/sparse.js' { declare module.exports: $Exports<'d3-selection/src/selection/sparse'>; } declare module 'd3-selection/src/selection/style.js' { declare module.exports: $Exports<'d3-selection/src/selection/style'>; } declare module 'd3-selection/src/selection/text.js' { declare module.exports: $Exports<'d3-selection/src/selection/text'>; } declare module 'd3-selection/src/selector.js' { declare module.exports: $Exports<'d3-selection/src/selector'>; } declare module 'd3-selection/src/selectorAll.js' { declare module.exports: $Exports<'d3-selection/src/selectorAll'>; } declare module 'd3-selection/src/sourceEvent.js' { declare module.exports: $Exports<'d3-selection/src/sourceEvent'>; } declare module 'd3-selection/src/window.js' { declare module.exports: $Exports<'d3-selection/src/window'>; } ================================================ FILE: flow-typed/npm/d3-transition_vx.x.x.js ================================================ // flow-typed signature: c2d2412c06877d819d3cd4360b374aeb // flow-typed version: <>/d3-transition_v2.0.0/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'd3-transition' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'd3-transition' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/d3_vx.x.x.js ================================================ // flow-typed signature: 33ef3a2d3f6bf1c8a39549ccc666bc3d // flow-typed version: <>/d3_v5.16.0/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'd3' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'd3' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/hamt_plus_vx.x.x.js ================================================ // flow-typed signature: b8127b5d14b8b3f8e090da6b48fa7966 // flow-typed version: <>/hamt_plus_v1.0.2/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'hamt_plus' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'hamt_plus' { declare module.exports: any; } /** * We include stubs for each file inside this npm package in case you need to * require those files directly. Feel free to delete any files that aren't * needed. */ declare module 'hamt_plus/hamt' { declare module.exports: any; } // Filename aliases declare module 'hamt_plus/hamt.js' { declare module.exports: $Exports<'hamt_plus/hamt'>; } ================================================ FILE: flow-typed/npm/immutable_vx.x.x.js ================================================ // flow-typed signature: ab2e7a5a8e0e3e4c07cea3c0b0f8b4d6 // flow-typed version: <>/immutable_v4.0.0-rc.12/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'immutable' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'immutable' { declare module.exports: any; } /** * We include stubs for each file inside this npm package in case you need to * require those files directly. Feel free to delete any files that aren't * needed. */ declare module 'immutable/contrib/cursor' { declare module.exports: any; } declare module 'immutable/dist/immutable.es' { declare module.exports: any; } declare module 'immutable/dist/immutable' { declare module.exports: any; } declare module 'immutable/dist/immutable.min' { declare module.exports: any; } // Filename aliases declare module 'immutable/contrib/cursor/index' { declare module.exports: $Exports<'immutable/contrib/cursor'>; } declare module 'immutable/contrib/cursor/index.js' { declare module.exports: $Exports<'immutable/contrib/cursor'>; } declare module 'immutable/dist/immutable.es.js' { declare module.exports: $Exports<'immutable/dist/immutable.es'>; } declare module 'immutable/dist/immutable.js' { declare module.exports: $Exports<'immutable/dist/immutable'>; } declare module 'immutable/dist/immutable.min.js' { declare module.exports: $Exports<'immutable/dist/immutable.min'>; } ================================================ FILE: flow-typed/npm/jest_v26.x.x.js ================================================ // flow-typed signature: 681725d1525989df4ff4c352015f5c2b // flow-typed version: 9a968c602c/jest_v26.x.x/flow_>=v0.201.x type JestMockFn, TReturn> = { (...args: TArguments): TReturn, /** * An object for introspecting mock calls */ mock: { /** * An array that represents all calls that have been made into this mock * function. Each call is represented by an array of arguments that were * passed during the call. */ calls: Array, /** * An array that contains all the object instances that have been * instantiated from this mock function. */ instances: Array, /** * An array that contains all the object results that have been * returned by this mock function call */ results: Array<{ isThrow: boolean, value: TReturn, ... }>, ... }, /** * Resets all information stored in the mockFn.mock.calls and * mockFn.mock.instances arrays. Often this is useful when you want to clean * up a mock's usage data between two assertions. */ mockClear(): void, /** * Resets all information stored in the mock. This is useful when you want to * completely restore a mock back to its initial state. */ mockReset(): void, /** * Removes the mock and restores the initial implementation. This is useful * when you want to mock functions in certain test cases and restore the * original implementation in others. Beware that mockFn.mockRestore only * works when mock was created with jest.spyOn. Thus you have to take care of * restoration yourself when manually assigning jest.fn(). */ mockRestore(): void, /** * Accepts a function that should be used as the implementation of the mock. * The mock itself will still record all calls that go into and instances * that come from itself -- the only difference is that the implementation * will also be executed when the mock is called. */ mockImplementation( fn: (...args: TArguments) => TReturn, ): JestMockFn, /** * Accepts a function that will be used as an implementation of the mock for * one call to the mocked function. Can be chained so that multiple function * calls produce different results. */ mockImplementationOnce( fn: (...args: TArguments) => TReturn, ): JestMockFn, /** * Accepts a string to use in test result output in place of "jest.fn()" to * indicate which mock function is being referenced. */ mockName(name: string): JestMockFn, /** * Just a simple sugar function for returning `this` */ mockReturnThis(): void, /** * Accepts a value that will be returned whenever the mock function is called. */ mockReturnValue(value: TReturn): JestMockFn, /** * Sugar for only returning a value once inside your mock */ mockReturnValueOnce(value: TReturn): JestMockFn, /** * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) */ mockResolvedValue(value: TReturn): JestMockFn>, /** * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) */ mockResolvedValueOnce( value: TReturn, ): JestMockFn>, /** * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) */ mockRejectedValue(value: TReturn): JestMockFn>, /** * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) */ mockRejectedValueOnce(value: TReturn): JestMockFn>, ... }; type JestAsymmetricEqualityType = { /** * A custom Jasmine equality tester */ asymmetricMatch(value: mixed): boolean, ... }; type JestCallsType = { allArgs(): mixed, all(): mixed, any(): boolean, count(): number, first(): mixed, mostRecent(): mixed, reset(): void, ... }; type JestClockType = { install(): void, mockDate(date: Date): void, tick(milliseconds?: number): void, uninstall(): void, ... }; type JestMatcherResult = { message?: string | (() => string), pass: boolean, ... }; type JestMatcher = ( received: any, ...actual: Array ) => JestMatcherResult | Promise; type JestPromiseType = { /** * Use rejects to unwrap the reason of a rejected promise so any other * matcher can be chained. If the promise is fulfilled the assertion fails. */ rejects: JestExpectType, /** * Use resolves to unwrap the value of a fulfilled promise so any other * matcher can be chained. If the promise is rejected the assertion fails. */ resolves: JestExpectType, ... }; /** * Jest allows functions and classes to be used as test names in test() and * describe() */ type JestTestName = string | Function; /** * Plugin: jest-styled-components */ type JestStyledComponentsMatcherValue = | string | JestAsymmetricEqualityType | RegExp | typeof undefined; type JestStyledComponentsMatcherOptions = { media?: string, modifier?: string, supports?: string, ... }; type JestStyledComponentsMatchersType = { toHaveStyleRule( property: string, value: JestStyledComponentsMatcherValue, options?: JestStyledComponentsMatcherOptions, ): void, ... }; /** * Plugin: jest-enzyme */ type EnzymeMatchersType = { // 5.x toBeEmpty(): void, toBePresent(): void, // 6.x toBeChecked(): void, toBeDisabled(): void, toBeEmptyRender(): void, toContainMatchingElement(selector: string): void, toContainMatchingElements(n: number, selector: string): void, toContainExactlyOneMatchingElement(selector: string): void, toContainReact(element: React$Element): void, toExist(): void, toHaveClassName(className: string): void, toHaveHTML(html: string): void, toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: {...}) => void), toHaveRef(refName: string): void, toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: {...}) => void), toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: {...}) => void), toHaveTagName(tagName: string): void, toHaveText(text: string): void, toHaveValue(value: any): void, toIncludeText(text: string): void, toMatchElement( element: React$Element, options?: {|ignoreProps?: boolean, verbose?: boolean|}, ): void, toMatchSelector(selector: string): void, // 7.x toHaveDisplayName(name: string): void, ... }; // DOM testing library extensions (jest-dom) // https://github.com/testing-library/jest-dom type DomTestingLibraryType = { /** * @deprecated */ toBeInTheDOM(container?: HTMLElement): void, // 4.x toBeInTheDocument(): void, toBeVisible(): void, toBeEmpty(): void, toBeDisabled(): void, toBeEnabled(): void, toBeInvalid(): void, toBeRequired(): void, toBeValid(): void, toContainElement(element: HTMLElement | null): void, toContainHTML(htmlText: string): void, toHaveAttribute(attr: string, value?: any): void, toHaveClass(...classNames: string[]): void, toHaveFocus(): void, toHaveFormValues(expectedValues: {[name: string]: any, ...}): void, toHaveStyle(css: string | {[name: string]: any, ...}): void, toHaveTextContent( text: string | RegExp, options?: {|normalizeWhitespace: boolean|}, ): void, toHaveValue(value?: string | string[] | number): void, // 5.x toHaveDisplayValue(value: string | string[]): void, toBeChecked(): void, toBeEmptyDOMElement(): void, toBePartiallyChecked(): void, toHaveDescription(text: string | RegExp): void, ... }; // Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers type JestJQueryMatchersType = { toExist(): void, toHaveLength(len: number): void, toHaveId(id: string): void, toHaveClass(className: string): void, toHaveTag(tag: string): void, toHaveAttr(key: string, val?: any): void, toHaveProp(key: string, val?: any): void, toHaveText(text: string | RegExp): void, toHaveData(key: string, val?: any): void, toHaveValue(val: any): void, toHaveCss(css: {[key: string]: any, ...}): void, toBeChecked(): void, toBeDisabled(): void, toBeEmpty(): void, toBeHidden(): void, toBeSelected(): void, toBeVisible(): void, toBeFocused(): void, toBeInDom(): void, toBeMatchedBy(sel: string): void, toHaveDescendant(sel: string): void, toHaveDescendantWithText(sel: string, text: string | RegExp): void, ... }; // Jest Extended Matchers: https://github.com/jest-community/jest-extended type JestExtendedMatchersType = { /** * Note: Currently unimplemented * Passing assertion * * @param {String} message */ // pass(message: string): void; /** * Note: Currently unimplemented * Failing assertion * * @param {String} message */ // fail(message: string): void; /** * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. */ toBeEmpty(): void, /** * Use .toBeOneOf when checking if a value is a member of a given Array. * @param {Array.<*>} members */ toBeOneOf(members: any[]): void, /** * Use `.toBeNil` when checking a value is `null` or `undefined`. */ toBeNil(): void, /** * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. * @param {Function} predicate */ toSatisfy(predicate: (n: any) => boolean): void, /** * Use `.toBeArray` when checking if a value is an `Array`. */ toBeArray(): void, /** * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. * @param {Number} x */ toBeArrayOfSize(x: number): void, /** * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. * @param {Array.<*>} members */ toIncludeAllMembers(members: any[]): void, /** * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. * @param {Array.<*>} members */ toIncludeAnyMembers(members: any[]): void, /** * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. * @param {Function} predicate */ toSatisfyAll(predicate: (n: any) => boolean): void, /** * Use `.toBeBoolean` when checking if a value is a `Boolean`. */ toBeBoolean(): void, /** * Use `.toBeTrue` when checking a value is equal (===) to `true`. */ toBeTrue(): void, /** * Use `.toBeFalse` when checking a value is equal (===) to `false`. */ toBeFalse(): void, /** * Use .toBeDate when checking if a value is a Date. */ toBeDate(): void, /** * Use `.toBeFunction` when checking if a value is a `Function`. */ toBeFunction(): void, /** * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. * * Note: Required Jest version >22 * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same * * @param {Mock} mock */ toHaveBeenCalledBefore(mock: JestMockFn): void, /** * Use `.toBeNumber` when checking if a value is a `Number`. */ toBeNumber(): void, /** * Use `.toBeNaN` when checking a value is `NaN`. */ toBeNaN(): void, /** * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. */ toBeFinite(): void, /** * Use `.toBePositive` when checking if a value is a positive `Number`. */ toBePositive(): void, /** * Use `.toBeNegative` when checking if a value is a negative `Number`. */ toBeNegative(): void, /** * Use `.toBeEven` when checking if a value is an even `Number`. */ toBeEven(): void, /** * Use `.toBeOdd` when checking if a value is an odd `Number`. */ toBeOdd(): void, /** * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). * * @param {Number} start * @param {Number} end */ toBeWithin(start: number, end: number): void, /** * Use `.toBeObject` when checking if a value is an `Object`. */ toBeObject(): void, /** * Use `.toContainKey` when checking if an object contains the provided key. * * @param {String} key */ toContainKey(key: string): void, /** * Use `.toContainKeys` when checking if an object has all of the provided keys. * * @param {Array.} keys */ toContainKeys(keys: string[]): void, /** * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. * * @param {Array.} keys */ toContainAllKeys(keys: string[]): void, /** * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. * * @param {Array.} keys */ toContainAnyKeys(keys: string[]): void, /** * Use `.toContainValue` when checking if an object contains the provided value. * * @param {*} value */ toContainValue(value: any): void, /** * Use `.toContainValues` when checking if an object contains all of the provided values. * * @param {Array.<*>} values */ toContainValues(values: any[]): void, /** * Use `.toContainAllValues` when checking if an object only contains all of the provided values. * * @param {Array.<*>} values */ toContainAllValues(values: any[]): void, /** * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. * * @param {Array.<*>} values */ toContainAnyValues(values: any[]): void, /** * Use `.toContainEntry` when checking if an object contains the provided entry. * * @param {Array.} entry */ toContainEntry(entry: [string, string]): void, /** * Use `.toContainEntries` when checking if an object contains all of the provided entries. * * @param {Array.>} entries */ toContainEntries(entries: [string, string][]): void, /** * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. * * @param {Array.>} entries */ toContainAllEntries(entries: [string, string][]): void, /** * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. * * @param {Array.>} entries */ toContainAnyEntries(entries: [string, string][]): void, /** * Use `.toBeExtensible` when checking if an object is extensible. */ toBeExtensible(): void, /** * Use `.toBeFrozen` when checking if an object is frozen. */ toBeFrozen(): void, /** * Use `.toBeSealed` when checking if an object is sealed. */ toBeSealed(): void, /** * Use `.toBeString` when checking if a value is a `String`. */ toBeString(): void, /** * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. * * @param {String} string */ toEqualCaseInsensitive(string: string): void, /** * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. * * @param {String} prefix */ toStartWith(prefix: string): void, /** * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. * * @param {String} suffix */ toEndWith(suffix: string): void, /** * Use `.toInclude` when checking if a `String` includes the given `String` substring. * * @param {String} substring */ toInclude(substring: string): void, /** * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. * * @param {String} substring * @param {Number} times */ toIncludeRepeated(substring: string, times: number): void, /** * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. * * @param {Array.} substring */ toIncludeMultiple(substring: string[]): void, ... }; // Diffing snapshot utility for Jest (snapshot-diff) // https://github.com/jest-community/snapshot-diff type SnapshotDiffType = { /** * Compare the difference between the actual in the `expect()` * vs the object inside `valueB` with some extra options. */ toMatchDiffSnapshot( valueB: any, options?: {| expand?: boolean, colors?: boolean, contextLines?: number, stablePatchmarks?: boolean, aAnnotation?: string, bAnnotation?: string, |}, testName?: string, ): void, ... }; interface JestExpectType { not: JestExpectType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & JestStyledComponentsMatchersType & JestExtendedMatchersType & SnapshotDiffType; /** * If you have a mock function, you can use .lastCalledWith to test what * arguments it was last called with. */ lastCalledWith(...args: Array): void; /** * toBe just checks that a value is what you expect. It uses === to check * strict equality. */ toBe(value: any): void; /** * Use .toBeCalledWith to ensure that a mock function was called with * specific arguments. */ toBeCalledWith(...args: Array): void; /** * Using exact equality with floating point numbers is a bad idea. Rounding * means that intuitive things fail. */ toBeCloseTo(num: number, delta: any): void; /** * Use .toBeDefined to check that a variable is not undefined. */ toBeDefined(): void; /** * Use .toBeFalsy when you don't care what a value is, you just want to * ensure a value is false in a boolean context. */ toBeFalsy(): void; /** * To compare floating point numbers, you can use toBeGreaterThan. */ toBeGreaterThan(number: number): void; /** * To compare floating point numbers, you can use toBeGreaterThanOrEqual. */ toBeGreaterThanOrEqual(number: number): void; /** * To compare floating point numbers, you can use toBeLessThan. */ toBeLessThan(number: number): void; /** * To compare floating point numbers, you can use toBeLessThanOrEqual. */ toBeLessThanOrEqual(number: number): void; /** * Use .toBeInstanceOf(Class) to check that an object is an instance of a * class. */ toBeInstanceOf(cls: Class): void; /** * .toBeNull() is the same as .toBe(null) but the error messages are a bit * nicer. */ toBeNull(): void; /** * Use .toBeTruthy when you don't care what a value is, you just want to * ensure a value is true in a boolean context. */ toBeTruthy(): void; /** * Use .toBeUndefined to check that a variable is undefined. */ toBeUndefined(): void; /** * Use .toContain when you want to check that an item is in a list. For * testing the items in the list, this uses ===, a strict equality check. */ toContain(item: any): void; /** * Use .toContainEqual when you want to check that an item is in a list. For * testing the items in the list, this matcher recursively checks the * equality of all fields, rather than checking for object identity. */ toContainEqual(item: any): void; /** * Use .toEqual when you want to check that two objects have the same value. * This matcher recursively checks the equality of all fields, rather than * checking for object identity. */ toEqual(value: any): void; /** * Use .toHaveBeenCalled to ensure that a mock function got called. */ toHaveBeenCalled(): void; toBeCalled(): void; /** * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact * number of times. */ toHaveBeenCalledTimes(number: number): void; toBeCalledTimes(number: number): void; /** * */ toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void; nthCalledWith(nthCall: number, ...args: Array): void; /** * */ toHaveReturned(): void; toReturn(): void; /** * */ toHaveReturnedTimes(number: number): void; toReturnTimes(number: number): void; /** * */ toHaveReturnedWith(value: any): void; toReturnWith(value: any): void; /** * */ toHaveLastReturnedWith(value: any): void; lastReturnedWith(value: any): void; /** * */ toHaveNthReturnedWith(nthCall: number, value: any): void; nthReturnedWith(nthCall: number, value: any): void; /** * Use .toHaveBeenCalledWith to ensure that a mock function was called with * specific arguments. */ toHaveBeenCalledWith(...args: Array): void; toBeCalledWith(...args: Array): void; /** * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called * with specific arguments. */ toHaveBeenLastCalledWith(...args: Array): void; lastCalledWith(...args: Array): void; /** * Check that an object has a .length property and it is set to a certain * numeric value. */ toHaveLength(number: number): void; /** * */ toHaveProperty(propPath: string | $ReadOnlyArray, value?: any): void; /** * Use .toMatch to check that a string matches a regular expression or string. */ toMatch(regexpOrString: RegExp | string): void; /** * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. */ toMatchObject(object: Object | Array): void; /** * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. */ toStrictEqual(value: any): void; /** * This ensures that an Object matches the most recent snapshot. */ toMatchSnapshot(propertyMatchers?: any, name?: string): void; /** * This ensures that an Object matches the most recent snapshot. */ toMatchSnapshot(name: string): void; toMatchInlineSnapshot(snapshot?: string): void; toMatchInlineSnapshot(propertyMatchers?: any, snapshot?: string): void; /** * Use .toThrow to test that a function throws when it is called. * If you want to test that a specific error gets thrown, you can provide an * argument to toThrow. The argument can be a string for the error message, * a class for the error, or a regex that should match the error. * * Alias: .toThrowError */ toThrow(message?: string | Error | Class | RegExp): void; toThrowError(message?: string | Error | Class | RegExp): void; /** * Use .toThrowErrorMatchingSnapshot to test that a function throws a error * matching the most recent snapshot when it is called. */ toThrowErrorMatchingSnapshot(): void; toThrowErrorMatchingInlineSnapshot(snapshot?: string): void; } type JestObjectType = { /** * Disables automatic mocking in the module loader. * * After this method is called, all `require()`s will return the real * versions of each module (rather than a mocked version). */ disableAutomock(): JestObjectType, /** * An un-hoisted version of disableAutomock */ autoMockOff(): JestObjectType, /** * Enables automatic mocking in the module loader. */ enableAutomock(): JestObjectType, /** * An un-hoisted version of enableAutomock */ autoMockOn(): JestObjectType, /** * Clears the mock.calls and mock.instances properties of all mocks. * Equivalent to calling .mockClear() on every mocked function. */ clearAllMocks(): JestObjectType, /** * Resets the state of all mocks. Equivalent to calling .mockReset() on every * mocked function. */ resetAllMocks(): JestObjectType, /** * Restores all mocks back to their original value. */ restoreAllMocks(): JestObjectType, /** * Removes any pending timers from the timer system. */ clearAllTimers(): void, /** * Returns the number of fake timers still left to run. */ getTimerCount(): number, /** * Set the current system time used by fake timers. * Simulates a user changing the system clock while your program is running. * It affects the current time but it does not in itself cause * e.g. timers to fire; they will fire exactly as they would have done * without the call to jest.setSystemTime(). */ setSystemTime(now?: number | Date): void, /** * The same as `mock` but not moved to the top of the expectation by * babel-jest. */ doMock(moduleName: string, moduleFactory?: any): JestObjectType, /** * The same as `unmock` but not moved to the top of the expectation by * babel-jest. */ dontMock(moduleName: string): JestObjectType, /** * Returns a new, unused mock function. Optionally takes a mock * implementation. */ fn, TReturn>( implementation?: (...args: TArguments) => TReturn, ): JestMockFn, /** * Determines if the given function is a mocked function. */ isMockFunction(fn: Function): boolean, /** * Alias of `createMockFromModule`. */ genMockFromModule(moduleName: string): any, /** * Given the name of a module, use the automatic mocking system to generate a * mocked version of the module for you. */ createMockFromModule(moduleName: string): any, /** * Mocks a module with an auto-mocked version when it is being required. * * The second argument can be used to specify an explicit module factory that * is being run instead of using Jest's automocking feature. * * The third argument can be used to create virtual mocks -- mocks of modules * that don't exist anywhere in the system. */ mock( moduleName: string, moduleFactory?: any, options?: Object, ): JestObjectType, /** * Returns the actual module instead of a mock, bypassing all checks on * whether the module should receive a mock implementation or not. */ requireActual(m: $Flow$ModuleRef | string): T, /** * Returns a mock module instead of the actual module, bypassing all checks * on whether the module should be required normally or not. */ requireMock(moduleName: string): any, /** * Resets the module registry - the cache of all required modules. This is * useful to isolate modules where local state might conflict between tests. */ resetModules(): JestObjectType, /** * Creates a sandbox registry for the modules that are loaded inside the * callback function. This is useful to isolate specific modules for every * test so that local module state doesn't conflict between tests. */ isolateModules(fn: () => void): JestObjectType, /** * Exhausts the micro-task queue (usually interfaced in node via * process.nextTick). */ runAllTicks(): void, /** * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), * setInterval(), and setImmediate()). */ runAllTimers(): void, /** * Exhausts all tasks queued by setImmediate(). */ runAllImmediates(): void, /** * Executes only the macro task queue (i.e. all tasks queued by setTimeout() * or setInterval() and setImmediate()). */ advanceTimersByTime(msToRun: number): void, /** * Executes only the macro task queue (i.e. all tasks queued by setTimeout() * or setInterval() and setImmediate()). * * Renamed to `advanceTimersByTime`. */ runTimersToTime(msToRun: number): void, /** * Executes only the macro-tasks that are currently pending (i.e., only the * tasks that have been queued by setTimeout() or setInterval() up to this * point) */ runOnlyPendingTimers(): void, /** * Explicitly supplies the mock object that the module system should return * for the specified module. Note: It is recommended to use jest.mock() * instead. */ setMock(moduleName: string, moduleExports: any): JestObjectType, /** * Indicates that the module system should never return a mocked version of * the specified module from require() (e.g. that it should always return the * real module). */ unmock(moduleName: string): JestObjectType, /** * Instructs Jest to use fake versions of the standard timer functions * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, * setImmediate and clearImmediate). */ useFakeTimers(mode?: 'modern' | 'legacy'): JestObjectType, /** * Instructs Jest to use the real versions of the standard timer functions. */ useRealTimers(): JestObjectType, /** * Creates a mock function similar to jest.fn but also tracks calls to * object[methodName]. */ spyOn( object: Object, methodName: string, accessType?: 'get' | 'set', ): JestMockFn, /** * Set the default timeout interval for tests and before/after hooks in milliseconds. * Note: The default timeout interval is 5 seconds if this method is not called. */ setTimeout(timeout: number): JestObjectType, ... }; type JestSpyType = {calls: JestCallsType, ...}; type JestDoneFn = {| (error?: Error): void, fail: (error: Error) => void, |}; /** Runs this function after every test inside this context */ declare function afterEach( fn: (done: JestDoneFn) => ?Promise, timeout?: number, ): void; /** Runs this function before every test inside this context */ declare function beforeEach( fn: (done: JestDoneFn) => ?Promise, timeout?: number, ): void; /** Runs this function after all tests have finished inside this context */ declare function afterAll( fn: (done: JestDoneFn) => ?Promise, timeout?: number, ): void; /** Runs this function before any tests have started inside this context */ declare function beforeAll( fn: (done: JestDoneFn) => ?Promise, timeout?: number, ): void; /** A context for grouping tests together */ declare var describe: { /** * Creates a block that groups together several related tests in one "test suite" */ (name: JestTestName, fn: () => void): void, /** * Only run this describe block */ only(name: JestTestName, fn: () => void): void, /** * Skip running this describe block */ skip(name: JestTestName, fn: () => void): void, /** * each runs this test against array of argument arrays per each run * * @param {table} table of Test */ each( ...table: Array | mixed> | [Array, string] ): ( name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number, ) => void, ... }; /** An individual test unit */ declare var it: { /** * An individual test unit * * @param {JestTestName} Name of Test * @param {Function} Test * @param {number} Timeout for the test, in milliseconds. */ ( name: JestTestName, fn?: (done: JestDoneFn) => ?Promise, timeout?: number, ): void, /** * Only run this test * * @param {JestTestName} Name of Test * @param {Function} Test * @param {number} Timeout for the test, in milliseconds. */ only: {| ( name: JestTestName, fn?: (done: JestDoneFn) => ?Promise, timeout?: number, ): void, each( ...table: Array | mixed> | [Array, string] ): ( name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number, ) => void, |}, /** * Skip running this test * * @param {JestTestName} Name of Test * @param {Function} Test * @param {number} Timeout for the test, in milliseconds. */ skip: {| ( name: JestTestName, fn?: (done: JestDoneFn) => ?Promise, timeout?: number, ): void, each( ...table: Array | mixed> | [Array, string] ): ( name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number, ) => void, |}, /** * Highlight planned tests in the summary output * * @param {String} Name of Test to do */ todo(name: string): void, /** * Run the test concurrently * * @param {JestTestName} Name of Test * @param {Function} Test * @param {number} Timeout for the test, in milliseconds. */ concurrent( name: JestTestName, fn?: (done: JestDoneFn) => ?Promise, timeout?: number, ): void, /** * each runs this test against array of argument arrays per each run * * @param {table} table of Test */ each( ...table: Array | mixed> | [Array, string] ): ( name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number, ) => void, ... }; declare function fit( name: JestTestName, fn: (done: JestDoneFn) => ?Promise, timeout?: number, ): void; /** An individual test unit */ declare var test: typeof it; /** A disabled group of tests */ declare var xdescribe: typeof describe; /** A focused group of tests */ declare var fdescribe: typeof describe; /** A disabled individual test */ declare var xit: typeof it; /** A disabled individual test */ declare var xtest: typeof it; type JestPrettyFormatColors = { comment: { close: string, open: string, ... }, content: { close: string, open: string, ... }, prop: { close: string, open: string, ... }, tag: { close: string, open: string, ... }, value: { close: string, open: string, ... }, ... }; type JestPrettyFormatIndent = string => string; type JestPrettyFormatRefs = Array; type JestPrettyFormatPrint = any => string; type JestPrettyFormatStringOrNull = string | null; type JestPrettyFormatOptions = {| callToJSON: boolean, edgeSpacing: string, escapeRegex: boolean, highlight: boolean, indent: number, maxDepth: number, min: boolean, plugins: JestPrettyFormatPlugins, printFunctionName: boolean, spacing: string, theme: {| comment: string, content: string, prop: string, tag: string, value: string, |}, |}; type JestPrettyFormatPlugin = { print: ( val: any, serialize: JestPrettyFormatPrint, indent: JestPrettyFormatIndent, opts: JestPrettyFormatOptions, colors: JestPrettyFormatColors, ) => string, test: any => boolean, ... }; type JestPrettyFormatPlugins = Array; /** The expect function is used every time you want to test a value */ declare var expect: { /** The object that you want to make assertions against */ ( value: any, ): JestExpectType & JestPromiseType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & JestStyledComponentsMatchersType & JestExtendedMatchersType & SnapshotDiffType, /** Add additional Jasmine matchers to Jest's roster */ extend(matchers: {[name: string]: JestMatcher, ...}): void, /** Add a module that formats application-specific data structures. */ addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, assertions(expectedAssertions: number): void, hasAssertions(): void, any(value: mixed): JestAsymmetricEqualityType, anything(): any, arrayContaining(value: Array): Array, objectContaining(value: Object): Object, /** Matches any received string that contains the exact expected string. */ stringContaining(value: string): string, stringMatching(value: string | RegExp): string, not: { arrayContaining: (value: $ReadOnlyArray) => Array, objectContaining: (value: {...}) => Object, stringContaining: (value: string) => string, stringMatching: (value: string | RegExp) => string, ... }, ... }; // TODO handle return type // http://jasmine.github.io/2.4/introduction.html#section-Spies declare function spyOn(value: mixed, method: string): Object; /** Holds all functions related to manipulating test runner */ declare var jest: JestObjectType; /** * The global Jasmine object, this is generally not exposed as the public API, * using features inside here could break in later versions of Jest. */ declare var jasmine: { DEFAULT_TIMEOUT_INTERVAL: number, any(value: mixed): JestAsymmetricEqualityType, anything(): any, arrayContaining(value: Array): Array, clock(): JestClockType, createSpy(name: string): JestSpyType, createSpyObj( baseName: string, methodNames: Array, ): {[methodName: string]: JestSpyType, ...}, objectContaining(value: Object): Object, stringMatching(value: string): string, ... }; ================================================ FILE: flow-typed/npm/jsondiffpatch-for-react_vx.x.x.js ================================================ // flow-typed signature: 01f138bab8c1a273bb901b529eed460c // flow-typed version: <>/jsondiffpatch-for-react_v1.0.4/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'jsondiffpatch-for-react' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'jsondiffpatch-for-react' { declare module.exports: any; } ================================================ FILE: flow-typed/npm/nullthrows_vx.x.x.js ================================================ // flow-typed signature: 0b4008173515b0c71528fd8763afbff9 // flow-typed version: <>/nullthrows_v1.1.1/flow_v0.129.0 /** * This is an autogenerated libdef stub for: * * 'nullthrows' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'nullthrows' { declare module.exports: any; } /** * We include stubs for each file inside this npm package in case you need to * require those files directly. Feel free to delete any files that aren't * needed. */ declare module 'nullthrows/nullthrows' { declare module.exports: any; } // Filename aliases declare module 'nullthrows/nullthrows.js' { declare module.exports: $Exports<'nullthrows/nullthrows'>; } ================================================ FILE: flow-typed/npm/react-dom_v18.x.x.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @oncall recoil */ // flow-typed signature: de0a47185086152df6ab4a598943384d // flow-typed version: cf9120ecbb/react-dom_v18.x.x/flow_>=v0.127.x declare module 'react-dom_shared-types' { /** * Copied from react-reconciler * https://github.com/facebook/react/blob/168da8d55782f3b34e2a6aa0c4dd0587696afdbd/packages/react-reconciler/src/ReactInternalTypes.js#L271 */ declare type TransitionTracingCallbacks = {| onTransitionStart?: (transitionName: string, startTime: number) => void, onTransitionProgress?: ( transitionName: string, startTime: number, currentTime: number, pending: Array<{| name: null | string, |}>, ) => void, onTransitionIncomplete?: ( transitionName: string, startTime: number, deletions: Array<{| type: string, name?: string, newName?: string, endTime: number, |}>, ) => void, onTransitionComplete?: ( transitionName: string, startTime: number, endTime: number, ) => void, onMarkerProgress?: ( transitionName: string, marker: string, startTime: number, currentTime: number, pending: Array<{| name: null | string |}>, ) => void, onMarkerIncomplete?: ( transitionName: string, marker: string, startTime: number, deletions: Array<{| type: string, name?: string, newName?: string, endTime: number, |}>, ) => void, onMarkerComplete?: ( transitionName: string, marker: string, startTime: number, endTime: number, ) => void, |}; declare type ReactEmpty = null | void | boolean; declare type ReactNodeList = ReactEmpty | React$Node; // Mutable source version can be anything (e.g. number, string, immutable data structure) // so long as it changes every time any part of the source changes. declare type MutableSourceVersion = $NonMaybeType; declare type MutableSourceGetVersionFn = ( source: $NonMaybeType, ) => MutableSourceVersion; declare type MutableSource> = {| _source: Source, _getVersion: MutableSourceGetVersionFn, // Tracks the version of this source at the time it was most recently read. // Used to determine if a source is safe to read from before it has been subscribed to. // Version number is only used during mount, // since the mechanism for determining safety after subscription is expiration time. // // As a workaround to support multiple concurrent renderers, // we categorize some renderers as primary and others as secondary. // We only expect there to be two concurrent renderers at most: // React Native (primary) and Fabric (secondary); // React DOM (primary) and React ART (secondary). // Secondary renderers store their context values on separate fields. // We use the same approach for Context. _workInProgressVersionPrimary: null | MutableSourceVersion, _workInProgressVersionSecondary: null | MutableSourceVersion, // DEV only // Used to detect multiple renderers using the same mutable source. _currentPrimaryRenderer?: any, _currentSecondaryRenderer?: any, // DEV only // Used to detect side effects that update a mutable source during render. // See https://github.com/facebook/react/issues/19948 _currentlyRenderingFiber?: any, _initialVersionAsOfFirstRender?: MutableSourceVersion | null, |}; } declare module 'react-dom' { declare var version: string; declare function findDOMNode( componentOrElement: Element | ?React$Component ): null | Element | Text; declare function render( element: React$Element, container: Element, callback?: () => void ): React$ElementRef; declare function hydrate( element: React$Element, container: Element, callback?: () => void ): React$ElementRef; declare function createPortal( node: React$Node, container: Element ): React$Portal; declare function unmountComponentAtNode(container: any): boolean; declare function unstable_batchedUpdates( callback: (a: A, b: B, c: C, d: D, e: E) => mixed, a: A, b: B, c: C, d: D, e: E ): void; declare function unstable_renderSubtreeIntoContainer< ElementType: React$ElementType >( parentComponent: React$Component, nextElement: React$Element, container: any, callback?: () => void ): React$ElementRef; } declare module 'react-dom/client' { import type { TransitionTracingCallbacks, ReactNodeList, MutableSource, } from 'react-dom_shared-types'; declare opaque type FiberRoot; declare type RootType = { render(children: ReactNodeList): void, unmount(): void, _internalRoot: FiberRoot | null, ... }; declare type CreateRootOptions = { unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, identifierPrefix?: string, onRecoverableError?: (error: mixed) => void, transitionCallbacks?: TransitionTracingCallbacks, ... }; declare export function createRoot( container: Element | DocumentFragment, options?: CreateRootOptions, ): RootType; declare type HydrateRootOptions = { // Hydration options hydratedSources?: Array>, onHydrated?: (suspenseNode: Comment) => void, onDeleted?: (suspenseNode: Comment) => void, // Options for all roots unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, identifierPrefix?: string, onRecoverableError?: (error: mixed) => void, ... }; declare export function hydrateRoot( container: Document | Element, initialChildren: ReactNodeList, options?: HydrateRootOptions, ): RootType; } declare module 'react-dom/server' { declare var version: string; declare function renderToString(element: React$Node): string; declare function renderToStaticMarkup(element: React$Node): string; declare function renderToNodeStream(element: React$Node): stream$Readable; declare function renderToStaticNodeStream( element: React$Node ): stream$Readable; } declare module 'react-dom/test-utils' { declare interface Thenable { then(resolve: () => mixed, reject?: () => mixed): mixed, } declare var Simulate: { [eventName: string]: ( element: Element, eventData?: { [key: string]: mixed, ... } ) => void, ... }; declare function renderIntoDocument( instance: React$Element ): React$Component; declare function mockComponent( componentClass: React$ElementType, mockTagName?: string ): { [key: string]: mixed, ... }; declare function isElement(element: React$Element): boolean; declare function isElementOfType( element: React$Element, componentClass: React$ElementType ): boolean; declare function isDOMComponent(instance: any): boolean; declare function isCompositeComponent( instance: React$Component ): boolean; declare function isCompositeComponentWithType( instance: React$Component, componentClass: React$ElementType ): boolean; declare function findAllInRenderedTree( tree: React$Component, test: (child: React$Component) => boolean ): Array>; declare function scryRenderedDOMComponentsWithClass( tree: React$Component, className: string ): Array; declare function findRenderedDOMComponentWithClass( tree: React$Component, className: string ): ?Element; declare function scryRenderedDOMComponentsWithTag( tree: React$Component, tagName: string ): Array; declare function findRenderedDOMComponentWithTag( tree: React$Component, tagName: string ): ?Element; declare function scryRenderedComponentsWithType( tree: React$Component, componentClass: React$ElementType ): Array>; declare function findRenderedComponentWithType( tree: React$Component, componentClass: React$ElementType ): ?React$Component; declare function act(callback: () => void | Thenable): Thenable; } ================================================ FILE: flow-typed/package.json.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @oncall recoil */ declare module '../package.json' { declare var repository: string } ================================================ FILE: flow-typed/public-stubs.js ================================================ declare var ScopeRules: Object; declare var ReactElement: any; declare var __DEV__: boolean; ================================================ FILE: jest.config.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ module.exports = { timers: 'fake', globals: { __DEV__: true, }, moduleNameMapper: { '^recoil-shared(.*)$': '/packages/shared$1', '^Recoil$': '/packages/recoil', '^recoil-sync$': '/packages/recoil-sync', '^refine$': '/packages/refine', }, testPathIgnorePatterns: [ '/node_modules/', '/packages-ext/', '/__generated__/', '/mock-graphql/', ], setupFiles: ['./setupJestMock.js'], }; ================================================ FILE: package.json ================================================ { "name": "recoil", "private": true, "description": "Recoil - A state management library for React", "main": "cjs/index.js", "module": "es/index.js", "react-native": "native/index.js", "unpkg": "umd/index.js", "files": [ "umd", "es", "cjs", "native", "index.d.ts" ], "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT", "scripts": { "prepare": "install-peers", "build": "node scripts/build.mjs", "pack": "node scripts/pack.mjs", "test": "yarn relay && jest packages/*", "format": "prettier --write \"./**/*.{js,md,json}\"", "flow": "flow --show-all-errors", "flow:restart": "flow stop && npm run flow", "test:typescript": "dtslint typescript", "lint": "eslint .", "relay": "relay-compiler", "deploy-nightly": "yarn build && node scripts/deploy_nightly_build.js" }, "dependencies": { "hamt_plus": "1.0.2", "transit-js": "^0.8.874" }, "peerDependencies": { "react": ">=16.13.1" }, "peerDependenciesMeta": { "react-dom": { "optional": true }, "react-native": { "optional": true } }, "devDependencies": { "@babel/core": "^7.16.0", "@babel/plugin-proposal-class-properties": "^7.16.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", "@babel/plugin-proposal-optional-chaining": "^7.16.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-flow-strip-types": "^7.16.0", "@babel/preset-flow": "^7.16.0", "@babel/preset-react": "^7.16.0", "@rollup/plugin-alias": "^3.1.5", "@rollup/plugin-babel": "^5.0.0", "@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-replace": "^2.3.2", "@types/react": ">=18.0.9", "@types/react-relay": ">=13.0.2", "@types/relay-runtime": ">=13.0.3", "babel-jest": "^26.0.1", "babel-plugin-module-resolver": "^4.0.0", "babel-plugin-relay": "^13.2.0", "babel-preset-fbjs": "^3.3.0", "dtslint": "^4.2.0", "eslint": "^8.2.0", "eslint-plugin-fb-www": "^1.11.0", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-jest": "^23.13.2", "eslint-plugin-react": "^7.20.0", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-relay": "^1.8.3", "eslint-plugin-rulesdir": "^0.2.0", "eslint-plugin-unused-imports": "^2.0.0", "flow-bin": "0.207.0", "flow-copy-source": "^2.0.9", "flow-interfaces-chrome": "^0.6.0", "flow-typed": "^3.7.0", "hermes-eslint": "^0.4.8", "husky": ">=4", "immutable": "^4.0.0-rc.12", "install-peers-cli": "^2.2.0", "jest-cli": "^26.0.1", "lint-staged": ">=10", "prettier": "^2.4.1", "promise-polyfill": "^8.1.3", "react": ">=16.13.1", "react-dom": ">=16.13.1", "react-relay": "^13.2.0", "relay-compiler": "^13.2.0", "relay-runtime": "^13.2.0", "relay-test-utils": "^13.2.0", "rollup": "^2.10.0", "rollup-plugin-includepaths": "^0.2.3", "rollup-plugin-terser": "^5.3.0", "typescript": "^3.9.5" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,mjs,md,json}": "prettier --write", "*.{js,mjs}": "eslint --fix" } } ================================================ FILE: packages/recoil/Recoil_index.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; export type {StoreID} from './core/Recoil_Keys'; export type {PersistenceType} from './core/Recoil_Node'; export type { RecoilValue, RecoilState, RecoilValueReadOnly, } from './core/Recoil_RecoilValue'; export type { MutableSnapshot, Snapshot, SnapshotID, } from './core/Recoil_Snapshot'; export type {SetterOrUpdater} from './hooks/Recoil_Hooks'; export type {RecoilCallbackInterface} from './hooks/Recoil_useRecoilCallback'; export type {RecoilBridge} from './hooks/Recoil_useRecoilBridgeAcrossReactRoots'; export type {Loadable} from './adt/Recoil_Loadable'; export type { AtomEffect, PersistenceSettings, } from './recoil_values/Recoil_atom'; export type {TransactionInterface} from './core/Recoil_AtomicUpdates'; export type { GetRecoilValue, SetRecoilState, ResetRecoilState, } from './recoil_values/Recoil_callbackTypes'; export type { Parameter, SelectorFamilyOptions, } from './recoil_values/Recoil_selectorFamily'; const {RecoilLoadable} = require('./adt/Recoil_Loadable'); const {DefaultValue} = require('./core/Recoil_Node'); const {RecoilRoot, useRecoilStoreID} = require('./core/Recoil_RecoilRoot'); const {isRecoilValue} = require('./core/Recoil_RecoilValue'); const {retentionZone} = require('./core/Recoil_RetentionZone'); const {freshSnapshot} = require('./core/Recoil_Snapshot'); const { useRecoilState, useRecoilState_TRANSITION_SUPPORT_UNSTABLE, useRecoilStateLoadable, useRecoilValue, useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, useRecoilValueLoadable, useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE, useResetRecoilState, useSetRecoilState, } = require('./hooks/Recoil_Hooks'); const { useGotoRecoilSnapshot, useRecoilSnapshot, useRecoilTransactionObserver, } = require('./hooks/Recoil_SnapshotHooks'); const useGetRecoilValueInfo = require('./hooks/Recoil_useGetRecoilValueInfo'); const useRecoilBridgeAcrossReactRoots = require('./hooks/Recoil_useRecoilBridgeAcrossReactRoots'); const {useRecoilCallback} = require('./hooks/Recoil_useRecoilCallback'); const useRecoilRefresher = require('./hooks/Recoil_useRecoilRefresher'); const useRecoilTransaction = require('./hooks/Recoil_useRecoilTransaction'); const useRetain = require('./hooks/Recoil_useRetain'); const atom = require('./recoil_values/Recoil_atom'); const atomFamily = require('./recoil_values/Recoil_atomFamily'); const constSelector = require('./recoil_values/Recoil_constSelector'); const errorSelector = require('./recoil_values/Recoil_errorSelector'); const readOnlySelector = require('./recoil_values/Recoil_readOnlySelector'); const selector = require('./recoil_values/Recoil_selector'); const selectorFamily = require('./recoil_values/Recoil_selectorFamily'); const { noWait, waitForAll, waitForAllSettled, waitForAny, waitForNone, } = require('./recoil_values/Recoil_WaitFor'); const RecoilEnv = require('recoil-shared/util/Recoil_RecoilEnv'); module.exports = { // Types DefaultValue, isRecoilValue, RecoilLoadable, // Global Recoil environment settings RecoilEnv, // Recoil Root RecoilRoot, useRecoilStoreID, useRecoilBridgeAcrossReactRoots_UNSTABLE: useRecoilBridgeAcrossReactRoots, // Atoms/Selectors atom, selector, // Convenience Atoms/Selectors atomFamily, selectorFamily, constSelector, errorSelector, readOnlySelector, // Concurrency Helpers for Atoms/Selectors noWait, waitForNone, waitForAny, waitForAll, waitForAllSettled, // Hooks for Atoms/Selectors useRecoilValue, useRecoilValueLoadable, useRecoilState, useRecoilStateLoadable, useSetRecoilState, useResetRecoilState, useGetRecoilValueInfo_UNSTABLE: useGetRecoilValueInfo, useRecoilRefresher_UNSTABLE: useRecoilRefresher, useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE, useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, useRecoilState_TRANSITION_SUPPORT_UNSTABLE, // Hooks for complex operations useRecoilCallback, useRecoilTransaction_UNSTABLE: useRecoilTransaction, // Snapshots useGotoRecoilSnapshot, useRecoilSnapshot, useRecoilTransactionObserver_UNSTABLE: useRecoilTransactionObserver, snapshot_UNSTABLE: freshSnapshot, // Memory Management useRetain, retentionZone, }; ================================================ FILE: packages/recoil/adt/Recoil_ArrayKeyedMap.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Implements (a subset of) the interface of built-in Map but supports arrays as * keys. Two keys are equal if corresponding elements are equal according to the * equality semantics of built-in Map. Operations are at worst O(n*b) where n is * the array length and b is the complexity of the built-in operation. * * @flow * @format * @oncall recoil */ 'use strict'; const LEAF = {}; const emptyMap = new Map(); class ArrayKeyedMap { _base: Map = new Map(); constructor( existing?: ArrayKeyedMap | Iterable<[mixed, V]>, // $FlowFixMe[incompatible-return] ): ArrayKeyedMap { if (existing instanceof ArrayKeyedMap) { for (const [k, v] of existing.entries()) { this.set(k, v); } } else if (existing) { for (const [k, v] of existing) { this.set(k, v); } } return this; } get(key: mixed): V | void { const ks = Array.isArray(key) ? key : [key]; let map = this._base; ks.forEach(k => { map = map.get(k) ?? emptyMap; }); return map === undefined ? undefined : map.get(LEAF); } set(key: mixed, value: V): any { const ks = Array.isArray(key) ? key : [key]; let map: ?(any | Map | Map) = this._base; let next: ?(any | Map | Map) = map; ks.forEach(k => { // $FlowFixMe[incompatible-use] next = map.get(k); if (!next) { next = new Map(); // $FlowFixMe[incompatible-use] map.set(k, next); } map = next; }); // $FlowFixMe[incompatible-use] next.set(LEAF, value); return this; } delete(key: mixed): any { const ks = Array.isArray(key) ? key : [key]; let map: ?(any | Map | Map) = this._base; let next: ?(any | Map | Map) = map; ks.forEach(k => { // $FlowFixMe[incompatible-use] next = map.get(k); if (!next) { next = new Map(); // $FlowFixMe[incompatible-use] map.set(k, next); } map = next; }); // $FlowFixMe[incompatible-use] next.delete(LEAF); // TODO We could cleanup empty maps return this; } entries(): Iterator<[$ReadOnlyArray, V]> { const answer = []; function recurse(level: any | Map, prefix: Array) { level.forEach((v, k) => { if (k === LEAF) { answer.push([prefix, v]); } else { recurse(v, prefix.concat(k)); } }); } recurse(this._base, []); return answer.values(); } toBuiltInMap(): Map<$ReadOnlyArray, V> { return new Map(this.entries()); } } module.exports = {ArrayKeyedMap}; ================================================ FILE: packages/recoil/adt/Recoil_Loadable.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * A type that represents a value that may or may not be loaded. It differs from * LoadObject in that the `loading` state has a Promise that is meant to resolve * when the value is available (but as with LoadObject, an individual Loadable * is a value type and is not mutated when the status of a request changes). * * @flow strict * @format * @oncall recoil */ 'use strict'; const err = require('recoil-shared/util/Recoil_err'); const isPromise = require('recoil-shared/util/Recoil_isPromise'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); class BaseLoadable { getValue(): T { throw err('BaseLoadable'); } toPromise(): Promise { throw err('BaseLoadable'); } valueMaybe(): T | void { throw err('BaseLoadable'); } valueOrThrow(): T { // $FlowFixMe[prop-missing] throw err(`Loadable expected value, but in "${this.state}" state`); } promiseMaybe(): Promise | void { throw err('BaseLoadable'); } promiseOrThrow(): Promise { // $FlowFixMe[prop-missing] throw err(`Loadable expected promise, but in "${this.state}" state`); } errorMaybe(): mixed | void { throw err('BaseLoadable'); } errorOrThrow(): mixed { // $FlowFixMe[prop-missing] throw err(`Loadable expected error, but in "${this.state}" state`); } is(other: Loadable): boolean { // $FlowFixMe[prop-missing] return other.state === this.state && other.contents === this.contents; } map(_map: T => Promise | Loadable | S): Loadable { throw err('BaseLoadable'); } } class ValueLoadable extends BaseLoadable { state: 'hasValue' = 'hasValue'; contents: T; constructor(value: T) { super(); this.contents = value; } getValue(): T { return this.contents; } toPromise(): Promise { return Promise.resolve(this.contents); } valueMaybe(): T { return this.contents; } valueOrThrow(): T { return this.contents; } promiseMaybe(): void { return undefined; } errorMaybe(): void { return undefined; } map(map: T => Promise | Loadable | S): Loadable { try { const next = map(this.contents); return isPromise(next) ? loadableWithPromise(next) : isLoadable(next) ? next : loadableWithValue(next); } catch (e) { return isPromise(e) ? // If we "suspended", then try again. // errors and subsequent retries will be handled in 'loading' case // $FlowFixMe[prop-missing] loadableWithPromise(e.next(() => this.map(map))) : loadableWithError(e); } } } class ErrorLoadable extends BaseLoadable { state: 'hasError' = 'hasError'; contents: mixed; constructor(error: mixed) { super(); this.contents = error; } getValue(): T { throw this.contents; } toPromise(): Promise { return Promise.reject(this.contents); } valueMaybe(): void { return undefined; } promiseMaybe(): void { return undefined; } errorMaybe(): mixed { return this.contents; } errorOrThrow(): mixed { return this.contents; } map(_map: T => Promise | Loadable | S): $ReadOnly> { // $FlowIssue[incompatible-return] return this; } } class LoadingLoadable extends BaseLoadable { state: 'loading' = 'loading'; contents: Promise; constructor(promise: Promise) { super(); this.contents = promise; } getValue(): T { throw this.contents; } toPromise(): Promise { return this.contents; } valueMaybe(): void { return undefined; } promiseMaybe(): Promise { return this.contents; } promiseOrThrow(): Promise { return this.contents; } errorMaybe(): void { return undefined; } map( map: T => Promise | Loadable | S, ): $ReadOnly> { return loadableWithPromise( this.contents .then(value => { const next = map(value); if (isLoadable(next)) { const nextLoadable: Loadable = next; switch (nextLoadable.state) { case 'hasValue': return nextLoadable.contents; case 'hasError': throw nextLoadable.contents; case 'loading': return nextLoadable.contents; } } return next; }) // $FlowFixMe[incompatible-call] .catch(e => { if (isPromise(e)) { // we were "suspended," try again return e.then(() => this.map(map).contents); } throw e; }), ); } } export type Loadable<+T> = | $ReadOnly> | $ReadOnly> | $ReadOnly>; export type ValueLoadableType<+T> = $ReadOnly>; export type ErrorLoadableType<+T> = $ReadOnly>; export type LoadingLoadableType<+T> = $ReadOnly>; function loadableWithValue<+T>(value: T): $ReadOnly> { return Object.freeze(new ValueLoadable(value)); } function loadableWithError<+T>(error: mixed): $ReadOnly> { return Object.freeze(new ErrorLoadable(error)); } function loadableWithPromise<+T>( promise: Promise, ): $ReadOnly> { return Object.freeze(new LoadingLoadable(promise)); } function loadableLoading<+T>(): $ReadOnly> { return Object.freeze(new LoadingLoadable(new Promise(() => {}))); } type UnwrapLoadables = $TupleMap(Loadable) => T>; type LoadableAllOfTuple = < Tuple: $ReadOnlyArray | Promise | mixed>, >( tuple: Tuple, ) => Loadable<$TupleMap(Loadable | Promise | V) => V>>; type LoadableAllOfObj = < Obj: $ReadOnly<{[string]: Loadable | Promise | mixed, ...}>, >( obj: Obj, ) => Loadable<$ObjMap(Loadable | Promise | V) => V>>; type LoadableAll = LoadableAllOfTuple & LoadableAllOfObj; function loadableAllArray>>( inputs: Inputs, ): Loadable> { return inputs.every(i => i.state === 'hasValue') ? // $FlowFixMe[incompatible-return] loadableWithValue(inputs.map(i => i.contents)) : inputs.some(i => i.state === 'hasError') ? loadableWithError( nullthrows( inputs.find(i => i.state === 'hasError'), 'Invalid loadable passed to loadableAll', ).contents, ) : loadableWithPromise(Promise.all(inputs.map(i => i.contents))); } function loadableAll< Inputs: | $ReadOnlyArray | Promise | mixed> | $ReadOnly<{[string]: Loadable | Promise | mixed, ...}>, >( inputs: Inputs, ): Loadable<$ReadOnlyArray | $ReadOnly<{[string]: mixed, ...}>> { const unwrapedInputs = Array.isArray(inputs) ? inputs : Object.getOwnPropertyNames(inputs).map(key => inputs[key]); const normalizedInputs = unwrapedInputs.map(x => isLoadable(x) ? x : isPromise(x) ? loadableWithPromise(x) : loadableWithValue(x), ); const output = loadableAllArray(normalizedInputs); return Array.isArray(inputs) ? // $FlowIssue[incompatible-return] output : // Object.getOwnPropertyNames() has consistent key ordering with ES6 // $FlowIssue[incompatible-call] // $FlowFixMe[incompatible-return] Pre-supress errors for Flow 0.207.0 output.map(outputs => Object.getOwnPropertyNames(inputs).reduce( (out, key, idx) => ({...out, [key]: outputs[idx]}), {}, ), ); } function isLoadable(x: mixed): boolean %checks { return x instanceof BaseLoadable; } const LoadableStaticInterface = { of: (value: Promise | Loadable | T): Loadable => isPromise(value) ? loadableWithPromise(value) : isLoadable(value) ? value : loadableWithValue(value), error: (error: mixed): $ReadOnly> => loadableWithError(error), // $FlowIssue[incompatible-return] loading: (): LoadingLoadable => loadableLoading(), // $FlowIssue[unclear-type] all: ((loadableAll: any): LoadableAll), isLoadable, }; module.exports = { loadableWithValue, loadableWithError, loadableWithPromise, loadableLoading, loadableAll, isLoadable, RecoilLoadable: LoadableStaticInterface, }; ================================================ FILE: packages/recoil/adt/Recoil_PersistentMap.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; import type {HAMTPlusMap} from 'hamt_plus'; const hamt = require('hamt_plus'); const gkx = require('recoil-shared/util/Recoil_gkx'); export interface PersistentMap { keys(): Iterable; entries(): Iterable<[K, V]>; get(key: K): V | void; has(key: K): boolean; set(key: K, value: V): PersistentMap; delete(key: K): PersistentMap; clone(): PersistentMap; toMap(): Map; } class BuiltInMap implements PersistentMap { _map: Map; constructor(existing?: PersistentMap) { this._map = new Map(existing?.entries()); } keys(): Iterable { return this._map.keys(); } entries(): Iterable<[K, V]> { return this._map.entries(); } get(k: K): V | void { return this._map.get(k); } has(k: K): boolean { return this._map.has(k); } set(k: K, v: V): PersistentMap { this._map.set(k, v); return this; } delete(k: K): PersistentMap { this._map.delete(k); return this; } clone(): PersistentMap { return persistentMap(this); } toMap(): Map { return new Map(this._map); } } class HashArrayMappedTrieMap implements PersistentMap { // Because hamt.empty is not a function there is no way to introduce type // parameters on it, so empty is typed as HAMTPlusMap. // $FlowIssue _hamt: HAMTPlusMap = ((hamt.empty: any).beginMutation(): HAMTPlusMap< K, V, >); constructor(existing?: PersistentMap) { if (existing instanceof HashArrayMappedTrieMap) { const h = existing._hamt.endMutation(); existing._hamt = h.beginMutation(); this._hamt = h.beginMutation(); } else if (existing) { for (const [k, v] of existing.entries()) { this._hamt.set(k, v); } } } keys(): Iterable { return this._hamt.keys(); } entries(): Iterable<[K, V]> { return this._hamt.entries(); } get(k: K): V | void { return this._hamt.get(k); } has(k: K): boolean { return this._hamt.has(k); } set(k: K, v: V): PersistentMap { this._hamt.set(k, v); return this; } delete(k: K): PersistentMap { this._hamt.delete(k); return this; } clone(): PersistentMap { return persistentMap(this); } toMap(): Map { return new Map(this._hamt); } } function persistentMap( existing?: PersistentMap, ): PersistentMap { if (gkx('recoil_hamt_2020')) { return new HashArrayMappedTrieMap(existing); } else { return new BuiltInMap(existing); } } module.exports = { persistentMap, }; ================================================ FILE: packages/recoil/adt/Recoil_Queue.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; function enqueueExecution(s: string, f: () => mixed) { f(); } module.exports = { enqueueExecution, }; ================================================ FILE: packages/recoil/adt/Recoil_Wrapper.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; class WrappedValue { value: T; constructor(value: T) { this.value = value; } } module.exports = { WrappedValue, }; ================================================ FILE: packages/recoil/adt/__tests__/Recoil_ArrayKeyedMap-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {ArrayKeyedMap} = require('../Recoil_ArrayKeyedMap'); test('basic operation', () => { const m = new ArrayKeyedMap(); m.set([], 0); m.set(['a'], 1); m.set(['a', 'b'], 2); expect(m.get([])).toBe(0); expect(m.get(['a'])).toBe(1); expect(m.get(['a', 'b'])).toBe(2); }); test('enumeration of properties', () => { const m = new ArrayKeyedMap(); m.set([], 0); m.set(['a'], 1); m.set(['a', 'b'], 2); const entries = Array.from(m.entries()); expect(entries[0][0]).toEqual([]); expect(entries[0][1]).toBe(0); expect(entries[1][0]).toEqual(['a']); expect(entries[1][1]).toBe(1); expect(entries[2][0]).toEqual(['a', 'b']); expect(entries[2][1]).toBe(2); }); test('copying', () => { const m = new ArrayKeyedMap(); m.set([], 0); m.set(['a'], 1); m.set(['a', 'b'], 2); const mm = new ArrayKeyedMap(m); expect(mm.get([])).toBe(0); expect(mm.get(['a'])).toBe(1); expect(mm.get(['a', 'b'])).toBe(2); expect(Array.from(m.entries())).toEqual(Array.from(mm.entries())); }); ================================================ FILE: packages/recoil/adt/__tests__/Recoil_Loadable-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { RecoilLoadable, loadableWithError, loadableWithPromise, loadableWithValue, } = require('../Recoil_Loadable'); const ERROR = new Error('ERROR'); test('Value Loadable', async () => { const loadable = loadableWithValue('VALUE'); expect(loadable.state).toBe('hasValue'); expect(loadable.contents).toBe('VALUE'); expect(loadable.getValue()).toBe('VALUE'); await expect(loadable.toPromise()).resolves.toBe('VALUE'); expect(loadable.valueMaybe()).toBe('VALUE'); expect(loadable.valueOrThrow()).toBe('VALUE'); expect(loadable.errorMaybe()).toBe(undefined); expect(() => loadable.errorOrThrow()).toThrow(); expect(loadable.promiseMaybe()).toBe(undefined); expect(() => loadable.promiseOrThrow()).toThrow(); }); test('Error Loadable', async () => { const loadable = loadableWithError<$FlowFixMe>(ERROR); expect(loadable.state).toBe('hasError'); expect(loadable.contents).toBe(ERROR); expect(() => loadable.getValue()).toThrow(ERROR); await expect(loadable.toPromise()).rejects.toBe(ERROR); expect(loadable.valueMaybe()).toBe(undefined); expect(() => loadable.valueOrThrow()).toThrow(); expect(loadable.errorMaybe()).toBe(ERROR); expect(loadable.errorOrThrow()).toBe(ERROR); expect(loadable.promiseMaybe()).toBe(undefined); expect(() => loadable.promiseOrThrow()).toThrow(); }); test('Pending Value Loadable', async () => { const promise = Promise.resolve('VALUE'); const loadable = loadableWithPromise(promise); expect(loadable.state).toBe('loading'); expect(loadable.contents).toBe(promise); expect(() => loadable.getValue()).toThrow(); await expect(loadable.toPromise()).resolves.toBe('VALUE'); expect(loadable.valueMaybe()).toBe(undefined); expect(() => loadable.valueOrThrow()).toThrow(); expect(loadable.errorMaybe()).toBe(undefined); expect(() => loadable.errorOrThrow()).toThrow(); await expect(loadable.promiseMaybe()).resolves.toBe('VALUE'); await expect(loadable.promiseOrThrow()).resolves.toBe('VALUE'); }); describe('Loadable mapping', () => { test('Loadable mapping value', () => { const loadable = loadableWithValue('VALUE').map(x => 'MAPPED ' + x); expect(loadable.state).toBe('hasValue'); expect(loadable.contents).toBe('MAPPED VALUE'); }); test('Loadable mapping value to error', () => { const loadable = loadableWithValue('VALUE').map<$FlowFixMe>(() => { throw ERROR; }); expect(loadable.state).toBe('hasError'); expect(loadable.contents).toBe(ERROR); }); test('Loadable mapping value to Promise', async () => { const loadable = loadableWithValue('VALUE').map(value => Promise.resolve('MAPPED ' + value), ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).resolves.toBe('MAPPED VALUE'); }); test('Loadable mapping value to reject', async () => { const loadable = loadableWithValue('VALUE').map(() => Promise.reject(ERROR), ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).rejects.toBe(ERROR); }); test('Loadable mapping error', () => { const loadable = loadableWithError(ERROR).map(() => 'NOT_USED'); expect(loadable.state).toBe('hasError'); expect(loadable.contents).toBe(ERROR); }); test('Loadable mapping promise value', async () => { const loadable = loadableWithPromise(Promise.resolve('VALUE')).map( x => 'MAPPED ' + x, ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).resolves.toBe('MAPPED VALUE'); }); test('Loadable mapping promise value to reject', async () => { const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(() => Promise.reject(ERROR), ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).rejects.toBe(ERROR); }); test('Loadable mapping promise value to error', async () => { const loadable = loadableWithPromise(Promise.resolve('VALUE')).map( () => { throw ERROR; }, ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).rejects.toBe(ERROR); }); test('Loadable mapping promise error', async () => { const loadable = loadableWithPromise(Promise.reject(ERROR)).map( () => 'NOT_USED', ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).rejects.toBe(ERROR); }); test('Loadable mapping to loadable', () => { const loadable = loadableWithValue('VALUE').map(value => loadableWithValue(value), ); expect(loadable.state).toBe('hasValue'); expect(loadable.contents).toBe('VALUE'); }); test('Loadable mapping promise to loadable value', async () => { const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(value => loadableWithValue('MAPPED ' + value), ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).resolves.toBe('MAPPED VALUE'); }); test('Loadable mapping promise to loadable error', async () => { const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(() => // $FlowFixMe[underconstrained-implicit-instantiation] loadableWithError(ERROR), ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).rejects.toBe(ERROR); }); test('Loadable mapping promise to loadable promise', async () => { const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(value => loadableWithPromise(Promise.resolve('MAPPED ' + value)), ); expect(loadable.state).toBe('loading'); await expect(loadable.toPromise()).resolves.toBe('MAPPED VALUE'); }); }); test('Loadable Factory Interface', async () => { const valueLoadable = RecoilLoadable.of('VALUE'); expect(valueLoadable.state).toBe('hasValue'); expect(valueLoadable.contents).toBe('VALUE'); const valueLoadable2 = RecoilLoadable.of(RecoilLoadable.of('VALUE')); expect(valueLoadable2.state).toBe('hasValue'); expect(valueLoadable2.contents).toBe('VALUE'); const promiseLoadable = RecoilLoadable.of(Promise.resolve('ASYNC')); expect(promiseLoadable.state).toBe('loading'); await expect(promiseLoadable.contents).resolves.toBe('ASYNC'); const promiseLoadable2 = RecoilLoadable.of( RecoilLoadable.of(Promise.resolve('ASYNC')), ); expect(promiseLoadable2.state).toBe('loading'); await expect(promiseLoadable2.contents).resolves.toBe('ASYNC'); const errorLoadable = RecoilLoadable.error('ERROR'); expect(errorLoadable.state).toBe('hasError'); expect(errorLoadable.contents).toBe('ERROR'); // $FlowFixMe[underconstrained-implicit-instantiation] const errorLoadable2 = RecoilLoadable.of(RecoilLoadable.error('ERROR')); expect(errorLoadable2.state).toBe('hasError'); expect(errorLoadable2.contents).toBe('ERROR'); const loadingLoadable = RecoilLoadable.loading(); expect(loadingLoadable.state).toBe('loading'); }); describe('Loadable All', () => { test('Array', async () => { expect( RecoilLoadable.all([RecoilLoadable.of('x'), RecoilLoadable.of(123)]) .contents, ).toEqual(['x', 123]); await expect( RecoilLoadable.all([ RecoilLoadable.of(Promise.resolve('x')), RecoilLoadable.of(123), ]).contents, ).resolves.toEqual(['x', 123]); expect( RecoilLoadable.all([ RecoilLoadable.of('x'), RecoilLoadable.of(123), // $FlowFixMe[underconstrained-implicit-instantiation] RecoilLoadable.error('ERROR'), ]).contents, ).toEqual('ERROR'); expect( RecoilLoadable.all([ RecoilLoadable.of('x'), RecoilLoadable.all([RecoilLoadable.of(1), RecoilLoadable.of(2)]), ]).contents, ).toEqual(['x', [1, 2]]); }); test('Object', async () => { expect( RecoilLoadable.all({ str: RecoilLoadable.of('x'), num: RecoilLoadable.of(123), }).contents, ).toEqual({ str: 'x', num: 123, }); await expect( RecoilLoadable.all({ str: RecoilLoadable.of(Promise.resolve('x')), num: RecoilLoadable.of(123), }).contents, ).resolves.toEqual({ str: 'x', num: 123, }); expect( RecoilLoadable.all({ str: RecoilLoadable.of('x'), num: RecoilLoadable.of(123), // $FlowFixMe[underconstrained-implicit-instantiation] err: RecoilLoadable.error('ERROR'), }).contents, ).toEqual('ERROR'); }); test('mixed values', async () => { expect(RecoilLoadable.all([RecoilLoadable.of('A'), 'B']).contents).toEqual([ 'A', 'B', ]); await expect( RecoilLoadable.all([RecoilLoadable.of('A'), Promise.resolve('B')]) .contents, ).resolves.toEqual(['A', 'B']); await expect( RecoilLoadable.all([RecoilLoadable.of('A'), Promise.reject('B')]) .contents, ).rejects.toEqual('B'); await expect( RecoilLoadable.all({ a: 'A', b: RecoilLoadable.of('B'), c: Promise.resolve('C'), }).contents, ).resolves.toEqual({a: 'A', b: 'B', c: 'C'}); }); }); ================================================ FILE: packages/recoil/caches/Recoil_CacheImplementationType.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; export interface CacheImplementation { get(K): ?V; set(K, V): void; delete(K): void; clear(): void; size(): number; } ================================================ FILE: packages/recoil/caches/Recoil_CachePolicy.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; export type EqualityPolicy = 'reference' | 'value'; export type EvictionPolicy = 'lru' | 'keep-all' | 'most-recent'; export type CachePolicy = | {eviction: 'lru', maxSize: number, equality?: EqualityPolicy} | {eviction: 'keep-all', equality?: EqualityPolicy} | {eviction: 'most-recent', equality?: EqualityPolicy} | {equality: EqualityPolicy}; export type CachePolicyWithoutEviction = {equality: EqualityPolicy}; ================================================ FILE: packages/recoil/caches/Recoil_LRUCache.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); type CacheNode = { key: K, value: V, left: ?CacheNode, right: ?CacheNode, }; type Options = { maxSize: number, mapKey?: K => mixed, }; class LRUCache { _maxSize: number; _size: number; _head: ?CacheNode; _tail: ?CacheNode; _map: Map>; _keyMapper: K => mixed; constructor(options: Options) { this._maxSize = options.maxSize; this._size = 0; this._head = null; this._tail = null; this._map = new Map>(); this._keyMapper = options.mapKey ?? (v => v); } head(): ?CacheNode { return this._head; } tail(): ?CacheNode { return this._tail; } size(): number { return this._size; } maxSize(): number { return this._maxSize; } has(key: K): boolean { return this._map.has(this._keyMapper(key)); } get(key: K): ?V { const mappedKey = this._keyMapper(key); const node = this._map.get(mappedKey); if (!node) { return undefined; } this.set(key, node.value); return node.value; } set(key: K, val: V): void { const mappedKey = this._keyMapper(key); const existingNode = this._map.get(mappedKey); if (existingNode) { this.delete(key); } const head = this.head(); const node = { key, right: head, left: null, value: val, }; if (head) { head.left = node; } else { this._tail = node; } this._map.set(mappedKey, node); this._head = node; this._size++; this._maybeDeleteLRU(); } _maybeDeleteLRU() { if (this.size() > this.maxSize()) { this.deleteLru(); } } deleteLru(): void { const tail = this.tail(); if (tail) { this.delete(tail.key); } } delete(key: K): void { const mappedKey = this._keyMapper(key); if (!this._size || !this._map.has(mappedKey)) { return; } const node = nullthrows(this._map.get(mappedKey)); const right = node.right; const left = node.left; if (right) { right.left = node.left; } if (left) { left.right = node.right; } if (node === this.head()) { this._head = right; } if (node === this.tail()) { this._tail = left; } this._map.delete(mappedKey); this._size--; } clear(): void { this._size = 0; this._head = null; this._tail = null; this._map = new Map>(); } } module.exports = {LRUCache}; ================================================ FILE: packages/recoil/caches/Recoil_MapCache.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; type Options = { mapKey: K => mixed, }; class MapCache { _map: Map; _keyMapper: K => mixed; constructor(options?: Options) { this._map = new Map(); this._keyMapper = options?.mapKey ?? (v => v); } size(): number { return this._map.size; } has(key: K): boolean { return this._map.has(this._keyMapper(key)); } get(key: K): ?V { return this._map.get(this._keyMapper(key)); } set(key: K, val: V): void { this._map.set(this._keyMapper(key), val); } delete(key: K): void { this._map.delete(this._keyMapper(key)); } clear(): void { this._map.clear(); } } module.exports = {MapCache}; ================================================ FILE: packages/recoil/caches/Recoil_TreeCache.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { GetHandlers, NodeCacheRoute, NodeValueGet, SetHandlers, TreeCacheBranch, TreeCacheLeaf, TreeCacheNode, } from './Recoil_TreeCacheImplementationType'; const {isFastRefreshEnabled} = require('../core/Recoil_ReactMode'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); export type Options = { name?: string, mapNodeValue?: (value: mixed) => mixed, onHit?: (node: TreeCacheLeaf) => void, onSet?: (node: TreeCacheLeaf) => void, }; class ChangedPathError extends Error {} class TreeCache { _name: ?string; _numLeafs: number; // $FlowIssue[unclear-type] _root: TreeCacheNode | null; _onHit: $NonMaybeType['onHit']>; _onSet: $NonMaybeType['onSet']>; _mapNodeValue: $NonMaybeType['mapNodeValue']>; constructor(options?: Options) { this._name = options?.name; this._numLeafs = 0; this._root = null; this._onHit = options?.onHit ?? (() => {}); this._onSet = options?.onSet ?? (() => {}); this._mapNodeValue = options?.mapNodeValue ?? (val => val); } size(): number { return this._numLeafs; } // $FlowIssue[unclear-type] root(): TreeCacheNode | null { return this._root; } get(getNodeValue: NodeValueGet, handlers?: GetHandlers): ?T { return this.getLeafNode(getNodeValue, handlers)?.value; } getLeafNode( getNodeValue: NodeValueGet, handlers?: GetHandlers, ): ?TreeCacheLeaf { if (this._root == null) { return undefined; } // Iterate down the tree based on the current node values until we hit a leaf // $FlowIssue[unclear-type] let node: ?TreeCacheNode = this._root; while (node) { handlers?.onNodeVisit(node); if (node.type === 'leaf') { this._onHit(node); return node; } const nodeValue = this._mapNodeValue(getNodeValue(node.nodeKey)); node = node.branches.get(nodeValue); } return undefined; } set(route: NodeCacheRoute, value: T, handlers?: SetHandlers): void { const addLeaf = () => { // First, setup the branch nodes for the route: // Iterate down the tree to find or add branch nodes following the route let node: ?TreeCacheBranch; let branchKey; for (const [nodeKey, nodeValue] of route) { // If the previous root was a leaf, while we not have a get(), it means // the selector has inconsistent values or implementation changed. const root = this._root; if (root?.type === 'leaf') { throw this.invalidCacheError(); } // node now refers to the next node down in the tree const parent = node; // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-type] node = parent ? parent.branches.get(branchKey) : root; // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-type] node = node ?? { type: 'branch', nodeKey, parent, branches: new Map(), branchKey, }; // If we found an existing node, confirm it has a consistent value if (node.type !== 'branch' || node.nodeKey !== nodeKey) { throw this.invalidCacheError(); } // Add the branch node to the tree parent?.branches.set(branchKey, node); handlers?.onNodeVisit?.(node); // Prepare for next iteration and install root if it is new. branchKey = this._mapNodeValue(nodeValue); this._root = this._root ?? node; } // Second, setup the leaf node: // If there is an existing leaf for this route confirm it is consistent const oldLeaf: ?TreeCacheNode = node ? node?.branches.get(branchKey) : this._root; if ( oldLeaf != null && (oldLeaf.type !== 'leaf' || oldLeaf.branchKey !== branchKey) ) { throw this.invalidCacheError(); } // Create a new or replacement leaf. const leafNode = { type: 'leaf', value, parent: node, branchKey, }; // Install the leaf and call handlers node?.branches.set(branchKey, leafNode); this._root = this._root ?? leafNode; this._numLeafs++; this._onSet(leafNode); handlers?.onNodeVisit?.(leafNode); }; try { addLeaf(); } catch (error) { // If the cache was stale or observed inconsistent values, such as with // Fast Refresh, then clear it and rebuild with the new values. if (error instanceof ChangedPathError) { this.clear(); addLeaf(); } else { throw error; } } } // Returns true if leaf was actually deleted from the tree delete(leaf: TreeCacheLeaf): boolean { const root = this.root(); if (!root) { return false; } if (leaf === root) { this._root = null; this._numLeafs = 0; return true; } // Iterate up from the leaf deleteing it from it's parent's branches. let node = leaf.parent; let branchKey = leaf.branchKey; while (node) { node.branches.delete(branchKey); // Stop iterating if we hit the root. if (node === root) { if (node.branches.size === 0) { this._root = null; this._numLeafs = 0; } else { this._numLeafs--; } return true; } // Stop iterating if there are other branches since we don't need to // remove any more nodes. if (node.branches.size > 0) { break; } // Iterate up to our parent branchKey = node?.branchKey; node = node.parent; } // Confirm that the leaf we are deleting is actually attached to our tree for (; node !== root; node = node.parent) { if (node == null) { return false; } } this._numLeafs--; return true; } clear(): void { this._numLeafs = 0; this._root = null; } invalidCacheError(): ChangedPathError { const CHANGED_PATH_ERROR_MESSAGE = isFastRefreshEnabled() ? 'Possible Fast Refresh module reload detected. ' + 'This may also be caused by an selector returning inconsistent values. ' + 'Resetting cache.' : 'Invalid cache values. This happens when selectors do not return ' + 'consistent values for the same input dependency values. That may also ' + 'be caused when using Fast Refresh to change a selector implementation. ' + 'Resetting cache.'; recoverableViolation( CHANGED_PATH_ERROR_MESSAGE + (this._name != null ? ` - ${this._name}` : ''), 'recoil', ); throw new ChangedPathError(); } } module.exports = {TreeCache}; ================================================ FILE: packages/recoil/caches/Recoil_TreeCacheImplementationType.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; import type {NodeKey} from '../core/Recoil_Keys'; export type NodeCacheRoute = Array<[NodeKey, mixed]>; export type TreeCacheNode = TreeCacheLeaf | TreeCacheBranch; export type TreeCacheLeaf = { type: 'leaf', value: T, branchKey?: mixed, parent: ?TreeCacheBranch, }; export type TreeCacheBranch = { type: 'branch', nodeKey: NodeKey, branches: Map>, branchKey?: mixed, parent: ?TreeCacheBranch, }; export type NodeValueGet = (nodeKey: NodeKey) => mixed; type NodeVisitHandler = (node: TreeCacheNode) => void; export type GetHandlers = { onNodeVisit: NodeVisitHandler, }; export type SetHandlers = { onNodeVisit: NodeVisitHandler, }; /** * This is an opinionated tree cache that conforms to the requirements needed * by Recoil selectors. * * Unlike a conventional cache, the tree cache does not store key-value pairs, * but "routes" that point to values. In the context of selectors these routes * represent dependencies that a selector has to other atoms and selectors. * * In order to retrieve a value from the cache, a function is passed to the * cache's `get()` method, and the tree cache will use that function to traverse * itself, passing the provided function a "key" (the first part of the route tuple), * reconstructing the route to some value (or undefined). * * The handlers are necessary for the selector to be able to capture the * incremental nodes in the tree that are traversed while looking for a cache * hit as these incremental nodes represent dependencies to the selector, which * are used internally by the selector. */ export interface TreeCacheImplementation { get(NodeValueGet, handlers?: GetHandlers): ?T; set(NodeCacheRoute, T, handlers?: SetHandlers): void; delete(TreeCacheLeaf): boolean; clear(): void; root(): ?TreeCacheNode; size(): number; } ================================================ FILE: packages/recoil/caches/Recoil_cacheFromPolicy.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; import type {CacheImplementation} from './Recoil_CacheImplementationType'; import type { CachePolicy, EqualityPolicy, EvictionPolicy, } from './Recoil_CachePolicy'; const {LRUCache} = require('./Recoil_LRUCache'); const {MapCache} = require('./Recoil_MapCache'); const err = require('recoil-shared/util/Recoil_err'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const stableStringify = require('recoil-shared/util/Recoil_stableStringify'); const defaultPolicy: { equality: 'reference', eviction: 'none', maxSize: number, } = { equality: 'reference', eviction: 'none', maxSize: Infinity, }; function cacheFromPolicy({ equality = defaultPolicy.equality, eviction = defaultPolicy.eviction, maxSize = defaultPolicy.maxSize, }: // $FlowFixMe[incompatible-type] CachePolicy = defaultPolicy): CacheImplementation { const valueMapper = getValueMapper(equality); const cache = getCache(eviction, maxSize, valueMapper); return cache; } function getValueMapper(equality: EqualityPolicy): mixed => mixed { switch (equality) { case 'reference': return val => val; case 'value': return val => stableStringify(val); } throw err(`Unrecognized equality policy ${equality}`); } function getCache( eviction: EvictionPolicy, maxSize: ?number, mapKey: mixed => mixed, ): CacheImplementation { switch (eviction) { case 'keep-all': return new MapCache({mapKey}); case 'lru': return new LRUCache({mapKey, maxSize: nullthrows(maxSize)}); case 'most-recent': return new LRUCache({mapKey, maxSize: 1}); } throw err(`Unrecognized eviction policy ${eviction}`); } module.exports = cacheFromPolicy; ================================================ FILE: packages/recoil/caches/Recoil_treeCacheFromPolicy.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { CachePolicy, EqualityPolicy, EvictionPolicy, } from './Recoil_CachePolicy'; import type {TreeCacheImplementation} from './Recoil_TreeCacheImplementationType'; const {TreeCache} = require('./Recoil_TreeCache'); const treeCacheLRU = require('./Recoil_treeCacheLRU'); const err = require('recoil-shared/util/Recoil_err'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const stableStringify = require('recoil-shared/util/Recoil_stableStringify'); const defaultPolicy: { equality: 'reference', eviction: 'keep-all', maxSize: number, } = { equality: 'reference', eviction: 'keep-all', maxSize: Infinity, }; function treeCacheFromPolicy( { equality = defaultPolicy.equality, eviction = defaultPolicy.eviction, maxSize = defaultPolicy.maxSize, }: // $FlowFixMe[incompatible-type] CachePolicy = defaultPolicy, name?: string, ): TreeCacheImplementation { const valueMapper = getValueMapper(equality); return getTreeCache(eviction, maxSize, valueMapper, name); } function getValueMapper(equality: EqualityPolicy): mixed => mixed { switch (equality) { case 'reference': return val => val; case 'value': return val => stableStringify(val); } throw err(`Unrecognized equality policy ${equality}`); } function getTreeCache( eviction: EvictionPolicy, maxSize: ?number, mapNodeValue: mixed => mixed, name?: string, ): TreeCacheImplementation { switch (eviction) { case 'keep-all': return new TreeCache({name, mapNodeValue}); case 'lru': return treeCacheLRU({ name, maxSize: nullthrows(maxSize), mapNodeValue, }); case 'most-recent': return treeCacheLRU({name, maxSize: 1, mapNodeValue}); } throw err(`Unrecognized eviction policy ${eviction}`); } module.exports = treeCacheFromPolicy; ================================================ FILE: packages/recoil/caches/Recoil_treeCacheLRU.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {TreeCacheImplementation} from './Recoil_TreeCacheImplementationType'; const {LRUCache} = require('./Recoil_LRUCache'); const {TreeCache} = require('./Recoil_TreeCache'); function treeCacheLRU({ name, maxSize, mapNodeValue = (v: mixed) => v, }: { name?: string, maxSize: number, mapNodeValue?: mixed => mixed, }): TreeCacheImplementation { const lruCache = new LRUCache({maxSize}); const cache: TreeCache = new TreeCache({ name, mapNodeValue, onHit: node => { lruCache.set(node, true); }, onSet: node => { const lruNode = lruCache.tail(); lruCache.set(node, true); if (lruNode && cache.size() > maxSize) { // $FlowFixMe[incompatible-call] cache.delete(lruNode.key); } }, }); return cache; } module.exports = treeCacheLRU; ================================================ FILE: packages/recoil/caches/__tests__/Recoil_LRUCache-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let LRUCache; const testRecoil = getRecoilTestFn(() => { ({LRUCache} = require('../Recoil_LRUCache')); }); describe('LRUCache', () => { testRecoil('setting and getting (without hitting max size)', () => { const cache = new LRUCache({ maxSize: 10, }); cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); expect(cache.size()).toBe(3); expect(cache.get('a')).toBe(1); expect(cache.get('b')).toBe(2); expect(cache.get('c')).toBe(3); cache.delete('a'); cache.delete('b'); expect(cache.size()).toBe(1); }); testRecoil('setting and getting (hitting max size)', () => { const cache = new LRUCache({ maxSize: 2, }); cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); expect(cache.size()).toBe(2); expect(cache.get('a')).toBe(undefined); expect(cache.get('b')).toBe(2); expect(cache.get('c')).toBe(3); cache.delete('a'); cache.delete('b'); expect(cache.size()).toBe(1); cache.set('d', 4); cache.set('e', 5); expect(cache.size()).toBe(2); expect(cache.get('b')).toBe(undefined); expect(cache.get('c')).toBe(undefined); }); testRecoil('manually deleting LRU', () => { const cache = new LRUCache({ maxSize: 10, }); cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); expect(cache.size()).toBe(3); expect(cache.get('a')).toBe(1); expect(cache.get('b')).toBe(2); expect(cache.get('c')).toBe(3); cache.deleteLru(); // delete 'a' expect(cache.get('a')).toBe(undefined); expect(cache.size()).toBe(2); cache.deleteLru(); // delete 'b' expect(cache.get('b')).toBe(undefined); expect(cache.size()).toBe(1); }); testRecoil('head() and tail()', () => { const cache = new LRUCache({ maxSize: 10, }); cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); expect(cache.size()).toBe(3); expect(cache.tail()).toBeDefined(); expect(cache.tail()?.value).toBe(1); expect(cache.head()?.value).toBe(3); expect(cache.get('c')).toBe(3); expect(cache.get('b')).toBe(2); expect(cache.get('a')).toBe(1); expect(cache.tail()?.value).toBe(3); expect(cache.head()?.value).toBe(1); cache.delete('a'); cache.delete('b'); expect(cache.tail()?.value).toBe(3); expect(cache.head()?.value).toBe(3); expect(cache.size()).toBe(1); }); }); ================================================ FILE: packages/recoil/caches/__tests__/Recoil_MapCache-test.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let MapCache; const testRecoil = getRecoilTestFn(() => { ({MapCache} = require('../Recoil_MapCache')); }); describe('MapCache', () => { testRecoil('setting and getting', () => { const cache = new MapCache(); cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); expect(cache.size()).toBe(3); expect(cache.get('a')).toBe(1); expect(cache.get('b')).toBe(2); expect(cache.get('c')).toBe(3); }); testRecoil('deleting', () => { const cache = new MapCache(); cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); expect(cache.size()).toBe(3); cache.delete('a'); expect(cache.size()).toBe(2); expect(cache.get('a')).toBe(undefined); expect(cache.get('b')).toBe(2); expect(cache.has('a')).toBe(false); }); }); ================================================ FILE: packages/recoil/caches/__tests__/Recoil_TreeCache-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {NodeKey} from 'Recoil_Keys'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let TreeCache, loadableWithValue, nullthrows; const testRecoil = getRecoilTestFn(() => { ({TreeCache} = require('../Recoil_TreeCache')); nullthrows = require('recoil-shared/util/Recoil_nullthrows'); ({loadableWithValue} = require('../../adt/Recoil_Loadable')); }); describe('TreeCache', () => { testRecoil('setting and getting values', () => { const cache = new TreeCache(); const [route1, loadable1] = [ [ ['a', 2], ['b', 3], ], loadableWithValue('value1'), ]; const [route2, loadable2] = [ [ ['a', 3], ['b', 4], ], loadableWithValue('value2'), ]; const [route3, loadable3] = [[['a', 4]], loadableWithValue('value3')]; cache.set(route1, loadable1); cache.set(route2, loadable2); cache.set(route3, loadable3); expect( cache.get(nodeKey => route1.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable1); expect( cache.get(nodeKey => route2.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable2); expect( cache.get(nodeKey => route3.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable3); expect(cache.size()).toBe(3); }); testRecoil('deleting values', () => { const cache = new TreeCache(); const [route1, loadable1] = [ [ ['a', 2], ['b', 3], ], loadableWithValue('value1'), ]; const [route2, loadable2] = [ [ ['a', 2], ['b', 4], ['c', 5], ], loadableWithValue('value2'), ]; const [route3, loadable3] = [[['a', 6]], loadableWithValue('value3')]; cache.set(route1, loadable1); cache.set(route2, loadable2); cache.set(route3, loadable3); const leaf1 = cache.getLeafNode( nodeKey => route1.find(([key]) => key === nodeKey)?.[1], ); const leaf2 = cache.getLeafNode( nodeKey => route2.find(([key]) => key === nodeKey)?.[1], ); const leaf3 = cache.getLeafNode( nodeKey => route3.find(([key]) => key === nodeKey)?.[1], ); expect(leaf1).toBeDefined(); expect(leaf2).toBeDefined(); expect(leaf3).toBeDefined(); const leaf1Node = nullthrows(leaf1); const leaf2Node = nullthrows(leaf2); const leaf3Node = nullthrows(leaf3); expect(cache.size()).toBe(3); const deleted1 = cache.delete(leaf1Node); expect(deleted1).toBe(true); expect(cache.size()).toBe(2); const deleted2 = cache.delete(leaf2Node); expect(deleted2).toBe(true); expect(cache.size()).toBe(1); const deleted3 = cache.delete(leaf3Node); expect(deleted3).toBe(true); expect(cache.size()).toBe(0); expect(cache.root()).toBeNull(); const deletedAgain = cache.delete(leaf1Node); expect(deletedAgain).toBe(false); }); testRecoil('onHit() handler', () => { const [route1, loadable1] = [ [ ['a', 2], ['b', 3], ], loadableWithValue('value1'), ]; const onHit = jest.fn(); const cache = new TreeCache({ onHit, }); const getter = (nodeKey: NodeKey) => route1.find(([key]) => key === nodeKey)?.[1]; cache.set(route1, loadable1); // hit cache.get(getter); // miss cache.get(() => {}); // hit cache.get(getter); expect(onHit).toHaveBeenCalledTimes(2); }); testRecoil('onSet() handler', () => { const onSet = jest.fn(); const cache = new TreeCache({ onSet, }); const [route1, loadable1] = [ [ ['a', 2], ['b', 3], ], loadableWithValue('value1'), ]; const [route2, loadable2] = [ [ ['a', 3], ['b', 4], ], loadableWithValue('value2'), ]; const [route3, loadable3] = [[['a', 4]], loadableWithValue('value3')]; cache.set(route1, loadable1); cache.set(route2, loadable2); cache.set(route3, loadable3); expect(onSet).toHaveBeenCalledTimes(3); }); testRecoil('default key generation uses reference equality', () => { const [route1, loadable1] = [ [ ['a', [2]], ['b', [3]], ], loadableWithValue('value1'), ]; const cache = new TreeCache(); cache.set(route1, loadable1); const resultWithKeyCopy = cache.get(nodeKey => [ ...(route1.find(([key]) => key === nodeKey)?.[1] ?? []), ]); expect(resultWithKeyCopy).toBeUndefined(); const result = cache.get( nodeKey => route1.find(([key]) => key === nodeKey)?.[1], ); expect(result).toBe(loadable1); }); testRecoil('mapNodeValue() to implement value equality keys', () => { const cache = new TreeCache({ mapNodeValue: value => JSON.stringify(value), }); const [route1, loadable1] = [ [ ['a', [2]], ['b', [3]], ], loadableWithValue('value1'), ]; cache.set(route1, loadable1); const resultWithKeyCopy = cache.get(nodeKey => [ ...(route1.find(([key]) => key === nodeKey)?.[1] ?? []), ]); expect(resultWithKeyCopy).toBe(loadable1); }); // Test ability to scale cache to large number of entries. // Use more dependencies than the JavaScript callstack depth limit to ensure // we are not using a recursive algorithm. testRecoil('Scalability', () => { const cache = new TreeCache(); const route = Array.from(Array(10000).keys()).map(i => [ String(i), String(i), ]); // $FlowFixMe[incompatible-call] cache.set(route, 'VALUE'); expect(cache.get(x => x)).toBe('VALUE'); const leafNode = cache.getLeafNode(x => x); expect(cache.delete(nullthrows(leafNode))).toBe(true); }); }); ================================================ FILE: packages/recoil/caches/__tests__/Recoil_cacheFromPolicy-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let cacheFromPolicy; const testRecoil = getRecoilTestFn(() => { cacheFromPolicy = require('../Recoil_cacheFromPolicy'); }); describe('cacheFromPolicy()', () => { testRecoil('equality: reference, eviction: keep-all', () => { const policy = {equality: 'reference', eviction: 'keep-all'}; const cache = cacheFromPolicy<{[string]: number}, boolean>(policy); const obj1 = {a: 1}; const obj2 = {b: 2}; const obj3 = {c: 3}; cache.set(obj1, true); cache.set(obj2, true); cache.set(obj3, true); expect(cache.size()).toBe(3); expect(cache.get(obj1)).toBe(true); expect(cache.get(obj2)).toBe(true); expect(cache.get(obj3)).toBe(true); expect(cache.get({...obj1})).toBe(undefined); expect(cache.get({...obj2})).toBe(undefined); expect(cache.get({...obj3})).toBe(undefined); }); testRecoil('equality: value, eviction: keep-all', () => { const policy = {equality: 'value', eviction: 'keep-all'}; const cache = cacheFromPolicy<{[string]: number}, boolean>(policy); const obj1 = {a: 1}; const obj2 = {b: 2}; const obj3 = {c: 3}; cache.set(obj1, true); cache.set(obj2, true); cache.set(obj3, true); expect(cache.size()).toBe(3); expect(cache.get(obj1)).toBe(true); expect(cache.get(obj2)).toBe(true); expect(cache.get(obj3)).toBe(true); expect(cache.get({...obj1})).toBe(true); expect(cache.get({...obj2})).toBe(true); expect(cache.get({...obj3})).toBe(true); }); testRecoil('equality: reference, eviction: lru', () => { const policy = {equality: 'reference', eviction: 'lru', maxSize: 2}; const cache = cacheFromPolicy<{[string]: number}, boolean>(policy); const obj1 = {a: 1}; const obj2 = {b: 2}; const obj3 = {c: 3}; cache.set(obj1, true); cache.set(obj2, true); cache.set(obj3, true); expect(cache.size()).toBe(2); expect(cache.get(obj1)).toBe(undefined); expect(cache.get(obj2)).toBe(true); expect(cache.get(obj3)).toBe(true); cache.set(obj1, true); expect(cache.size()).toBe(2); expect(cache.get(obj2)).toBe(undefined); expect(cache.get(obj1)).toBe(true); expect(cache.get(obj3)).toBe(true); expect(cache.get({...obj1})).toBe(undefined); expect(cache.get({...obj3})).toBe(undefined); }); testRecoil('equality: value, eviction: lru', () => { const policy = {equality: 'value', eviction: 'lru', maxSize: 2}; const cache = cacheFromPolicy<{[string]: number}, boolean>(policy); const obj1 = {a: 1}; const obj2 = {b: 2}; const obj3 = {c: 3}; cache.set(obj1, true); cache.set(obj2, true); cache.set(obj3, true); expect(cache.size()).toBe(2); expect(cache.get(obj1)).toBe(undefined); expect(cache.get(obj2)).toBe(true); expect(cache.get(obj3)).toBe(true); cache.set(obj1, true); expect(cache.size()).toBe(2); expect(cache.get(obj2)).toBe(undefined); expect(cache.get(obj1)).toBe(true); expect(cache.get(obj3)).toBe(true); expect(cache.get({...obj2})).toBe(undefined); expect(cache.get({...obj1})).toBe(true); expect(cache.get({...obj3})).toBe(true); }); testRecoil('equality: reference, eviction: most-recent', () => { const policy = {equality: 'reference', eviction: 'most-recent'}; const cache = cacheFromPolicy<{[string]: number}, boolean>(policy); const obj1 = {a: 1}; const obj2 = {b: 2}; const obj3 = {c: 3}; cache.set(obj1, true); cache.set(obj2, true); cache.set(obj3, true); expect(cache.size()).toBe(1); expect(cache.get(obj1)).toBe(undefined); expect(cache.get(obj2)).toBe(undefined); expect(cache.get(obj3)).toBe(true); cache.set(obj1, true); expect(cache.size()).toBe(1); expect(cache.get(obj2)).toBe(undefined); expect(cache.get(obj3)).toBe(undefined); expect(cache.get(obj1)).toBe(true); expect(cache.get({...obj2})).toBe(undefined); expect(cache.get({...obj1})).toBe(undefined); expect(cache.get({...obj3})).toBe(undefined); }); testRecoil('equality: value, eviction: most-recent', () => { const policy = {equality: 'value', eviction: 'most-recent'}; const cache = cacheFromPolicy<{[string]: number}, boolean>(policy); const obj1 = {a: 1}; const obj2 = {b: 2}; const obj3 = {c: 3}; cache.set(obj1, true); cache.set(obj2, true); cache.set(obj3, true); expect(cache.size()).toBe(1); expect(cache.get(obj1)).toBe(undefined); expect(cache.get(obj2)).toBe(undefined); expect(cache.get(obj3)).toBe(true); cache.set(obj1, true); expect(cache.size()).toBe(1); expect(cache.get(obj2)).toBe(undefined); expect(cache.get(obj3)).toBe(undefined); expect(cache.get(obj1)).toBe(true); expect(cache.get({...obj2})).toBe(undefined); expect(cache.get({...obj3})).toBe(undefined); expect(cache.get({...obj1})).toBe(true); }); }); ================================================ FILE: packages/recoil/caches/__tests__/Recoil_treeCacheFromPolicy-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {NodeKey} from 'Recoil_Keys'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let treeCacheFromPolicy; const testRecoil = getRecoilTestFn(() => { treeCacheFromPolicy = require('../Recoil_treeCacheFromPolicy'); }); /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ const valGetterFromPath = path => (nodeKey: NodeKey) => path.find(([k]) => k === nodeKey)?.[1]; /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ const clonePath = path => JSON.parse(JSON.stringify(path)); describe('treeCacheFromPolicy()', () => { testRecoil('equality: reference, eviction: keep-all', () => { const policy = {equality: 'reference', eviction: 'keep-all'}; const cache = treeCacheFromPolicy<{[string]: number}>(policy); const path1 = [ ['a', [1]], ['b', [2]], ]; const obj1 = {a: 1}; const path2 = [['a', [2]]]; const obj2 = {b: 2}; const path3 = [ ['a', [3]], ['c', [4]], ]; const obj3 = {c: 3}; cache.set(path1, obj1); cache.set(path2, obj2); cache.set(path3, obj3); expect(cache.size()).toBe(3); expect(cache.get(valGetterFromPath(path1))).toBe(obj1); expect(cache.get(valGetterFromPath(path2))).toBe(obj2); expect(cache.get(valGetterFromPath(path3))).toBe(obj3); expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(undefined); expect(cache.get(valGetterFromPath(clonePath(path2)))).toBe(undefined); expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(undefined); }); testRecoil('equality: value, eviction: keep-all', () => { const policy = {equality: 'value', eviction: 'keep-all'}; const cache = treeCacheFromPolicy<{[string]: number}>(policy); const path1 = [ ['a', [1]], ['b', [2]], ]; const obj1 = {a: 1}; const path2 = [['a', [2]]]; const obj2 = {b: 2}; const path3 = [ ['a', [3]], ['c', [4]], ]; const obj3 = {c: 3}; cache.set(path1, obj1); cache.set(path2, obj2); cache.set(path3, obj3); expect(cache.size()).toBe(3); expect(cache.get(valGetterFromPath(path1))).toBe(obj1); expect(cache.get(valGetterFromPath(path2))).toBe(obj2); expect(cache.get(valGetterFromPath(path3))).toBe(obj3); expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(obj1); expect(cache.get(valGetterFromPath(clonePath(path2)))).toBe(obj2); expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(obj3); }); testRecoil('equality: reference, eviction: lru', () => { const policy = {equality: 'reference', eviction: 'lru', maxSize: 2}; const cache = treeCacheFromPolicy<{[string]: number}>(policy); const path1 = [ ['a', [1]], ['b', [2]], ]; const obj1 = {a: 1}; const path2 = [['a', [2]]]; const obj2 = {b: 2}; const path3 = [ ['a', [3]], ['c', [4]], ]; const obj3 = {c: 3}; cache.set(path1, obj1); cache.set(path2, obj2); cache.set(path3, obj3); expect(cache.size()).toBe(2); expect(cache.get(valGetterFromPath(path1))).toBe(undefined); expect(cache.get(valGetterFromPath(path2))).toBe(obj2); expect(cache.get(valGetterFromPath(path3))).toBe(obj3); cache.set(path1, obj1); expect(cache.size()).toBe(2); expect(cache.get(valGetterFromPath(path2))).toBe(undefined); expect(cache.get(valGetterFromPath(path1))).toBe(obj1); expect(cache.get(valGetterFromPath(path3))).toBe(obj3); expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(undefined); expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(undefined); }); testRecoil('equality: value, eviction: lru', () => { const policy = {equality: 'value', eviction: 'lru', maxSize: 2}; const cache = treeCacheFromPolicy<{[string]: number}>(policy); const path1 = [ ['a', [1]], ['b', [2]], ]; const obj1 = {a: 1}; const path2 = [['a', [2]]]; const obj2 = {b: 2}; const path3 = [ ['a', [3]], ['c', [4]], ]; const obj3 = {c: 3}; cache.set(path1, obj1); cache.set(path2, obj2); cache.set(path3, obj3); expect(cache.size()).toBe(2); expect(cache.get(valGetterFromPath(path1))).toBe(undefined); expect(cache.get(valGetterFromPath(path2))).toBe(obj2); expect(cache.get(valGetterFromPath(path3))).toBe(obj3); cache.set(path1, obj1); expect(cache.size()).toBe(2); expect(cache.get(valGetterFromPath(path2))).toBe(undefined); expect(cache.get(valGetterFromPath(path1))).toBe(obj1); expect(cache.get(valGetterFromPath(path3))).toBe(obj3); expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(obj1); expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(obj3); }); testRecoil('equality: reference, eviction: most-recent', () => { const policy = {equality: 'reference', eviction: 'most-recent'}; const cache = treeCacheFromPolicy<{[string]: number}>(policy); const path1 = [ ['a', [1]], ['b', [2]], ]; const obj1 = {a: 1}; const path2 = [['a', [2]]]; const obj2 = {b: 2}; const path3 = [ ['a', [3]], ['c', [4]], ]; const obj3 = {c: 3}; cache.set(path1, obj1); cache.set(path2, obj2); cache.set(path3, obj3); expect(cache.size()).toBe(1); expect(cache.get(valGetterFromPath(path1))).toBe(undefined); expect(cache.get(valGetterFromPath(path2))).toBe(undefined); expect(cache.get(valGetterFromPath(path3))).toBe(obj3); cache.set(path1, obj1); expect(cache.size()).toBe(1); expect(cache.get(valGetterFromPath(path2))).toBe(undefined); expect(cache.get(valGetterFromPath(path3))).toBe(undefined); expect(cache.get(valGetterFromPath(path1))).toBe(obj1); expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(undefined); expect(cache.get(valGetterFromPath(clonePath(path2)))).toBe(undefined); expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(undefined); }); testRecoil('equality: value, eviction: most-recent', () => { const policy = {equality: 'value', eviction: 'most-recent'}; const cache = treeCacheFromPolicy<{[string]: number}>(policy); const path1 = [ ['a', [1]], ['b', [2]], ]; const obj1 = {a: 1}; const path2 = [['a', [2]]]; const obj2 = {b: 2}; const path3 = [ ['a', [3]], ['c', [4]], ]; const obj3 = {c: 3}; cache.set(path1, obj1); cache.set(path2, obj2); cache.set(path3, obj3); expect(cache.size()).toBe(1); expect(cache.get(valGetterFromPath(path1))).toBe(undefined); expect(cache.get(valGetterFromPath(path2))).toBe(undefined); expect(cache.get(valGetterFromPath(path3))).toBe(obj3); cache.set(path1, obj1); expect(cache.size()).toBe(1); expect(cache.get(valGetterFromPath(path2))).toBe(undefined); expect(cache.get(valGetterFromPath(path3))).toBe(undefined); expect(cache.get(valGetterFromPath(path1))).toBe(obj1); expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(obj1); expect(cache.get(valGetterFromPath(clonePath(path2)))).toBe(undefined); expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(undefined); }); }); ================================================ FILE: packages/recoil/caches/__tests__/Recoil_treeCacheLRU-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let treeCacheLRU, loadableWithValue; const testRecoil = getRecoilTestFn(() => { treeCacheLRU = require('../Recoil_treeCacheLRU'); ({loadableWithValue} = require('../../adt/Recoil_Loadable')); }); describe('treeCacheLRU()', () => { testRecoil('getting and setting cache', () => { const cache = treeCacheLRU<$FlowFixMe>({maxSize: 10}); const [route1, loadable1] = [ [ ['a', 2], ['b', 3], ], loadableWithValue('value1'), ]; const [route2, loadable2] = [ [ ['a', 3], ['b', 4], ], loadableWithValue('value2'), ]; const [route3, loadable3] = [[['a', 4]], loadableWithValue('value3')]; cache.set(route1, loadable1); cache.set(route2, loadable2); cache.set(route3, loadable3); expect( cache.get(nodeKey => route1.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable1); expect( cache.get(nodeKey => route2.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable2); expect( cache.get(nodeKey => route3.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable3); expect(cache.size()).toBe(3); }); testRecoil('getting and setting cache (hitting max size)', () => { const cache = treeCacheLRU<$FlowFixMe>({maxSize: 2}); const [route1, loadable1] = [ [ ['a', 2], ['b', 3], ], loadableWithValue('value1'), ]; const [route2, loadable2] = [ [ ['a', 3], ['b', 4], ], loadableWithValue('value2'), ]; const [route3, loadable3] = [[['a', 4]], loadableWithValue('value3')]; cache.set(route1, loadable1); cache.set(route2, loadable2); cache.set(route3, loadable3); expect( cache.get(nodeKey => route1.find(([key]) => key === nodeKey)?.[1]), ).toBe(undefined); expect( cache.get(nodeKey => route2.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable2); expect( cache.get(nodeKey => route3.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable3); expect(cache.size()).toBe(2); cache.set(route1, loadable1); expect( cache.get(nodeKey => route1.find(([key]) => key === nodeKey)?.[1]), ).toBe(loadable1); expect( cache.get(nodeKey => route2.find(([key]) => key === nodeKey)?.[1]), ).toBe(undefined); expect(cache.size()).toBe(2); }); }); ================================================ FILE: packages/recoil/contrib/devtools_connector/RecoilDevTools_Connector.react.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Snapshot} from '../../core/Recoil_Snapshot'; const { useGotoRecoilSnapshot, useRecoilSnapshot, } = require('../../hooks/Recoil_SnapshotHooks'); const React = require('react'); const {useEffect, useRef} = require('react'); type Props = $ReadOnly<{ name?: string, persistenceLimit?: number, initialSnapshot?: ?Snapshot, devMode?: ?boolean, maxDepth?: number, maxItems?: number, serializeFn?: (mixed, string) => mixed, }>; type ConnectProps = $ReadOnly<{ ...Props, goToSnapshot: Snapshot => void, }>; function connect(props: ConnectProps): ?{ track: (transactionId: number, snapshot: Snapshot) => void, disconnect: () => void, } { if (typeof window === 'undefined') { return null; } return window.__RECOIL_DEVTOOLS_EXTENSION__?.connect?.(props); } let CONNECTION_INDEX = 0; /** * @explorer-desc * Recoil Dev Tools Connector */ function Connector({ name = `Recoil Connection ${CONNECTION_INDEX++}`, persistenceLimit = 50, maxDepth, maxItems, serializeFn, devMode = true, }: Props): React.Node { const transactionIdRef = useRef(0); const connectionRef = useRef void, track: (transactionId: number, snapshot: Snapshot) => void, }>(null); const goToSnapshot = useGotoRecoilSnapshot(); const snapshot = useRecoilSnapshot(); const release = snapshot.retain(); useEffect(() => { if (connectionRef.current == null) { connectionRef.current = connect({ name, persistenceLimit, devMode, goToSnapshot, maxDepth, maxItems, serializeFn, }); } return () => { connectionRef.current?.disconnect(); connectionRef.current = null; }; }, [ devMode, goToSnapshot, maxDepth, maxItems, name, persistenceLimit, serializeFn, ]); useEffect(() => { try { const transactionID = transactionIdRef.current++; connectionRef.current?.track?.(transactionID, snapshot); } finally { release(); } }, [snapshot, release]); return null; } module.exports = Connector; ================================================ FILE: packages/recoil/contrib/uri_persistence/Recoil_Link.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {MutableSnapshot, Snapshot} from '../../core/Recoil_Snapshot'; const { useGotoRecoilSnapshot, useRecoilSnapshot, } = require('../../hooks/Recoil_SnapshotHooks'); const React = require('react'); const {useCallback} = require('react'); type AnchorProps = { download?: true | string, rel?: string, target?: '_self' | '_blank' | '_parent' | '_top', onClick?: (SyntheticUIEvent) => void, style?: {[string]: string | number, ...}, children?: React.Node, }; type SerializationProps = { uriFromSnapshot: Snapshot => string, }; type LinkToSnapshotProps = { ...AnchorProps, ...SerializationProps, snapshot: Snapshot, }; // A Link component based on the provided `uriFromSnapshot` mapping // of a URI from a Recoil Snapshot. // // The Link element renders an anchor element. But instead of an href, use a // `snapshot` property. When clicked, the Link element updates the current // state to the snapshot without loading a new document. // // The href property of the anchor will set using `uriFromSnapshot`. This // allows users to copy the link, choose to open in a new tab, &c. // // If an `onClick` handler is provided, it is called before the state transition // and may call preventDefault on the event to stop the state transition. function LinkToRecoilSnapshot({ uriFromSnapshot, snapshot, ...anchorProps }: LinkToSnapshotProps): React.Node { const gotoSnapshot = useGotoRecoilSnapshot(); const {onClick, target} = anchorProps; const onClickWrapper = useCallback( (event: $FlowFixMe) => { onClick?.(event); if ( !event.defaultPrevented && event.button === 0 && // left-click !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) && (!target || target === '_self') ) { event.preventDefault(); gotoSnapshot(snapshot); } }, [target, onClick, gotoSnapshot, snapshot], ); return ( ); } type LinkToStateChangeProps = { ...AnchorProps, ...SerializationProps, stateChange: MutableSnapshot => void, }; // A Link component based on the provided `uriFromSnapshot` mapping // of a URI from a Recoil Snapshot. // // The Link element renders an anchor element. But instead of an href, use a // `stateChange` property. When clicked, the Link element updates the current // state based on the `stateChange` callback without loading a new document. // `stateChange` is a function which takes a `MutableSnapshot` that can be used // to read the current state and set or update any changes. // // The href property of the anchor will set using `uriFromSnapshot`. This // allows users to copy the link, choose to open in a new tab, &c. // // If an `onClick` handler is provided, it is called before the state transition // and may call preventDefault on the event to stop the state transition. // // Note that, because the link renders the href based on the current state // snapshot, it is re-rendered whenever any state change is made. Keep the // performance implications of this in mind. function LinkToRecoilStateChange({ stateChange, ...linkProps }: LinkToStateChangeProps): React.Node { const currentSnapshot = useRecoilSnapshot(); const snapshot = currentSnapshot.map(stateChange); return ; } module.exports = { LinkToRecoilSnapshot, LinkToRecoilStateChange, }; ================================================ FILE: packages/recoil/contrib/uri_persistence/__tests__/Recoil_Link-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {MutableSnapshot, Snapshot} from 'Recoil_Snapshot'; const {Simulate, act} = require('ReactTestUtils'); const {freshSnapshot} = require('../../../core/Recoil_Snapshot'); const atom = require('../../../recoil_values/Recoil_atom'); const { LinkToRecoilSnapshot, LinkToRecoilStateChange, } = require('../Recoil_Link'); const React = require('react'); const { componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const myAtom = atom({key: 'Link Snapshot', default: 'DEFAULT'}); const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const LinkToSnapshot = ({ snapshot, children, }: $TEMPORARY$object<{ children: Array<$TEMPORARY$string<'LINK-'> | string>, snapshot: Snapshot, }>) => ( `https://test.com/test?atom="${getLoadable(myAtom) .valueOrThrow() .toString()}` }> {children} ); const LinkToStateChange = ({ stateChange, children, }: $TEMPORARY$object<{ children: $TEMPORARY$string<'LINK'>, stateChange: MutableSnapshot => void, }>) => ( `https://test.com/test?atom="${getLoadable(myAtom) .valueOrThrow() .toString()}` }> {children} ); test('Link - snapshot', async () => { const snapshot = freshSnapshot().map(({set}) => set(myAtom, 'MAP')); const c = renderElements( <> LINK-{snapshot.getLoadable(myAtom).valueOrThrow().toString()} , ); expect(c.textContent).toEqual('"DEFAULT"LINK-MAP'); act(() => setAtom('SET')); expect(c.textContent).toEqual('"SET"LINK-MAP'); // flowlint-next-line unclear-type:off expect(((c.children[0]: any): HTMLAnchorElement).href).toEqual( 'https://test.com/test?atom=%22MAP', ); act(() => { Simulate.click(c.children[0], {button: 0}); }); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"MAP"LINK-MAP'); }); test('Link - stateChange', async () => { const c = renderElements( <> set(myAtom, 'MAP')}> LINK , ); expect(c.textContent).toEqual('"DEFAULT"LINK'); act(() => setAtom('SET')); expect(c.textContent).toEqual('"SET"LINK'); // flowlint-next-line unclear-type:off expect(((c.children[0]: any): HTMLAnchorElement).href).toEqual( 'https://test.com/test?atom=%22MAP', ); act(() => { Simulate.click(c.children[0], {button: 0}); }); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"MAP"LINK'); }); test('Link - state update', async () => { const c = renderElements( <> set(myAtom, value => 'MAP ' + value)}> LINK , ); expect(c.textContent).toEqual('"DEFAULT"LINK'); act(() => setAtom('SET')); expect(c.textContent).toEqual('"SET"LINK'); // flowlint-next-line unclear-type:off expect(((c.children[0]: any): HTMLAnchorElement).href).toEqual( 'https://test.com/test?atom=%22MAP%20SET', ); act(() => { Simulate.click(c.children[0], {button: 0}); }); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"MAP SET"LINK'); }); ================================================ FILE: packages/recoil/core/Recoil_AtomicUpdates.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {ValueOrUpdater} from '../recoil_values/Recoil_callbackTypes'; import type {RecoilState, RecoilValue} from './Recoil_RecoilValue'; import type {NodeKey, Store, TreeState} from './Recoil_State'; const {loadableWithValue} = require('../adt/Recoil_Loadable'); const {initializeNode} = require('./Recoil_FunctionalCore'); const {DEFAULT_VALUE, getNode} = require('./Recoil_Node'); const { copyTreeState, getRecoilValueAsLoadable, invalidateDownstreams, writeLoadableToTreeState, } = require('./Recoil_RecoilValueInterface'); const err = require('recoil-shared/util/Recoil_err'); export interface TransactionInterface { get: (RecoilValue) => T; set: (RecoilState, ValueOrUpdater) => void; reset: (RecoilState) => void; } function isAtom(recoilValue: RecoilValue): boolean { return getNode(recoilValue.key).nodeType === 'atom'; } class TransactionInterfaceImpl { _store: Store; _treeState: TreeState; _changes: Map; constructor(store: Store, treeState: TreeState) { this._store = store; this._treeState = treeState; this._changes = new Map(); } // Allow destructing // eslint-disable-next-line fb-www/extra-arrow-initializer get = (recoilValue: RecoilValue): T => { if (this._changes.has(recoilValue.key)) { // $FlowIssue[incompatible-return] return this._changes.get(recoilValue.key); } if (!isAtom(recoilValue)) { throw err('Reading selectors within atomicUpdate is not supported'); } const loadable = getRecoilValueAsLoadable( this._store, recoilValue, this._treeState, ); if (loadable.state === 'hasValue') { return loadable.contents; } else if (loadable.state === 'hasError') { throw loadable.contents; } else { throw err( `Expected Recoil atom ${recoilValue.key} to have a value, but it is in a loading state.`, ); } }; // Allow destructing // eslint-disable-next-line fb-www/extra-arrow-initializer set = ( recoilState: RecoilState, valueOrUpdater: ValueOrUpdater, ): void => { if (!isAtom(recoilState)) { throw err('Setting selectors within atomicUpdate is not supported'); } if (typeof valueOrUpdater === 'function') { const current = this.get(recoilState); this._changes.set(recoilState.key, (valueOrUpdater: any)(current)); // flowlint-line unclear-type:off } else { // Initialize atom and run effects if not initialized yet initializeNode(this._store, recoilState.key, 'set'); this._changes.set(recoilState.key, valueOrUpdater); } }; // Allow destructing // eslint-disable-next-line fb-www/extra-arrow-initializer reset = (recoilState: RecoilState): void => { this.set(recoilState, DEFAULT_VALUE); }; newTreeState_INTERNAL(): TreeState { if (this._changes.size === 0) { return this._treeState; } const newState = copyTreeState(this._treeState); for (const [k, v] of this._changes) { writeLoadableToTreeState(newState, k, loadableWithValue(v)); } invalidateDownstreams(this._store, newState); return newState; } } function atomicUpdater(store: Store): ((TransactionInterface) => void) => void { return fn => { store.replaceState(treeState => { const changeset = new TransactionInterfaceImpl(store, treeState); fn(changeset); return changeset.newTreeState_INTERNAL(); }); }; } module.exports = {atomicUpdater}; ================================================ FILE: packages/recoil/core/Recoil_Batching.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ const {batchStart} = require('../core/Recoil_RecoilValueInterface'); const { unstable_batchedUpdates, } = require('recoil-shared/util/Recoil_ReactBatchedUpdates'); // flowlint-next-line unclear-type:off type Callback = () => any; type Batcher = (callback: Callback) => void; /* * During SSR, unstable_batchedUpdates may be undefined so this * falls back to a basic function that executes the batch */ let batcher: Batcher = unstable_batchedUpdates || (batchFn => batchFn()); /** * Sets the provided batcher function as the batcher function used by Recoil. * * Set the batcher to a custom batcher for your renderer, * if you use a renderer other than React DOM or React Native. */ const setBatcher: Batcher => void = (newBatcher: Batcher) => { batcher = newBatcher; }; /** * Returns the current batcher function. */ const getBatcher: () => Batcher = () => batcher; /** * Calls the current batcher function and passes the * provided callback function. */ const batchUpdates: Callback => void = (callback: Callback) => { batcher(() => { let batchEnd = () => undefined; try { batchEnd = batchStart(); callback(); } finally { batchEnd(); } }); }; module.exports = { getBatcher, setBatcher, batchUpdates, }; ================================================ FILE: packages/recoil/core/Recoil_FunctionalCore.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type {DefaultValue, Trigger} from './Recoil_Node'; import type {RecoilValue} from './Recoil_RecoilValue'; import type {RetainedBy} from './Recoil_RetainedBy'; import type {AtomWrites, NodeKey, Store, TreeState} from './Recoil_State'; const {getNode, getNodeMaybe, recoilValuesForKeys} = require('./Recoil_Node'); const {RetentionZone} = require('./Recoil_RetentionZone'); const {setByAddingToSet} = require('recoil-shared/util/Recoil_CopyOnWrite'); const filterIterable = require('recoil-shared/util/Recoil_filterIterable'); const gkx = require('recoil-shared/util/Recoil_gkx'); const lazyProxy = require('recoil-shared/util/Recoil_lazyProxy'); const mapIterable = require('recoil-shared/util/Recoil_mapIterable'); // flowlint-next-line unclear-type:off const emptySet: $ReadOnlySet = Object.freeze(new Set()); class ReadOnlyRecoilValueError extends Error {} function initializeRetentionForNode( store: Store, nodeKey: NodeKey, retainedBy: RetainedBy, ): () => void { if (!gkx('recoil_memory_managament_2020')) { return () => undefined; } const {nodesRetainedByZone} = store.getState().retention; function addToZone(zone: RetentionZone) { let set = nodesRetainedByZone.get(zone); if (!set) { nodesRetainedByZone.set(zone, (set = new Set())); } set.add(nodeKey); } if (retainedBy instanceof RetentionZone) { addToZone(retainedBy); } else if (Array.isArray(retainedBy)) { for (const zone of retainedBy) { addToZone(zone); } } return () => { if (!gkx('recoil_memory_managament_2020')) { return; } const {retention} = store.getState(); function deleteFromZone(zone: RetentionZone) { const set = retention.nodesRetainedByZone.get(zone); set?.delete(nodeKey); if (set && set.size === 0) { retention.nodesRetainedByZone.delete(zone); } } if (retainedBy instanceof RetentionZone) { deleteFromZone(retainedBy); } else if (Array.isArray(retainedBy)) { for (const zone of retainedBy) { deleteFromZone(zone); } } }; } function initializeNodeIfNewToStore( store: Store, treeState: TreeState, key: NodeKey, trigger: Trigger, ): void { const storeState = store.getState(); if (storeState.nodeCleanupFunctions.has(key)) { return; } const node = getNode(key); const retentionCleanup = initializeRetentionForNode( store, key, node.retainedBy, ); const nodeCleanup = node.init(store, treeState, trigger); storeState.nodeCleanupFunctions.set(key, () => { nodeCleanup(); retentionCleanup(); }); } function initializeNode(store: Store, key: NodeKey, trigger: Trigger): void { initializeNodeIfNewToStore(store, store.getState().currentTree, key, trigger); } function cleanUpNode(store: Store, key: NodeKey) { const state = store.getState(); state.nodeCleanupFunctions.get(key)?.(); state.nodeCleanupFunctions.delete(key); } // Get the current value loadable of a node and update the state. // Update dependencies and subscriptions for selectors. // Update saved value validation for atoms. function getNodeLoadable( store: Store, state: TreeState, key: NodeKey, ): Loadable { initializeNodeIfNewToStore(store, state, key, 'get'); return getNode(key).get(store, state); } // Peek at the current value loadable for a node without any evaluation or state change function peekNodeLoadable( store: Store, state: TreeState, key: NodeKey, ): ?Loadable { return getNode(key).peek(store, state); } // Write value directly to state bypassing the Node interface as the node // definitions may not have been loaded yet when processing the initial snapshot. function setUnvalidatedAtomValue_DEPRECATED( state: TreeState, key: NodeKey, newValue: T, ): TreeState { const node = getNodeMaybe(key); node?.invalidate?.(state); return { ...state, atomValues: state.atomValues.clone().delete(key), nonvalidatedAtoms: state.nonvalidatedAtoms.clone().set(key, newValue), dirtyAtoms: setByAddingToSet(state.dirtyAtoms, key), }; } // Return the discovered dependencies and values to be written by setting // a node value. (Multiple values may be written due to selectors getting to // set upstreams; deps may be discovered because of reads in updater functions.) function setNodeValue( store: Store, state: TreeState, key: NodeKey, newValue: T | DefaultValue, ): AtomWrites { const node = getNode(key); if (node.set == null) { throw new ReadOnlyRecoilValueError( `Attempt to set read-only RecoilValue: ${key}`, ); } const set = node.set; // so flow doesn't lose the above refinement. initializeNodeIfNewToStore(store, state, key, 'set'); return set(store, state, newValue); } type ComponentInfo = { name: string, }; export type RecoilValueInfo = { loadable: ?Loadable, isActive: boolean, isSet: boolean, isModified: boolean, // TODO report modified selectors type: 'atom' | 'selector', deps: Iterable>, subscribers: { nodes: Iterable>, components: Iterable, }, }; function peekNodeInfo( store: Store, state: TreeState, key: NodeKey, ): RecoilValueInfo { const storeState = store.getState(); const graph = store.getGraph(state.version); const type = getNode(key).nodeType; return lazyProxy( { type, }, { // $FlowFixMe[underconstrained-implicit-instantiation] loadable: () => peekNodeLoadable(store, state, key), isActive: () => storeState.knownAtoms.has(key) || storeState.knownSelectors.has(key), isSet: () => (type === 'selector' ? false : state.atomValues.has(key)), isModified: () => state.dirtyAtoms.has(key), // Report current dependencies. If the node hasn't been evaluated, then // dependencies may be missing based on the current state. deps: () => recoilValuesForKeys(graph.nodeDeps.get(key) ?? []), // Reports all "current" subscribers. Evaluating other nodes or // previous in-progress async evaluations may introduce new subscribers. subscribers: () => ({ nodes: recoilValuesForKeys( filterIterable( getDownstreamNodes(store, state, new Set([key])), nodeKey => nodeKey !== key, ), ), components: mapIterable( storeState.nodeToComponentSubscriptions.get(key)?.values() ?? [], ([name]) => ({name}), ), }), }, ); } // Find all of the recursively dependent nodes function getDownstreamNodes( store: Store, state: TreeState, keys: $ReadOnlySet | $ReadOnlyArray, ): $ReadOnlySet { const visitedNodes = new Set(); const visitingNodes = Array.from(keys); const graph = store.getGraph(state.version); for (let key = visitingNodes.pop(); key; key = visitingNodes.pop()) { visitedNodes.add(key); const subscribedNodes = graph.nodeToNodeSubscriptions.get(key) ?? emptySet; for (const downstreamNode of subscribedNodes) { if (!visitedNodes.has(downstreamNode)) { visitingNodes.push(downstreamNode); } } } return visitedNodes; } module.exports = { getNodeLoadable, peekNodeLoadable, setNodeValue, initializeNode, cleanUpNode, setUnvalidatedAtomValue_DEPRECATED, peekNodeInfo, getDownstreamNodes, }; ================================================ FILE: packages/recoil/core/Recoil_Graph.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; import type {Graph} from './Recoil_GraphTypes'; import type {NodeKey, StateID} from './Recoil_Keys'; import type {Store} from './Recoil_State'; const differenceSets = require('recoil-shared/util/Recoil_differenceSets'); const mapMap = require('recoil-shared/util/Recoil_mapMap'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); function makeGraph(): Graph { return { nodeDeps: new Map(), nodeToNodeSubscriptions: new Map(), }; } function cloneGraph(graph: Graph): Graph { return { nodeDeps: mapMap(graph.nodeDeps, s => new Set(s)), nodeToNodeSubscriptions: mapMap( graph.nodeToNodeSubscriptions, s => new Set(s), ), }; } // Note that this overwrites the deps of existing nodes, rather than unioning // the new deps with the old deps. function mergeDepsIntoGraph( key: NodeKey, newDeps: $ReadOnlySet, graph: Graph, // If olderGraph is given then we will not overwrite changes made to the given // graph compared with olderGraph: olderGraph?: Graph, ): void { const {nodeDeps, nodeToNodeSubscriptions} = graph; const oldDeps = nodeDeps.get(key); if (oldDeps && olderGraph && oldDeps !== olderGraph.nodeDeps.get(key)) { return; } // Update nodeDeps: nodeDeps.set(key, newDeps); // Add new deps to nodeToNodeSubscriptions: const addedDeps = oldDeps == null ? newDeps : differenceSets(newDeps, oldDeps); for (const dep of addedDeps) { if (!nodeToNodeSubscriptions.has(dep)) { nodeToNodeSubscriptions.set(dep, new Set()); } const existing = nullthrows(nodeToNodeSubscriptions.get(dep)); existing.add(key); } // Remove removed deps from nodeToNodeSubscriptions: if (oldDeps) { const removedDeps = differenceSets(oldDeps, newDeps); for (const dep of removedDeps) { if (!nodeToNodeSubscriptions.has(dep)) { return; } const existing = nullthrows(nodeToNodeSubscriptions.get(dep)); existing.delete(key); if (existing.size === 0) { nodeToNodeSubscriptions.delete(dep); } } } } function saveDepsToStore( key: NodeKey, deps: $ReadOnlySet, store: Store, version: StateID, ): void { const storeState = store.getState(); if ( !( version === storeState.currentTree.version || version === storeState.nextTree?.version || version === storeState.previousTree?.version ) ) { recoverableViolation( 'Tried to save dependencies to a discarded tree', 'recoil', ); } // Merge the dependencies discovered into the store's dependency map // for the version that was read: const graph = store.getGraph(version); mergeDepsIntoGraph(key, deps, graph); // If this version is not the latest version, also write these dependencies // into later versions if they don't already have their own: if (version === storeState.previousTree?.version) { const currentGraph = store.getGraph(storeState.currentTree.version); mergeDepsIntoGraph(key, deps, currentGraph, graph); } if ( version === storeState.previousTree?.version || version === storeState.currentTree.version ) { const nextVersion = storeState.nextTree?.version; if (nextVersion !== undefined) { const nextGraph = store.getGraph(nextVersion); mergeDepsIntoGraph(key, deps, nextGraph, graph); } } } module.exports = { cloneGraph, graph: makeGraph, saveDepsToStore, }; ================================================ FILE: packages/recoil/core/Recoil_GraphTypes.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; import type {NodeKey} from './Recoil_Keys'; export type Graph = $ReadOnly<{ // TODO rename these properties to be more descriptive and symetric. // Upstream Node dependencies // NOTE: if you ever make the sets in nodeDeps mutable you must change the // logic in mergeDepsIntoGraph() that relies on reference equality // of these sets in avoiding overwriting newer deps with older ones. nodeDeps: Map>, // Downstream Node subscriptions nodeToNodeSubscriptions: Map>, }>; module.exports = ({}: {...}); ================================================ FILE: packages/recoil/core/Recoil_Keys.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; export type NodeKey = string; export opaque type StateID = number; export opaque type StoreID = number; export opaque type ComponentID = number; let nextTreeStateVersion = 0; const getNextTreeStateVersion: () => StateID = () => nextTreeStateVersion++; let nextStoreID = 0; const getNextStoreID: () => StoreID = () => nextStoreID++; let nextComponentID = 0; const getNextComponentID: () => ComponentID = () => nextComponentID++; module.exports = { getNextTreeStateVersion, getNextStoreID, getNextComponentID, }; ================================================ FILE: packages/recoil/core/Recoil_Node.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type {RecoilValue} from './Recoil_RecoilValue'; import type {RetainedBy} from './Recoil_RetainedBy'; import type {AtomWrites, NodeKey, Store, TreeState} from './Recoil_State'; const {isFastRefreshEnabled} = require('./Recoil_ReactMode'); const RecoilValueClasses = require('./Recoil_RecoilValue'); const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation'); const gkx = require('recoil-shared/util/Recoil_gkx'); const mapIterable = require('recoil-shared/util/Recoil_mapIterable'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const RecoilEnv = require('recoil-shared/util/Recoil_RecoilEnv'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); class DefaultValue {} const DEFAULT_VALUE: DefaultValue = new DefaultValue(); export type PersistenceType = 'none' | 'url'; export type PersistenceInfo = $ReadOnly<{ type: PersistenceType, backButton?: boolean, }>; export type Trigger = 'get' | 'set'; type NodeType = 'atom' | 'selector'; export type ReadOnlyNodeOptions = $ReadOnly<{ key: NodeKey, nodeType: NodeType, // Returns the current value without evaluating or modifying state peek: (Store, TreeState) => ?Loadable, // Returns the discovered deps and the loadable value of the node get: (Store, TreeState) => Loadable, // Informs the node the first time it is used (either ever or since the node was // last released). Returns a cleanup function for when the store ceases to be or // the node is released again. init: (Store, TreeState, Trigger) => () => void, // Invalidate the cached value stored in the TreeState. // It is used at the end of each batch for mutated state. // This does not affect any other caches such as the selector cache. invalidate: TreeState => void, // Clear all internal caches for this node. Unlike "invalidate()" this clears // the selector cache and clears for all possible dependency values. clearCache?: (Store, TreeState) => void, shouldRestoreFromSnapshots: boolean, dangerouslyAllowMutability?: boolean, persistence_UNSTABLE?: PersistenceInfo, // True for members of families, since another node can be created later for the // same parameter value; but false for individual atoms and selectors which have // a singleton config passed to us only once when they're defined: shouldDeleteConfigOnRelease?: () => boolean, retainedBy: RetainedBy, }>; export type ReadWriteNodeOptions = $ReadOnly<{ ...ReadOnlyNodeOptions, // Returns the discovered deps and the set of key-value pairs to be written. // (Deps may be discovered since selectors get an updater function which has // the ability to read other atoms, which may have deps.) set: ( store: Store, state: TreeState, newValue: T | DefaultValue, ) => AtomWrites, }>; type Node = ReadOnlyNodeOptions | ReadWriteNodeOptions; // flowlint-next-line unclear-type:off const nodes: Map> = new Map(); // flowlint-next-line unclear-type:off const recoilValues: Map> = new Map(); /* eslint-disable no-redeclare */ declare function registerNode( node: ReadWriteNodeOptions, ): RecoilValueClasses.RecoilState; declare function registerNode( node: ReadOnlyNodeOptions, ): RecoilValueClasses.RecoilValueReadOnly; function recoilValuesForKeys( keys: Iterable, ): Iterable> { return mapIterable(keys, key => nullthrows(recoilValues.get(key))); } function checkForDuplicateAtomKey(key: string): void { if (nodes.has(key)) { const message = `Duplicate atom key "${key}". This is a FATAL ERROR in production. But it is safe to ignore this warning if it occurred because of hot module replacement.`; if (__DEV__) { // TODO Figure this out for open-source if (!isFastRefreshEnabled()) { expectationViolation(message, 'recoil'); } } else { // @fb-only: recoverableViolation(message, 'recoil'); console.warn(message); // @oss-only } } } function registerNode(node: Node): RecoilValue { if (RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED) { checkForDuplicateAtomKey(node.key); } nodes.set(node.key, node); const recoilValue: RecoilValue = node.set == null ? new RecoilValueClasses.RecoilValueReadOnly(node.key) : new RecoilValueClasses.RecoilState(node.key); recoilValues.set(node.key, recoilValue); return recoilValue; } /* eslint-enable no-redeclare */ class NodeMissingError extends Error {} // flowlint-next-line unclear-type:off function getNode(key: NodeKey): Node { const node = nodes.get(key); if (node == null) { throw new NodeMissingError(`Missing definition for RecoilValue: "${key}""`); } return node; } // flowlint-next-line unclear-type:off function getNodeMaybe(key: NodeKey): void | Node { return nodes.get(key); } const configDeletionHandlers = new Map void>(); function deleteNodeConfigIfPossible(key: NodeKey): void { if (!gkx('recoil_memory_managament_2020')) { return; } const node = nodes.get(key); if (node?.shouldDeleteConfigOnRelease?.()) { nodes.delete(key); getConfigDeletionHandler(key)?.(); configDeletionHandlers.delete(key); } } function setConfigDeletionHandler(key: NodeKey, fn: void | (() => void)): void { if (!gkx('recoil_memory_managament_2020')) { return; } if (fn === undefined) { configDeletionHandlers.delete(key); } else { configDeletionHandlers.set(key, fn); } } function getConfigDeletionHandler(key: NodeKey): void | (() => void) { return configDeletionHandlers.get(key); } module.exports = { nodes, recoilValues, registerNode, getNode, getNodeMaybe, deleteNodeConfigIfPossible, setConfigDeletionHandler, getConfigDeletionHandler, recoilValuesForKeys, NodeMissingError, DefaultValue, DEFAULT_VALUE, }; ================================================ FILE: packages/recoil/core/Recoil_ReactMode.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const React = require('react'); const gkx = require('recoil-shared/util/Recoil_gkx'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); // https://github.com/reactwg/react-18/discussions/86 const useSyncExternalStore: ( subscribe: (() => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T, ) => T = // flowlint-next-line unclear-type:off (React: any).useSyncExternalStore ?? // flowlint-next-line unclear-type:off (React: any).unstable_useSyncExternalStore; let ReactRendererVersionMismatchWarnOnce = false; // Check if the current renderer supports `useSyncExternalStore()`. // Since React goes through a proxy dispatcher and the current renderer can // change we can't simply check if `React.useSyncExternalStore()` is defined. function currentRendererSupportsUseSyncExternalStore(): boolean { // $FlowFixMe[incompatible-use] const {ReactCurrentDispatcher, ReactCurrentOwner} = /* $FlowFixMe[prop-missing] This workaround was approved as a safer mechanism * to detect if the current renderer supports useSyncExternalStore() * https://fb.workplace.com/groups/reactjs/posts/9558682330846963/ */ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; const dispatcher = ReactCurrentDispatcher?.current ?? ReactCurrentOwner.currentDispatcher; const isUseSyncExternalStoreSupported = dispatcher.useSyncExternalStore != null; if ( useSyncExternalStore && !isUseSyncExternalStoreSupported && !ReactRendererVersionMismatchWarnOnce ) { ReactRendererVersionMismatchWarnOnce = true; recoverableViolation( 'A React renderer without React 18+ API support is being used with React 18+.', 'recoil', ); } return isUseSyncExternalStoreSupported; } type ReactMode = 'TRANSITION_SUPPORT' | 'SYNC_EXTERNAL_STORE' | 'LEGACY'; /** * mode: The React API and approach to use for syncing state with React * early: Re-renders from Recoil updates occur: * 1) earlier * 2) in sync with React updates in the same batch * 3) before transaction observers instead of after. * concurrent: Is the current mode compatible with Concurrent Mode and useTransition() */ function reactMode(): {mode: ReactMode, early: boolean, concurrent: boolean} { // NOTE: This mode is currently broken with some Suspense cases // see Recoil_selector-test.js if (gkx('recoil_transition_support')) { return {mode: 'TRANSITION_SUPPORT', early: true, concurrent: true}; } if (gkx('recoil_sync_external_store') && useSyncExternalStore != null) { return {mode: 'SYNC_EXTERNAL_STORE', early: true, concurrent: false}; } return gkx('recoil_suppress_rerender_in_callback') ? {mode: 'LEGACY', early: true, concurrent: false} : {mode: 'LEGACY', early: false, concurrent: false}; } // TODO Need to figure out if there is a standard/open-source equivalent to see if hot module replacement is happening: function isFastRefreshEnabled(): boolean { // @fb-only: const {isAcceptingUpdate} = require('__debug'); // @fb-only: return typeof isAcceptingUpdate === 'function' && isAcceptingUpdate(); return false; // @oss-only } module.exports = { useSyncExternalStore, currentRendererSupportsUseSyncExternalStore, reactMode, isFastRefreshEnabled, }; ================================================ FILE: packages/recoil/core/Recoil_RecoilRoot.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {NodeKey, StateID, StoreID} from './Recoil_Keys'; import type {RecoilValue} from './Recoil_RecoilValue'; import type {MutableSnapshot} from './Recoil_Snapshot'; import type {Store, StoreRef, StoreState, TreeState} from './Recoil_State'; // @fb-only: const FBLogger = require('FBLogger'); // @fb-only: const RecoilusagelogEvent = require('RecoilusagelogEvent'); // @fb-only: const RecoilUsageLogFalcoEvent = require('RecoilUsageLogFalcoEvent'); // @fb-only: const URI = require('URI'); const Queue = require('../adt/Recoil_Queue'); const { getNextTreeStateVersion, makeEmptyStoreState, } = require('../core/Recoil_State'); const { cleanUpNode, getDownstreamNodes, initializeNode, setNodeValue, setUnvalidatedAtomValue_DEPRECATED, } = require('./Recoil_FunctionalCore'); const {graph} = require('./Recoil_Graph'); const {cloneGraph} = require('./Recoil_Graph'); const {getNextStoreID} = require('./Recoil_Keys'); const {reactMode} = require('./Recoil_ReactMode'); const {applyAtomValueWrites} = require('./Recoil_RecoilValueInterface'); const {releaseScheduledRetainablesNow} = require('./Recoil_Retention'); const {freshSnapshot} = require('./Recoil_Snapshot'); const React = require('react'); const { Suspense, useCallback, useContext, useEffect, useRef, useState, } = require('react'); const err = require('recoil-shared/util/Recoil_err'); const gkx = require('recoil-shared/util/Recoil_gkx'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); const unionSets = require('recoil-shared/util/Recoil_unionSets'); const useRefInitOnce = require('recoil-shared/util/Recoil_useRefInitOnce'); type InternalProps = { initializeState_DEPRECATED?: ({ set: (RecoilValue, T) => void, setUnvalidatedAtomValues: (Map) => void, }) => void, initializeState?: MutableSnapshot => void, store_INTERNAL?: Store, children: React.Node, skipCircularDependencyDetection_DANGEROUS?: boolean, }; function notInAContext() { throw err('This component must be used inside a component.'); } const defaultStore: Store = Object.freeze({ storeID: getNextStoreID(), getState: notInAContext, replaceState: notInAContext, getGraph: notInAContext, subscribeToTransactions: notInAContext, addTransactionMetadata: notInAContext, }); let stateReplacerIsBeingExecuted: boolean = false; function startNextTreeIfNeeded(store: Store): void { if (stateReplacerIsBeingExecuted) { throw err( 'An atom update was triggered within the execution of a state updater function. State updater functions provided to Recoil must be pure functions.', ); } const storeState = store.getState(); if (storeState.nextTree === null) { if ( gkx('recoil_memory_managament_2020') && gkx('recoil_release_on_cascading_update_killswitch_2021') ) { // If this is a cascading update (that is, rendering due to one state change // invokes a second state change), we won't have cleaned up retainables yet // because this normally happens after notifying components. Do it before // proceeding with the cascading update so that it remains predictable: if (storeState.commitDepth > 0) { releaseScheduledRetainablesNow(store); } } const version = storeState.currentTree.version; const nextVersion = getNextTreeStateVersion(); storeState.nextTree = { ...storeState.currentTree, version: nextVersion, stateID: nextVersion, dirtyAtoms: new Set(), transactionMetadata: {}, }; storeState.graphsByVersion.set( nextVersion, cloneGraph(nullthrows(storeState.graphsByVersion.get(version))), ); } } const AppContext = React.createContext({current: defaultStore}); const useStoreRef = (): StoreRef => useContext(AppContext); function notifyComponents( store: Store, storeState: StoreState, treeState: TreeState, ): void { const dependentNodes = getDownstreamNodes( store, treeState, treeState.dirtyAtoms, ); for (const key of dependentNodes) { const comps = storeState.nodeToComponentSubscriptions.get(key); if (comps) { for (const [_subID, [_debugName, callback]] of comps) { callback(treeState); } } } } function sendEndOfBatchNotifications(store: Store) { const storeState = store.getState(); const treeState = storeState.currentTree; // Inform transaction subscribers of the transaction: const dirtyAtoms = treeState.dirtyAtoms; if (dirtyAtoms.size) { // Execute Node-specific subscribers before global subscribers for (const [ key, subscriptions, ] of storeState.nodeTransactionSubscriptions) { if (dirtyAtoms.has(key)) { for (const [_, subscription] of subscriptions) { subscription(store); } } } for (const [_, subscription] of storeState.transactionSubscriptions) { subscription(store); } if (!reactMode().early || storeState.suspendedComponentResolvers.size > 0) { // Notifying components is needed to wake from suspense, even when using // early rendering. notifyComponents(store, storeState, treeState); // Wake all suspended components so the right one(s) can try to re-render. // We need to wake up components not just when some asynchronous selector // resolved, but also when changing synchronous values because this may cause // a selector to change from asynchronous to synchronous, in which case there // would be no follow-up asynchronous resolution to wake us up. // TODO OPTIMIZATION Only wake up related downstream components storeState.suspendedComponentResolvers.forEach(cb => cb()); storeState.suspendedComponentResolvers.clear(); } } // Special behavior ONLY invoked by useInterface. // FIXME delete queuedComponentCallbacks_DEPRECATED when deleting useInterface. storeState.queuedComponentCallbacks_DEPRECATED.forEach(cb => cb(treeState)); storeState.queuedComponentCallbacks_DEPRECATED.splice( 0, storeState.queuedComponentCallbacks_DEPRECATED.length, ); } function endBatch(store: Store) { const storeState = store.getState(); storeState.commitDepth++; try { const {nextTree} = storeState; // Ignore commits that are not because of Recoil transactions -- namely, // because something above RecoilRoot re-rendered: if (nextTree == null) { return; } // nextTree is now committed -- note that copying and reset occurs when // a transaction begins, in startNextTreeIfNeeded: storeState.previousTree = storeState.currentTree; storeState.currentTree = nextTree; storeState.nextTree = null; sendEndOfBatchNotifications(store); if (storeState.previousTree != null) { storeState.graphsByVersion.delete(storeState.previousTree.version); } else { recoverableViolation( 'Ended batch with no previous state, which is unexpected', 'recoil', ); } storeState.previousTree = null; if (gkx('recoil_memory_managament_2020')) { // Only release retainables if there were no writes during the end of the // batch. This avoids releasing something we might be about to use. if (nextTree == null) { releaseScheduledRetainablesNow(store); } } } finally { storeState.commitDepth--; } } /* * The purpose of the Batcher is to observe when React batches end so that * Recoil state changes can be batched. Whenever Recoil state changes, we call * setState on the batcher. Then we wait for that change to be committed, which * signifies the end of the batch. That's when we respond to the Recoil change. */ function Batcher({ setNotifyBatcherOfChange, }: { setNotifyBatcherOfChange: (() => void) => void, }) { const storeRef = useStoreRef(); const [, setState] = useState(([]: Array<$FlowFixMe>)); // $FlowFixMe[incompatible-call] setNotifyBatcherOfChange(() => setState({})); useEffect(() => { // $FlowFixMe[incompatible-call] setNotifyBatcherOfChange(() => setState({})); // If an asynchronous selector resolves after the Batcher is unmounted, // notifyBatcherOfChange will still be called. An error gets thrown whenever // setState is called after a component is already unmounted, so this sets // notifyBatcherOfChange to be a no-op. return () => { setNotifyBatcherOfChange(() => {}); }; }, [setNotifyBatcherOfChange]); useEffect(() => { // enqueueExecution runs this function immediately; it is only used to // manipulate the order of useEffects during tests, since React seems to // call useEffect in an unpredictable order sometimes. Queue.enqueueExecution('Batcher', () => { endBatch(storeRef.current); }); }); return null; } if (__DEV__) { if (typeof window !== 'undefined' && !window.$recoilDebugStates) { window.$recoilDebugStates = []; } } // When removing this deprecated function, remove stateBySettingRecoilValue // which will no longer be needed. function initialStoreState_DEPRECATED( store: Store, initializeState: ({ set: (RecoilValue, T) => void, setUnvalidatedAtomValues: (Map) => void, }) => void, ): StoreState { const initial: StoreState = makeEmptyStoreState(); initializeState({ set: (atom: RecoilValue, value: T) => { const state = initial.currentTree; const writes = setNodeValue(store, state, atom.key, value); const writtenNodes = new Set(writes.keys()); const nonvalidatedAtoms = state.nonvalidatedAtoms.clone(); for (const n of writtenNodes) { nonvalidatedAtoms.delete(n); } initial.currentTree = { ...state, dirtyAtoms: unionSets(state.dirtyAtoms, writtenNodes), atomValues: applyAtomValueWrites(state.atomValues, writes), // NB: PLEASE un-export applyAtomValueWrites when deleting this code nonvalidatedAtoms, }; }, setUnvalidatedAtomValues: atomValues => { // FIXME replace this with a mutative loop atomValues.forEach((v, k) => { initial.currentTree = setUnvalidatedAtomValue_DEPRECATED( initial.currentTree, k, v, ); }); }, }); return initial; } // Initialize state snapshot for for the initializeState prop. // Atom effect initialization takes precedence over this prop. // Any atom effects will be run before initialization, but then cleaned up, // they are then re-run when used as part of rendering. These semantics are // compatible with React StrictMode where effects may be re-run multiple times // but state initialization only happens once the first time. function initialStoreState( initializeState: MutableSnapshot => void, ): StoreState { // Initialize a snapshot and get its store const snapshot = freshSnapshot(initializeState); const storeState = snapshot.getStore_INTERNAL().getState(); // Counteract the snapshot auto-release snapshot.retain(); // Cleanup any effects run during initialization and clear the handlers so // they will re-initialize if used during rendering. This allows atom effect // initialization to take precedence over initializeState and be compatible // with StrictMode semantics. storeState.nodeCleanupFunctions.forEach(cleanup => cleanup()); storeState.nodeCleanupFunctions.clear(); return storeState; } let warned = false; function RecoilSuspenseWarning() { // prettier-ignore if (!warned) { warned = true; console.warn( // @oss-only // @fb-only: FBLogger('recoil', 'root_suspended').warn( 'Suspended detected. The children of should be wrapped in a boundary since RecoilRoot is not designed to suspend.', ); } return null; } let nextID = 0; function RecoilRoot_INTERNAL({ initializeState_DEPRECATED, initializeState, store_INTERNAL: storeProp, // For use with React "context bridging" children, skipCircularDependencyDetection_DANGEROUS, }: InternalProps): React.Node { // prettier-ignore // @fb-only: useEffect(() => { // @fb-only: if (gkx('recoil_usage_logging')) { // @fb-only: RecoilUsageLogFalcoEvent.log(() => ({ // @fb-only: type: RecoilusagelogEvent.RECOIL_ROOT_MOUNTED, // @fb-only: path: URI.getRequestURI().getPath(), // @fb-only: })); // @fb-only: } // @fb-only: }, []); let storeStateRef: {current: StoreState}; // eslint-disable-line prefer-const const getGraph = (version: StateID) => { const graphs = storeStateRef.current.graphsByVersion; if (graphs.has(version)) { return nullthrows(graphs.get(version)); } const newGraph = graph(); graphs.set(version, newGraph); return newGraph; }; const subscribeToTransactions = ( callback: Store => void, key: ?NodeKey, ): ({release: () => void}) => { if (key == null) { // Global transaction subscriptions const {transactionSubscriptions} = storeRef.current.getState(); const id = nextID++; transactionSubscriptions.set(id, callback); return { release: () => { transactionSubscriptions.delete(id); }, }; } else { // Node-specific transaction subscriptions: const {nodeTransactionSubscriptions} = storeRef.current.getState(); if (!nodeTransactionSubscriptions.has(key)) { nodeTransactionSubscriptions.set(key, new Map()); } const id = nextID++; nullthrows(nodeTransactionSubscriptions.get(key)).set(id, callback); return { release: () => { const subs: ?Map void> = nodeTransactionSubscriptions.get(key); if (subs) { subs.delete(id); if (subs.size === 0) { nodeTransactionSubscriptions.delete(key); } } }, }; } }; const addTransactionMetadata = (metadata: {...}) => { startNextTreeIfNeeded(storeRef.current); for (const k of Object.keys(metadata)) { nullthrows(storeRef.current.getState().nextTree).transactionMetadata[k] = metadata[k]; } }; const replaceState = (replacer: TreeState => TreeState) => { startNextTreeIfNeeded(storeRef.current); // Use replacer to get the next state: const nextTree = nullthrows(storeStateRef.current.nextTree); let replaced; try { stateReplacerIsBeingExecuted = true; replaced = replacer(nextTree); } finally { stateReplacerIsBeingExecuted = false; } if (replaced === nextTree) { return; } if (__DEV__) { if (typeof window !== 'undefined') { window.$recoilDebugStates.push(replaced); // TODO this shouldn't happen here because it's not batched } } // Save changes to nextTree and schedule a React update: storeStateRef.current.nextTree = replaced; if (reactMode().early) { notifyComponents(storeRef.current, storeStateRef.current, replaced); } nullthrows(notifyBatcherOfChange.current)(); }; const notifyBatcherOfChange = useRef void)>(null); const setNotifyBatcherOfChange = useCallback( (x: mixed => void) => { notifyBatcherOfChange.current = x; }, [notifyBatcherOfChange], ); const storeRef: {current: Store} = useRefInitOnce( () => storeProp ?? { storeID: getNextStoreID(), getState: () => storeStateRef.current, replaceState, getGraph, subscribeToTransactions, addTransactionMetadata, skipCircularDependencyDetection_DANGEROUS, }, ); if (storeProp != null) { storeRef.current = storeProp; } storeStateRef = useRefInitOnce(() => initializeState_DEPRECATED != null ? initialStoreState_DEPRECATED( storeRef.current, initializeState_DEPRECATED, ) : initializeState != null ? initialStoreState(initializeState) : makeEmptyStoreState(), ); // Cleanup when the is unmounted useEffect(() => { // React is free to call effect cleanup handlers and effects at will, the // deps array is only an optimization. For example, React strict mode // will execute each effect twice for testing. Therefore, we need symmetry // to re-initialize all known atoms after they were cleaned up. const store = storeRef.current; for (const atomKey of new Set(store.getState().knownAtoms)) { initializeNode(store, atomKey, 'get'); } return () => { for (const atomKey of store.getState().knownAtoms) { cleanUpNode(store, atomKey); } }; }, [storeRef]); return ( }>{children} ); } type Props = | { initializeState_DEPRECATED?: ({ set: (RecoilValue, T) => void, setUnvalidatedAtomValues: (Map) => void, }) => void, initializeState?: MutableSnapshot => void, store_INTERNAL?: Store, override?: true, children: React.Node, skipCircularDependencyDetection_DANGEROUS?: boolean, } | { store_INTERNAL?: Store, /** * Defaults to true. If override is true, this RecoilRoot will create a * new Recoil scope. If override is false and this RecoilRoot is nested * within another RecoilRoot, this RecoilRoot will perform no function. * Children of this RecoilRoot will access the Recoil values of the * nearest ancestor RecoilRoot. */ override: false, children: React.Node, skipCircularDependencyDetection_DANGEROUS?: boolean, }; function RecoilRoot(props: Props): React.Node { const {override, ...propsExceptOverride} = props; const ancestorStoreRef = useStoreRef(); if (override === false && ancestorStoreRef.current !== defaultStore) { // If ancestorStoreRef.current !== defaultStore, it means that this // RecoilRoot is not nested within another. return props.children; } return ; } function useRecoilStoreID(): StoreID { return useStoreRef().current.storeID; } module.exports = { RecoilRoot, useStoreRef, useRecoilStoreID, notifyComponents_FOR_TESTING: notifyComponents, sendEndOfBatchNotifications_FOR_TESTING: sendEndOfBatchNotifications, }; ================================================ FILE: packages/recoil/core/Recoil_RecoilValue.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; import type {NodeKey} from './Recoil_State'; // eslint-disable-next-line no-unused-vars class AbstractRecoilValue<+T> { key: NodeKey; constructor(newKey: NodeKey) { this.key = newKey; } toJSON(): {key: string} { return {key: this.key}; } } class RecoilState extends AbstractRecoilValue {} class RecoilValueReadOnly<+T> extends AbstractRecoilValue {} export type RecoilValue = RecoilValueReadOnly | RecoilState; function isRecoilValue(x: mixed): boolean %checks { return x instanceof RecoilState || x instanceof RecoilValueReadOnly; } module.exports = { AbstractRecoilValue, RecoilState, RecoilValueReadOnly, isRecoilValue, }; ================================================ FILE: packages/recoil/core/Recoil_RecoilValueInterface.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type {ValueOrUpdater} from '../recoil_values/Recoil_callbackTypes'; import type { AtomValues, AtomWrites, NodeKey, Store, TreeState, } from './Recoil_State'; const { getDownstreamNodes, getNodeLoadable, setNodeValue, } = require('./Recoil_FunctionalCore'); const {getNextComponentID} = require('./Recoil_Keys'); const {getNode, getNodeMaybe} = require('./Recoil_Node'); const {DefaultValue} = require('./Recoil_Node'); const {reactMode} = require('./Recoil_ReactMode'); const { AbstractRecoilValue, RecoilState, RecoilValueReadOnly, isRecoilValue, } = require('./Recoil_RecoilValue'); const {invalidateMemoizedSnapshot} = require('./Recoil_SnapshotCache'); const err = require('recoil-shared/util/Recoil_err'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); function getRecoilValueAsLoadable( store: Store, {key}: AbstractRecoilValue, treeState: TreeState = store.getState().currentTree, ): Loadable { // Reading from an older tree can cause bugs because the dependencies that we // discover during the read are lost. const storeState = store.getState(); if ( !( treeState.version === storeState.currentTree.version || treeState.version === storeState.nextTree?.version || treeState.version === storeState.previousTree?.version ) ) { recoverableViolation('Tried to read from a discarded tree', 'recoil'); } const loadable = getNodeLoadable(store, treeState, key); if (loadable.state === 'loading') { loadable.contents.catch(() => { /** * HACK: intercept thrown error here to prevent an uncaught promise exception. Ideally this would happen closer to selector * execution (perhaps introducing a new ERROR class to be resolved by async selectors that are in an error state) */ return; }); } return loadable; } function applyAtomValueWrites( atomValues: AtomValues, writes: AtomWrites, ): AtomValues { const result = atomValues.clone(); writes.forEach((v, k) => { if (v.state === 'hasValue' && v.contents instanceof DefaultValue) { result.delete(k); } else { result.set(k, v); } }); return result; } function valueFromValueOrUpdater( store: Store, state: TreeState, {key}: AbstractRecoilValue, valueOrUpdater: ValueOrUpdater, ): T | DefaultValue { if (typeof valueOrUpdater === 'function') { // Updater form: pass in the current value. Throw if the current value // is unavailable (namely when updating an async selector that's // pending or errored): const current = getNodeLoadable<$FlowFixMe>(store, state, key); if (current.state === 'loading') { const msg = `Tried to set atom or selector "${key}" using an updater function while the current state is pending, this is not currently supported.`; recoverableViolation(msg, 'recoil'); throw err(msg); } else if (current.state === 'hasError') { throw current.contents; } // T itself may be a function, so our refinement is not sufficient: return (valueOrUpdater: any)(current.contents); // flowlint-line unclear-type:off } else { return valueOrUpdater; } } type Action = | { type: 'set', recoilValue: AbstractRecoilValue, valueOrUpdater: T | DefaultValue | (T => T | DefaultValue), } | { type: 'setLoadable', recoilValue: AbstractRecoilValue, loadable: Loadable, } | { type: 'setUnvalidated', recoilValue: AbstractRecoilValue, unvalidatedValue: mixed, } | {type: 'markModified', recoilValue: AbstractRecoilValue}; function applyAction(store: Store, state: TreeState, action: Action) { if (action.type === 'set') { const {recoilValue, valueOrUpdater} = action; const newValue = valueFromValueOrUpdater( store, state, recoilValue, valueOrUpdater, ); const writes = setNodeValue(store, state, recoilValue.key, newValue); for (const [key, loadable] of writes.entries()) { writeLoadableToTreeState(state, key, loadable); } } else if (action.type === 'setLoadable') { const { recoilValue: {key}, loadable, } = action; writeLoadableToTreeState(state, key, loadable); } else if (action.type === 'markModified') { const { recoilValue: {key}, } = action; state.dirtyAtoms.add(key); } else if (action.type === 'setUnvalidated') { // Write value directly to state bypassing the Node interface as the node // definitions may not have been loaded yet when processing the initial snapshot. const { recoilValue: {key}, unvalidatedValue, } = action; const node = getNodeMaybe(key); node?.invalidate?.(state); state.atomValues.delete(key); state.nonvalidatedAtoms.set(key, unvalidatedValue); state.dirtyAtoms.add(key); } else { recoverableViolation(`Unknown action ${action.type}`, 'recoil'); } } function writeLoadableToTreeState( state: TreeState, key: NodeKey, loadable: Loadable, ): void { if ( loadable.state === 'hasValue' && loadable.contents instanceof DefaultValue ) { state.atomValues.delete(key); } else { state.atomValues.set(key, loadable); } state.dirtyAtoms.add(key); state.nonvalidatedAtoms.delete(key); } function applyActionsToStore(store: Store, actions: Array>) { store.replaceState(state => { const newState = copyTreeState(state); for (const action of actions) { applyAction(store, newState, action); } invalidateDownstreams(store, newState); invalidateMemoizedSnapshot(); return newState; }); } function queueOrPerformStateUpdate(store: Store, action: Action): void { if (batchStack.length) { const actionsByStore = batchStack[batchStack.length - 1]; let actions = actionsByStore.get(store); if (!actions) { actionsByStore.set(store, (actions = [])); } actions.push(action); } else { applyActionsToStore(store, [action]); } } const batchStack: Array>>> = []; function batchStart(): () => void { const actionsByStore = new Map>>(); batchStack.push(actionsByStore); return () => { for (const [store, actions] of actionsByStore) { applyActionsToStore(store, actions); } const popped = batchStack.pop(); if (popped !== actionsByStore) { recoverableViolation('Incorrect order of batch popping', 'recoil'); } }; } function copyTreeState(state: TreeState): TreeState { return { ...state, atomValues: state.atomValues.clone(), nonvalidatedAtoms: state.nonvalidatedAtoms.clone(), dirtyAtoms: new Set(state.dirtyAtoms), }; } function invalidateDownstreams(store: Store, state: TreeState): void { // Inform any nodes that were changed or downstream of changes so that they // can clear out any caches as needed due to the update: const downstreams = getDownstreamNodes(store, state, state.dirtyAtoms); for (const key of downstreams) { getNodeMaybe(key)?.invalidate?.(state); } } function setRecoilValue( store: Store, recoilValue: AbstractRecoilValue, valueOrUpdater: T | DefaultValue | (T => T | DefaultValue), ): void { queueOrPerformStateUpdate(store, { type: 'set', recoilValue, valueOrUpdater, }); } function setRecoilValueLoadable( store: Store, recoilValue: AbstractRecoilValue, loadable: DefaultValue | Loadable, ): void { if (loadable instanceof DefaultValue) { return setRecoilValue(store, recoilValue, loadable); } queueOrPerformStateUpdate(store, { type: 'setLoadable', recoilValue, loadable: (loadable: Loadable), }); } function markRecoilValueModified( store: Store, recoilValue: AbstractRecoilValue, ): void { queueOrPerformStateUpdate(store, { type: 'markModified', recoilValue, }); } function setUnvalidatedRecoilValue( store: Store, recoilValue: AbstractRecoilValue, unvalidatedValue: T, ): void { queueOrPerformStateUpdate(store, { type: 'setUnvalidated', recoilValue, unvalidatedValue, }); } export type ComponentSubscription = {release: () => void}; function subscribeToRecoilValue( store: Store, {key}: AbstractRecoilValue, callback: TreeState => void, componentDebugName: ?string = null, ): ComponentSubscription { const subID = getNextComponentID(); const storeState = store.getState(); if (!storeState.nodeToComponentSubscriptions.has(key)) { storeState.nodeToComponentSubscriptions.set(key, new Map()); } nullthrows(storeState.nodeToComponentSubscriptions.get(key)).set(subID, [ componentDebugName ?? '', callback, ]); // Handle the case that, during the same tick that we are subscribing, an atom // has been updated by some effect handler. Otherwise we will miss the update. const mode = reactMode(); if (mode.early && mode.mode === 'LEGACY') { const nextTree = store.getState().nextTree; if (nextTree && nextTree.dirtyAtoms.has(key)) { callback(nextTree); } } return { release: () => { const releaseStoreState = store.getState(); const subs = releaseStoreState.nodeToComponentSubscriptions.get(key); if (subs === undefined || !subs.has(subID)) { recoverableViolation( `Subscription missing at release time for atom ${key}. This is a bug in Recoil.`, 'recoil', ); return; } subs.delete(subID); if (subs.size === 0) { releaseStoreState.nodeToComponentSubscriptions.delete(key); } }, }; } function refreshRecoilValue( store: Store, recoilValue: AbstractRecoilValue, ): void { const {currentTree} = store.getState(); const node = getNode(recoilValue.key); node.clearCache?.(store, currentTree); } module.exports = { RecoilValueReadOnly, AbstractRecoilValue, RecoilState, getRecoilValueAsLoadable, setRecoilValue, setRecoilValueLoadable, markRecoilValueModified, setUnvalidatedRecoilValue, subscribeToRecoilValue, isRecoilValue, applyAtomValueWrites, // TODO Remove export when deprecating initialStoreState_DEPRECATED in RecoilRoot batchStart, writeLoadableToTreeState, invalidateDownstreams, copyTreeState, refreshRecoilValue, }; ================================================ FILE: packages/recoil/core/Recoil_RetainedBy.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; import type {RetentionZone} from './Recoil_RetentionZone'; // This is a separate module to prevent an import cycle. // Options for how an atom can be retained: export type RetainedBy = | 'components' // only retained directly by components | 'recoilRoot' // lives for the lifetime of the root | RetentionZone // retained whenever this zone or these zones are retained | Array; module.exports = undefined; ================================================ FILE: packages/recoil/core/Recoil_Retention.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {NodeKey} from './Recoil_Keys'; import type {RetainedBy} from './Recoil_RetainedBy'; import type {Retainable, Store, StoreState, TreeState} from './Recoil_State'; const {cleanUpNode} = require('./Recoil_FunctionalCore'); const {deleteNodeConfigIfPossible, getNode} = require('./Recoil_Node'); const {RetentionZone} = require('./Recoil_RetentionZone'); const gkx = require('recoil-shared/util/Recoil_gkx'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); const someSet = require('recoil-shared/util/Recoil_someSet'); // Components that aren't mounted after suspending for this long will be assumed // to be discarded and their resources released. const SUSPENSE_TIMEOUT_MS = 120000; const emptySet = new Set(); function releaseRetainablesNowOnCurrentTree( store: Store, retainables: Set, ) { const storeState = store.getState(); const treeState = storeState.currentTree; if (storeState.nextTree) { recoverableViolation( 'releaseNodesNowOnCurrentTree should only be called at the end of a batch', 'recoil', ); return; // leak memory rather than erase something that's about to be used. } const nodes = new Set(); for (const r of retainables) { if (r instanceof RetentionZone) { for (const n of nodesRetainedByZone(storeState, r)) { nodes.add(n); } } else { nodes.add(r); } } const releasableNodes = findReleasableNodes(store, nodes); for (const node of releasableNodes) { releaseNode(store, treeState, node); } } function findReleasableNodes( store: Store, searchFromNodes: Set, ): Set { const storeState = store.getState(); const treeState = storeState.currentTree; const graph = store.getGraph(treeState.version); const releasableNodes: Set = new Set(); // mutated to collect answer const nonReleasableNodes: Set = new Set(); findReleasableNodesInner(searchFromNodes); return releasableNodes; function findReleasableNodesInner(searchFromNodes: Set): void { const releasableNodesFoundThisIteration = new Set(); const downstreams = getDownstreamNodesInTopologicalOrder( store, treeState, searchFromNodes, releasableNodes, // don't descend into these nonReleasableNodes, // don't descend into these ); // Find which of the downstream nodes are releasable and which are not: for (const node of downstreams) { // Not releasable if configured to be retained forever: if (getNode(node).retainedBy === 'recoilRoot') { nonReleasableNodes.add(node); continue; } // Not releasable if retained directly by a component: if ((storeState.retention.referenceCounts.get(node) ?? 0) > 0) { nonReleasableNodes.add(node); continue; } // Not releasable if retained by a zone: if ( zonesThatCouldRetainNode(node).some(z => storeState.retention.referenceCounts.get(z), ) ) { nonReleasableNodes.add(node); continue; } // Not releasable if it has a non-releasable child (which will already be in // nonReleasableNodes because we are going in topological order): const nodeChildren = graph.nodeToNodeSubscriptions.get(node); if ( nodeChildren && someSet(nodeChildren, child => nonReleasableNodes.has(child)) ) { nonReleasableNodes.add(node); continue; } releasableNodes.add(node); releasableNodesFoundThisIteration.add(node); } // If we found any releasable nodes, we need to walk UP from those nodes to // find whether their parents can now be released as well: const parents = new Set(); for (const node of releasableNodesFoundThisIteration) { for (const parent of graph.nodeDeps.get(node) ?? emptySet) { if (!releasableNodes.has(parent)) { parents.add(parent); } } } if (parents.size) { findReleasableNodesInner(parents); } } } // Children before parents function getDownstreamNodesInTopologicalOrder( store: Store, treeState: TreeState, nodes: Set, // Mutable set is destroyed in place doNotDescendInto1: Set, doNotDescendInto2: Set, ): Array { const graph = store.getGraph(treeState.version); const answer = []; const visited = new Set(); while (nodes.size > 0) { visit(nullthrows(nodes.values().next().value)); } return answer; function visit(node: NodeKey): void { if (doNotDescendInto1.has(node) || doNotDescendInto2.has(node)) { nodes.delete(node); return; } if (visited.has(node)) { return; } const children = graph.nodeToNodeSubscriptions.get(node); if (children) { for (const child of children) { visit(child); } } visited.add(node); nodes.delete(node); answer.push(node); } } function releaseNode(store: Store, treeState: TreeState, node: NodeKey) { if (!gkx('recoil_memory_managament_2020')) { return; } // Atom effects, in-closure caches, etc.: cleanUpNode(store, node); // Delete from store state: const storeState = store.getState(); storeState.knownAtoms.delete(node); storeState.knownSelectors.delete(node); storeState.nodeTransactionSubscriptions.delete(node); storeState.retention.referenceCounts.delete(node); const zones = zonesThatCouldRetainNode(node); for (const zone of zones) { storeState.retention.nodesRetainedByZone.get(zone)?.delete(node); } // Note that we DO NOT delete from nodeToComponentSubscriptions because this // already happens when the last component that was retaining the node unmounts, // and this could happen either before or after that. // Delete from TreeState and dep graph: treeState.atomValues.delete(node); treeState.dirtyAtoms.delete(node); treeState.nonvalidatedAtoms.delete(node); const graph = storeState.graphsByVersion.get(treeState.version); if (graph) { const deps = graph.nodeDeps.get(node); if (deps !== undefined) { graph.nodeDeps.delete(node); for (const dep of deps) { graph.nodeToNodeSubscriptions.get(dep)?.delete(node); } } // No need to delete sub's deps as there should be no subs at this point. // But an invariant would require deleting nodes in topological order. graph.nodeToNodeSubscriptions.delete(node); } // Node config (for family members only as their configs can be recreated, and // only if they are not retained within any other Stores): deleteNodeConfigIfPossible(node); } function nodesRetainedByZone( storeState: StoreState, zone: RetentionZone, ): Set { return storeState.retention.nodesRetainedByZone.get(zone) ?? emptySet; } function zonesThatCouldRetainNode(node: NodeKey): Array { const retainedBy = getNode(node).retainedBy; if ( retainedBy === undefined || retainedBy === 'components' || retainedBy === 'recoilRoot' ) { return []; } else if (retainedBy instanceof RetentionZone) { return [retainedBy]; } else { return retainedBy; // it's an array of zones } } function scheduleOrPerformPossibleReleaseOfRetainable( store: Store, retainable: Retainable, ) { const state = store.getState(); if (state.nextTree) { state.retention.retainablesToCheckForRelease.add(retainable); } else { releaseRetainablesNowOnCurrentTree(store, new Set([retainable])); } } function updateRetainCount( store: Store, retainable: Retainable, delta: 1 | -1, ): void { if (!gkx('recoil_memory_managament_2020')) { return; } const map = store.getState().retention.referenceCounts; const newCount = (map.get(retainable) ?? 0) + delta; if (newCount === 0) { updateRetainCountToZero(store, retainable); } else { map.set(retainable, newCount); } } function updateRetainCountToZero(store: Store, retainable: Retainable): void { if (!gkx('recoil_memory_managament_2020')) { return; } const map = store.getState().retention.referenceCounts; map.delete(retainable); scheduleOrPerformPossibleReleaseOfRetainable(store, retainable); } function releaseScheduledRetainablesNow(store: Store) { if (!gkx('recoil_memory_managament_2020')) { return; } const state = store.getState(); releaseRetainablesNowOnCurrentTree( store, state.retention.retainablesToCheckForRelease, ); state.retention.retainablesToCheckForRelease.clear(); } function retainedByOptionWithDefault(r: RetainedBy | void): RetainedBy { // The default will change from 'recoilRoot' to 'components' in the future. return r === undefined ? 'recoilRoot' : r; } module.exports = { SUSPENSE_TIMEOUT_MS, updateRetainCount, updateRetainCountToZero, releaseScheduledRetainablesNow, retainedByOptionWithDefault, }; ================================================ FILE: packages/recoil/core/Recoil_RetentionZone.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; class RetentionZone {} function retentionZone(): RetentionZone { return new RetentionZone(); } module.exports = { RetentionZone, retentionZone, }; ================================================ FILE: packages/recoil/core/Recoil_Snapshot.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type { ResetRecoilState, SetRecoilState, ValueOrUpdater, } from '../recoil_values/Recoil_callbackTypes'; import type {RecoilValueInfo} from './Recoil_FunctionalCore'; import type {Graph} from './Recoil_GraphTypes'; import type {NodeKey, StoreID} from './Recoil_Keys'; import type {RecoilState, RecoilValue} from './Recoil_RecoilValue'; import type {StateID, Store, StoreState, TreeState} from './Recoil_State'; const {batchUpdates} = require('./Recoil_Batching'); const {initializeNode, peekNodeInfo} = require('./Recoil_FunctionalCore'); const {graph} = require('./Recoil_Graph'); const {getNextStoreID} = require('./Recoil_Keys'); const { DEFAULT_VALUE, recoilValues, recoilValuesForKeys, } = require('./Recoil_Node'); const { AbstractRecoilValue, getRecoilValueAsLoadable, setRecoilValue, setUnvalidatedRecoilValue, } = require('./Recoil_RecoilValueInterface'); const {updateRetainCount} = require('./Recoil_Retention'); const {setInvalidateMemoizedSnapshot} = require('./Recoil_SnapshotCache'); const { getNextTreeStateVersion, makeEmptyStoreState, } = require('./Recoil_State'); const concatIterables = require('recoil-shared/util/Recoil_concatIterables'); const {isSSR} = require('recoil-shared/util/Recoil_Environment'); const err = require('recoil-shared/util/Recoil_err'); const filterIterable = require('recoil-shared/util/Recoil_filterIterable'); const gkx = require('recoil-shared/util/Recoil_gkx'); const mapIterable = require('recoil-shared/util/Recoil_mapIterable'); const { memoizeOneWithArgsHashAndInvalidation, } = require('recoil-shared/util/Recoil_Memoize'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); // Opaque at this surface because it's part of the public API from here. export type SnapshotID = StateID; const retainWarning = ` Recoil Snapshots only last for the duration of the callback they are provided to. To keep a Snapshot longer, do this: const release = snapshot.retain(); try { await doSomethingWithSnapshot(snapshot); } finally { release(); } This is currently a DEV-only warning but will become a thrown exception in the next release of Recoil. `; // A "Snapshot" is "read-only" and captures a specific set of values of atoms. // However, the data-flow-graph and selector values may evolve as selector // evaluation functions are executed and async selectors resolve. class Snapshot { // eslint-disable-next-line fb-www/no-uninitialized-properties _store: Store; _refCount: number = 1; constructor(storeState: StoreState, parentStoreID?: StoreID) { this._store = { storeID: getNextStoreID(), parentStoreID, getState: () => storeState, replaceState: replacer => { // no batching, so nextTree is never active storeState.currentTree = replacer(storeState.currentTree); }, getGraph: version => { const graphs = storeState.graphsByVersion; if (graphs.has(version)) { return nullthrows(graphs.get(version)); } const newGraph = graph(); graphs.set(version, newGraph); return newGraph; }, subscribeToTransactions: () => ({release: () => {}}), addTransactionMetadata: () => { throw err('Cannot subscribe to Snapshots'); }, }; // Initialize any nodes that are live in the parent store (primarily so that // this snapshot gets counted towards the node's live stores count). // TODO Optimize this when cloning snapshots for callbacks for (const nodeKey of this._store.getState().knownAtoms) { initializeNode(this._store, nodeKey, 'get'); updateRetainCount(this._store, nodeKey, 1); } this.autoRelease_INTERNAL(); } retain(): () => void { if (this._refCount <= 0) { if (__DEV__) { throw err('Snapshot has already been released.'); } else { recoverableViolation( 'Attempt to retain() Snapshot that was already released.', 'recoil', ); } } this._refCount++; let released = false; return () => { if (!released) { released = true; this._release(); } }; } /** * Release the snapshot on the next tick. This means the snapshot is retained * during the execution of the current function using it. */ autoRelease_INTERNAL(): void { if (!isSSR) { // Use timeout of 10 to workaround Firefox issue: https://github.com/facebookexperimental/Recoil/issues/1936 window.setTimeout(() => this._release(), 10); } } _release(): void { this._refCount--; if (this._refCount === 0) { this._store.getState().nodeCleanupFunctions.forEach(cleanup => cleanup()); this._store.getState().nodeCleanupFunctions.clear(); if (!gkx('recoil_memory_managament_2020')) { return; } // Temporarily nerfing this to allow us to find broken call sites without // actually breaking anybody yet. // for (const k of this._store.getState().knownAtoms) { // updateRetainCountToZero(this._store, k); // } } else if (this._refCount < 0) { if (__DEV__) { recoverableViolation('Snapshot released an extra time.', 'recoil'); } } } isRetained(): boolean { return this._refCount > 0; } checkRefCount_INTERNAL(): void { if (gkx('recoil_memory_managament_2020') && this._refCount <= 0) { if (__DEV__) { recoverableViolation(retainWarning, 'recoil'); } // What we will ship later: // throw err(retainWarning); } } getStore_INTERNAL(): Store { this.checkRefCount_INTERNAL(); return this._store; } getID(): SnapshotID { this.checkRefCount_INTERNAL(); return this._store.getState().currentTree.stateID; } getStoreID(): StoreID { this.checkRefCount_INTERNAL(); return this._store.storeID; } // We want to allow the methods to be destructured and used as accessors /* eslint-disable fb-www/extra-arrow-initializer */ getLoadable: (RecoilValue) => Loadable = ( recoilValue: RecoilValue, ): Loadable => { this.checkRefCount_INTERNAL(); return getRecoilValueAsLoadable(this._store, recoilValue); }; getPromise: (RecoilValue) => Promise = ( recoilValue: RecoilValue, ): Promise => { this.checkRefCount_INTERNAL(); return this.getLoadable(recoilValue).toPromise(); }; getNodes_UNSTABLE: ( { isModified?: boolean, isInitialized?: boolean, } | void, ) => Iterable> = opt => { this.checkRefCount_INTERNAL(); // TODO Deal with modified selectors if (opt?.isModified === true) { if (opt?.isInitialized === false) { return []; } const state = this._store.getState().currentTree; return recoilValuesForKeys(state.dirtyAtoms); } const knownAtoms = this._store.getState().knownAtoms; const knownSelectors = this._store.getState().knownSelectors; return opt?.isInitialized == null ? recoilValues.values() : opt.isInitialized === true ? recoilValuesForKeys(concatIterables([knownAtoms, knownSelectors])) : filterIterable( recoilValues.values(), ({key}) => !knownAtoms.has(key) && !knownSelectors.has(key), ); }; // Report the current status of a node. // This peeks the current state and does not affect the snapshot state at all getInfo_UNSTABLE: (RecoilValue) => RecoilValueInfo = ({ key, }: RecoilValue): RecoilValueInfo => { this.checkRefCount_INTERNAL(); return peekNodeInfo(this._store, this._store.getState().currentTree, key); }; map: ((MutableSnapshot) => void) => Snapshot = mapper => { this.checkRefCount_INTERNAL(); const mutableSnapshot = new MutableSnapshot(this, batchUpdates); mapper(mutableSnapshot); // if removing batchUpdates from `set` add it here return mutableSnapshot; }; asyncMap: ((MutableSnapshot) => Promise) => Promise = async mapper => { this.checkRefCount_INTERNAL(); const mutableSnapshot = new MutableSnapshot(this, batchUpdates); mutableSnapshot.retain(); // Retain new snapshot during async mapper await mapper(mutableSnapshot); // Continue to retain the new snapshot for the user, but auto-release it // after the next tick, the same as a new synchronous snapshot. mutableSnapshot.autoRelease_INTERNAL(); return mutableSnapshot; }; /* eslint-enable fb-www/extra-arrow-initializer */ } function cloneStoreState( store: Store, treeState: TreeState, bumpVersion: boolean = false, ): StoreState { const storeState = store.getState(); const version = bumpVersion ? getNextTreeStateVersion() : treeState.version; return { // Always clone the TreeState to isolate stores from accidental mutations. // For example, reading a selector from a cloned snapshot shouldn't cache // in the original treestate which may cause the original to skip // initialization of upstream atoms. currentTree: { // TODO snapshots shouldn't really have versions because a new version number // is always assigned when the snapshot is gone to. version: bumpVersion ? version : treeState.version, stateID: bumpVersion ? version : treeState.stateID, transactionMetadata: {...treeState.transactionMetadata}, dirtyAtoms: new Set(treeState.dirtyAtoms), atomValues: treeState.atomValues.clone(), nonvalidatedAtoms: treeState.nonvalidatedAtoms.clone(), }, commitDepth: 0, nextTree: null, previousTree: null, knownAtoms: new Set(storeState.knownAtoms), // FIXME here's a copy knownSelectors: new Set(storeState.knownSelectors), // FIXME here's a copy transactionSubscriptions: new Map(), nodeTransactionSubscriptions: new Map(), nodeToComponentSubscriptions: new Map(), queuedComponentCallbacks_DEPRECATED: [], suspendedComponentResolvers: new Set(), graphsByVersion: new Map().set( version, store.getGraph(treeState.version), ), retention: { referenceCounts: new Map(), nodesRetainedByZone: new Map(), retainablesToCheckForRelease: new Set(), }, // FIXME here's a copy // Create blank cleanup handlers for atoms so snapshots don't re-run // atom effects. nodeCleanupFunctions: new Map( mapIterable(storeState.nodeCleanupFunctions.entries(), ([key]) => [ key, () => {}, ]), ), }; } // Factory to build a fresh snapshot function freshSnapshot(initializeState?: MutableSnapshot => void): Snapshot { const snapshot = new Snapshot(makeEmptyStoreState()); return initializeState != null ? snapshot.map(initializeState) : snapshot; } // Factory to clone a snapshot state const [memoizedCloneSnapshot, invalidateMemoizedSnapshot] = memoizeOneWithArgsHashAndInvalidation( // $FlowFixMe[missing-local-annot] (store, version) => { const storeState = store.getState(); const treeState = version === 'latest' ? storeState.nextTree ?? storeState.currentTree : nullthrows(storeState.previousTree); return new Snapshot(cloneStoreState(store, treeState), store.storeID); }, (store, version) => String(version) + String(store.storeID) + String(store.getState().nextTree?.version) + String(store.getState().currentTree.version) + String(store.getState().previousTree?.version), ); // Avoid circular dependencies setInvalidateMemoizedSnapshot(invalidateMemoizedSnapshot); function cloneSnapshot( store: Store, version: 'latest' | 'previous' = 'latest', ): Snapshot { const snapshot = memoizedCloneSnapshot(store, version); if (!snapshot.isRetained()) { invalidateMemoizedSnapshot(); return memoizedCloneSnapshot(store, version); } return snapshot; } class MutableSnapshot extends Snapshot { _batch: (() => void) => void; constructor(snapshot: Snapshot, batch: (() => void) => void) { super( cloneStoreState( snapshot.getStore_INTERNAL(), snapshot.getStore_INTERNAL().getState().currentTree, true, ), snapshot.getStoreID(), ); this._batch = batch; } set: SetRecoilState = ( recoilState: RecoilState, newValueOrUpdater: ValueOrUpdater, ) => { this.checkRefCount_INTERNAL(); const store = this.getStore_INTERNAL(); // This batchUpdates ensures this `set` is applied immediately and you can // read the written value after calling `set`. I would like to remove this // behavior and only batch in `Snapshot.map`, but this would be a breaking // change potentially. this._batch(() => { updateRetainCount(store, recoilState.key, 1); setRecoilValue(this.getStore_INTERNAL(), recoilState, newValueOrUpdater); }); }; reset: ResetRecoilState = (recoilState: RecoilState) => { this.checkRefCount_INTERNAL(); const store = this.getStore_INTERNAL(); // See note at `set` about batched updates. this._batch(() => { updateRetainCount(store, recoilState.key, 1); setRecoilValue(this.getStore_INTERNAL(), recoilState, DEFAULT_VALUE); }); }; setUnvalidatedAtomValues_DEPRECATED: (Map) => void = ( values: Map, ) => { this.checkRefCount_INTERNAL(); const store = this.getStore_INTERNAL(); // See note at `set` about batched updates. batchUpdates(() => { for (const [k, v] of values.entries()) { updateRetainCount(store, k, 1); setUnvalidatedRecoilValue(store, new AbstractRecoilValue(k), v); } }); }; } module.exports = { Snapshot, MutableSnapshot, freshSnapshot, cloneSnapshot, }; ================================================ FILE: packages/recoil/core/Recoil_SnapshotCache.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; let _invalidateMemoizedSnapshot: ?() => void = null; function setInvalidateMemoizedSnapshot(invalidate: () => void): void { _invalidateMemoizedSnapshot = invalidate; } function invalidateMemoizedSnapshot(): void { _invalidateMemoizedSnapshot?.(); } module.exports = { setInvalidateMemoizedSnapshot, invalidateMemoizedSnapshot, }; ================================================ FILE: packages/recoil/core/Recoil_State.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type {PersistentMap} from '../adt/Recoil_PersistentMap'; import type {Graph} from './Recoil_GraphTypes'; import type {ComponentID, NodeKey, StateID, StoreID} from './Recoil_Keys'; import type {RetentionZone} from './Recoil_RetentionZone'; const {persistentMap} = require('../adt/Recoil_PersistentMap'); const {graph} = require('./Recoil_Graph'); const {getNextTreeStateVersion} = require('./Recoil_Keys'); export type {ComponentID, NodeKey, StateID, StoreID} from './Recoil_Keys'; // flowlint-next-line unclear-type:off export type AtomValues = PersistentMap>; // flowlint-next-line unclear-type:off export type AtomWrites = Map>; type ComponentCallback = TreeState => void; export type Retainable = RetentionZone | NodeKey; // TreeState represents the state of a rendered React tree. As such, multiple // TreeStates may be in play at one time due to concurrent rendering, and each // TreeState is immutable. export type TreeState = $ReadOnly<{ // Version always increments when moving from one state to another, even // if the same state has been seen before. version: StateID, // State ID usually increments, but when going to a snapshot that was // previously rendered the state ID will be re-used: stateID: StateID, transactionMetadata: {...}, // Atoms: dirtyAtoms: Set, atomValues: AtomValues, nonvalidatedAtoms: PersistentMap, }>; // StoreState represents the state of a Recoil context. It is global and mutable. // It is updated only during effects, except that the nextTree property is updated // when atom values change and async requests resolve, and suspendedComponentResolvers // is updated when components are suspended. export type StoreState = { // The "current" TreeState being either directly read from (legacy). It is replaced // with nextTree when a transaction is completed or async request finishes: currentTree: TreeState, // The TreeState that is written to when during the course of a transaction // (generally equal to a React batch) when atom values are updated. nextTree: null | TreeState, // This TreeState exists only during the time that components and observers // are being notified of a newly-committed tree: previousTree: null | TreeState, // Incremented when finishing a batch; used to detect cascading updates. commitDepth: number, // Node lifetimes knownAtoms: Set, knownSelectors: Set, +retention: { referenceCounts: Map, nodesRetainedByZone: Map>, retainablesToCheckForRelease: Set, }, // Between the time a component is first used and when it is released, // there will be a function in this map that cleans up the node upon release // (or upon root unmount). +nodeCleanupFunctions: Map void>, // Which components depend on a specific node. (COMMIT/SUSPEND updates). +nodeToComponentSubscriptions: Map< NodeKey, Map, >, // Which nodes depend on which. A pure function of the version (atom state) // and nodeToComponentSubscriptions. Recomputed when: // (1) A transaction occurs (atoms written) or // (2) An async request is completed or // (3) (IN FUTURE) nodeToComponentSubscriptions is updated // How incremental computation is performed: // In case of transactions, we walk downward from the updated atoms // In case of async request completion, we walk downward from updated selector // In (future) case of component subscriptions updated, we walk upwards from // component and then downward from any no-longer-depended on nodes +graphsByVersion: Map, // Side note: it would be useful to consider async request completion as // another type of transaction since it should increase version etc. and many // things have to happen in both of these cases. // For observing transactions: +transactionSubscriptions: Map void>, +nodeTransactionSubscriptions: Map void>>, // Callbacks to render external components that are subscribed to nodes // These are executed at the end of the transaction or asynchronously. // FIXME remove when removing useInterface +queuedComponentCallbacks_DEPRECATED: Array, // Promise resolvers to wake any components we suspended with React Suspense +suspendedComponentResolvers: Set<() => void>, }; // The Store is just the interface that is made available via the context. // It is constant within a given Recoil root. export type Store = $ReadOnly<{ storeID: StoreID, parentStoreID?: StoreID, getState: () => StoreState, replaceState: ((TreeState) => TreeState) => void, getGraph: StateID => Graph, subscribeToTransactions: ((Store) => void, ?NodeKey) => {release: () => void}, addTransactionMetadata: ({...}) => void, skipCircularDependencyDetection_DANGEROUS?: boolean, }>; export type StoreRef = { current: Store, }; function makeEmptyTreeState(): TreeState { const version = getNextTreeStateVersion(); return { version, stateID: version, transactionMetadata: {}, dirtyAtoms: new Set(), atomValues: persistentMap(), nonvalidatedAtoms: persistentMap(), }; } function makeEmptyStoreState(): StoreState { const currentTree = makeEmptyTreeState(); return { currentTree, nextTree: null, previousTree: null, commitDepth: 0, knownAtoms: new Set(), knownSelectors: new Set(), transactionSubscriptions: new Map(), nodeTransactionSubscriptions: new Map(), nodeToComponentSubscriptions: new Map(), queuedComponentCallbacks_DEPRECATED: [], suspendedComponentResolvers: new Set(), graphsByVersion: new Map().set( currentTree.version, graph(), ), retention: { referenceCounts: new Map(), nodesRetainedByZone: new Map(), retainablesToCheckForRelease: new Set(), }, nodeCleanupFunctions: new Map(), }; } module.exports = { makeEmptyTreeState, makeEmptyStoreState, getNextTreeStateVersion, }; ================================================ FILE: packages/recoil/core/__tests__/Recoil_RecoilRoot-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Store} from '../Recoil_State'; import type {MutableSnapshot} from 'Recoil_Snapshot'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useState, act, useSetRecoilState, atom, constSelector, selector, asyncSelector, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, renderUnwrappedElements, RecoilRoot, useStoreRef; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); ({act} = require('ReactTestUtils')); ({useSetRecoilState} = require('../../hooks/Recoil_Hooks')); atom = require('../../recoil_values/Recoil_atom'); constSelector = require('../../recoil_values/Recoil_constSelector'); selector = require('../../recoil_values/Recoil_selector'); ({ asyncSelector, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, renderUnwrappedElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({RecoilRoot, useStoreRef} = require('../Recoil_RecoilRoot')); }); describe('initializeState', () => { testRecoil('initialize atom', () => { const myAtom = atom({ key: 'RecoilRoot - initializeState - atom', default: 'DEFAULT', }); const mySelector = constSelector(myAtom); function initializeState({set, getLoadable}: MutableSnapshot) { expect(getLoadable(myAtom).contents).toEqual('DEFAULT'); expect(getLoadable(mySelector).contents).toEqual('DEFAULT'); set(myAtom, 'INITIALIZE'); expect(getLoadable(myAtom).contents).toEqual('INITIALIZE'); expect(getLoadable(mySelector).contents).toEqual('INITIALIZE'); } const container = renderElements( , ); expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"'); }); testRecoil('initialize selector', () => { const myAtom = atom({ key: 'RecoilRoot - initializeState - selector', default: 'DEFAULT', }); const mySelector = selector({ key: 'RecoilRoot - initializeState - selector selector', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom), // $FlowFixMe[missing-local-annot] set: ({set}, newValue) => set(myAtom, newValue), }); function initializeState({set, getLoadable}: MutableSnapshot) { expect(getLoadable(myAtom).contents).toEqual('DEFAULT'); expect(getLoadable(mySelector).contents).toEqual('DEFAULT'); set(mySelector, 'INITIALIZE'); expect(getLoadable(myAtom).contents).toEqual('INITIALIZE'); expect(getLoadable(mySelector).contents).toEqual('INITIALIZE'); } const container = renderElements( , ); expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"'); }); testRecoil( 'Atom Effects run with global initialization', async ({strictMode, concurrentMode}) => { let effectRan = 0; let effectCleanup = 0; const myAtom = atom({ key: 'RecoilRoot - initializeState - atom effects', default: 'DEFAULT', effects: [ ({setSelf}) => { effectRan++; setSelf('EFFECT'); return () => { effectCleanup++; }; }, ], }); function initializeState({set}: MutableSnapshot) { set(myAtom, current => { // Effects are run first expect(current).toEqual('EFFECT'); return 'INITIALIZE'; }); } expect(effectRan).toEqual(0); const container1 = renderElements( NO READ, ); // Effects are run when initialized with initializeState, even if not read. // Effects are run twice, once before initializeState, then again when rendering. expect(container1.textContent).toEqual('NO READ'); expect(effectRan).toEqual(strictMode ? (concurrentMode ? 4 : 3) : 2); // Auto-release of the initializing snapshot await flushPromisesAndTimers(); expect(effectCleanup).toEqual(strictMode ? (concurrentMode ? 3 : 2) : 1); // Test again when atom is actually used by the root effectRan = 0; effectCleanup = 0; const container2 = renderElements( , ); // Effects takes precedence expect(container2.textContent).toEqual('"EFFECT"'); expect(effectRan).toEqual(strictMode ? (concurrentMode ? 4 : 3) : 2); await flushPromisesAndTimers(); expect(effectCleanup).toEqual(strictMode ? (concurrentMode ? 3 : 2) : 1); }, ); testRecoil( 'onSet() called when atom initialized with initializeState', () => { const setValues = []; const myAtom = atom({ key: 'RecoilRoot - initializeState - onSet', default: 0, effects: [ ({onSet, setSelf}) => { onSet(value => { setValues.push(value); // Confirm setSelf() works when initialized with initializeState setSelf(value + 1); }); }, ], }); const [MyAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements( set(myAtom, 1)}> , ); expect(c.textContent).toBe('1'); expect(setValues).toEqual([]); act(() => setAtom(2)); expect(setValues).toEqual([2]); expect(c.textContent).toBe('3'); }, ); testRecoil( 'Selectors from global initialization are not canceled', async () => { const [asyncSel, resolve] = asyncSelector(); const depSel = selector({ key: 'RecoilRoot - initializeSTate - async selector', // $FlowFixMe[missing-local-annot] get: ({get}) => get(asyncSel), }); const container = renderUnwrappedElements( { getLoadable(asyncSel); getLoadable(depSel); }}> , ); expect(container.textContent).toEqual('loading'); // Wait for any potential auto-release of initializing snapshot await flushPromisesAndTimers(); // Ensure that async selectors resolve and are not canceled act(() => resolve('RESOLVE')); await flushPromisesAndTimers(); expect(container.textContent).toEqual('"RESOLVE""RESOLVE"'); }, ); testRecoil('initialize with nested store', () => { const GetStore = ({children}: {children: Store => React.Node}) => { return children(useStoreRef().current); }; const container = renderElements( {storeA => ( {storeB => { expect(storeA === storeB).toBe(true); return 'NESTED_ROOT/'; }} )} ROOT , ); expect(container.textContent).toEqual('NESTED_ROOT/ROOT'); }); testRecoil('initializeState is only called once', ({strictMode}) => { if (strictMode) { return; } const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const initializeState = jest.fn(({set}) => set(myAtom, 'INIT')); let forceUpdate: () => void = () => { throw new Error('not rendered'); }; let setRootKey: number => void = _ => { throw new Error(''); }; function MyRoot() { const [counter, setCounter] = useState(0); forceUpdate = () => setCounter(counter + 1); const [key, setKey] = useState(0); setRootKey = setKey; return ( {counter} ); } const container = renderElements(); expect(container.textContent).toEqual('0"INIT"'); act(forceUpdate); expect(initializeState).toHaveBeenCalledTimes(1); expect(container.textContent).toEqual('1"INIT"'); act(() => setAtom('SET')); expect(initializeState).toHaveBeenCalledTimes(1); expect(container.textContent).toEqual('1"SET"'); act(forceUpdate); expect(initializeState).toHaveBeenCalledTimes(1); expect(container.textContent).toEqual('2"SET"'); act(() => setRootKey(1)); expect(initializeState).toHaveBeenCalledTimes(2); expect(container.textContent).toEqual('2"INIT"'); }); }); testRecoil( 'Impure state updater functions that trigger atom updates are detected', () => { // This test ensures that we throw a clean error rather than mysterious breakage // if the user supplies a state updater function that triggers another update // within its execution. These state updater functions are supposed to be pure. // We can't detect all forms of impurity but this one in particular will make // Recoil break, so we detect it and throw an error. const atomA = atom({ key: 'RecoilRoot/impureUpdater/a', default: 0, }); const atomB = atom({ key: 'RecoilRoot/impureUpdater/b', default: 0, }); let update; function Component() { const updateA = useSetRecoilState(atomA); const updateB = useSetRecoilState(atomB); update = () => { updateA(() => { updateB(1); return 1; }); }; return null; } renderElements(); expect(() => act(() => { update(); }), ).toThrow('pure function'); }, ); describe('override prop', () => { testRecoil( 'RecoilRoots create a new Recoil scope when override is true or undefined', () => { const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); expect(container.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET')); expect(container.textContent).toEqual('"DEFAULT""SET"'); }, ); testRecoil( 'A RecoilRoot performs no function if override is false and it has an ancestor RecoilRoot', () => { const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); expect(container.textContent).toEqual('"DEFAULT""DEFAULT""DEFAULT"'); act(() => setAtom('SET')); expect(container.textContent).toEqual('"SET""SET""SET"'); }, ); testRecoil( 'Unmounting a nested RecoilRoot with override set to false does not clean up ancestor Recoil atoms', () => { const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); let setRenderNestedRoot; const NestedRootContainer = () => { const [renderNestedRoot, _setRenderNestedRoot] = useState(true); setRenderNestedRoot = _setRenderNestedRoot; return ( renderNestedRoot && ( ) ); }; const container = renderElements( , ); expect(container.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET')); act(() => setRenderNestedRoot(false)); expect(container.textContent).toEqual('"SET"'); }, ); testRecoil( 'A RecoilRoot functions normally if override is false and it does not have an ancestor RecoilRoot', () => { const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); expect(container.textContent).toEqual('"DEFAULT"'); act(() => setAtom('SET')); expect(container.textContent).toEqual('"SET"'); }, ); }); ================================================ FILE: packages/recoil/core/__tests__/Recoil_RecoilValueInterface-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let act, atom, selector, getRecoilValueAsLoadable, setRecoilValue, setUnvalidatedRecoilValue, subscribeToRecoilValue, refreshRecoilValue, a, dependsOnAFn, dependsOnA, dependsOnDependsOnA, b, store; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); ({act} = require('ReactTestUtils')); atom = require('../../recoil_values/Recoil_atom'); selector = require('../../recoil_values/Recoil_selector'); ({ getRecoilValueAsLoadable, setRecoilValue, setUnvalidatedRecoilValue, subscribeToRecoilValue, refreshRecoilValue, } = require('../Recoil_RecoilValueInterface')); a = atom({key: 'a', default: 0}); dependsOnAFn = jest.fn(x => x + 1); dependsOnA = selector({ key: 'dependsOnA', // $FlowFixMe[missing-local-annot] get: ({get}) => dependsOnAFn(get(a)), }); dependsOnDependsOnA = selector({ key: 'dependsOnDependsOnA', // $FlowFixMe[missing-local-annot] get: ({get}) => get(dependsOnA) + 1, }); b = atom({ key: 'b', default: 0, persistence_UNSTABLE: { type: 'url', validator: x => parseInt(x, 10), }, }); store = makeStore(); }); testRecoil('read default value', () => { expect(getRecoilValueAsLoadable(store, a)).toMatchObject({ state: 'hasValue', contents: 0, }); }); testRecoil('read written value, visited contains written value', () => { setRecoilValue(store, a, 1); expect(getRecoilValueAsLoadable(store, a)).toMatchObject({ state: 'hasValue', contents: 1, }); }); testRecoil('read selector based on default upstream', () => { expect(getRecoilValueAsLoadable(store, dependsOnA).contents).toEqual(1); }); testRecoil('read selector based on written upstream', () => { setRecoilValue(store, a, 1); expect(getRecoilValueAsLoadable(store, dependsOnA).contents).toEqual(2); }); testRecoil('selector subscriber is called when upstream changes', () => { const callback = jest.fn(); const {release} = subscribeToRecoilValue(store, dependsOnA, callback); getRecoilValueAsLoadable(store, dependsOnA); expect(callback).toHaveBeenCalledTimes(0); setRecoilValue(store, a, 1); expect(callback).toHaveBeenCalledTimes(1); release(); setRecoilValue(store, a, 2); expect(callback).toHaveBeenCalledTimes(1); }); testRecoil( 'selector is recursively visited when subscribed and upstream changes', () => { const callback = jest.fn(); const {release} = subscribeToRecoilValue( store, dependsOnDependsOnA, callback, ); getRecoilValueAsLoadable(store, dependsOnDependsOnA); expect(callback).toHaveBeenCalledTimes(0); setRecoilValue(store, a, 1); expect(callback).toHaveBeenCalledTimes(1); release(); setRecoilValue(store, a, 2); expect(callback).toHaveBeenCalledTimes(1); }, ); testRecoil('selector function is evaluated only on first read', () => { dependsOnAFn.mockClear(); const callback = jest.fn(); subscribeToRecoilValue(store, dependsOnA, callback); getRecoilValueAsLoadable(store, dependsOnA); expect(dependsOnAFn).toHaveBeenCalledTimes(1); // called once on initial read act(() => setRecoilValue(store, a, 1337)); // input number must not be used in any other test due to selector-internal caching getRecoilValueAsLoadable(store, dependsOnA); expect(dependsOnAFn).toHaveBeenCalledTimes(2); // called again on read following upstream change getRecoilValueAsLoadable(store, dependsOnA); expect(dependsOnAFn).toHaveBeenCalledTimes(2); // not called on subsequent read with no upstream change }); testRecoil('selector cache refresh', () => { const getA = jest.fn(() => 'A'); // $FlowFixMe[incompatible-call] const selectorA = selector({ key: 'useRecoilRefresher ancestors A', get: getA, }); const getB = jest.fn(({get}) => get(selectorA) + 'B'); const selectorB = selector({ key: 'useRecoilRefresher ancestors B', get: getB, }); const getC = jest.fn(({get}) => get(selectorB) + 'C'); const selectorC = selector({ key: 'useRecoilRefresher ancestors C', get: getC, }); expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC'); expect(getC).toHaveBeenCalledTimes(1); expect(getB).toHaveBeenCalledTimes(1); expect(getA).toHaveBeenCalledTimes(1); expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC'); expect(getC).toHaveBeenCalledTimes(1); expect(getB).toHaveBeenCalledTimes(1); expect(getA).toHaveBeenCalledTimes(1); act(() => { refreshRecoilValue(store, selectorC); }); expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC'); expect(getC).toHaveBeenCalledTimes(2); expect(getB).toHaveBeenCalledTimes(2); expect(getA).toHaveBeenCalledTimes(2); }); testRecoil('atom can go from unvalidated to normal value', () => { setUnvalidatedRecoilValue(store, b, '1'); expect(getRecoilValueAsLoadable(store, b)).toMatchObject({ state: 'hasValue', contents: 1, }); setRecoilValue(store, b, 2); expect(getRecoilValueAsLoadable(store, b)).toMatchObject({ state: 'hasValue', contents: 2, }); }); testRecoil('atom can go from normal to unvalidated value', () => { setRecoilValue(store, b, 1); expect(getRecoilValueAsLoadable(store, b)).toMatchObject({ state: 'hasValue', contents: 1, }); setUnvalidatedRecoilValue(store, b, '2'); expect(getRecoilValueAsLoadable(store, b)).toMatchObject({ state: 'hasValue', contents: 2, }); }); testRecoil('atom can go from unvalidated to unvalidated value', () => { // Regression test for an issue where setting an unvalidated value when // already in a has-unvalidated-value state would result in a stale value: setUnvalidatedRecoilValue(store, b, '1'); expect(getRecoilValueAsLoadable(store, b)).toMatchObject({ state: 'hasValue', contents: 1, }); setUnvalidatedRecoilValue(store, b, '2'); expect(getRecoilValueAsLoadable(store, b)).toMatchObject({ state: 'hasValue', contents: 2, }); }); ================================================ FILE: packages/recoil/core/__tests__/Recoil_Retention-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilState} from '../../core/Recoil_RecoilValue'; import type {RetentionZone} from 'Recoil_RetentionZone'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, atom, componentThatReadsAndWritesAtom, gkx, useRecoilValue, useRecoilValueLoadable, useRetain, useRecoilCallback, useState, selector, renderElements, retentionZone; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); ({act} = require('ReactTestUtils')); ({retentionZone} = require('../../core/Recoil_RetentionZone')); ({ useRecoilValue, useRecoilValueLoadable, } = require('../../hooks/Recoil_Hooks')); ({useRecoilCallback} = require('../../hooks/Recoil_useRecoilCallback')); useRetain = require('../../hooks/Recoil_useRetain'); atom = require('../../recoil_values/Recoil_atom'); selector = require('../../recoil_values/Recoil_selector'); ({ componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); gkx = require('recoil-shared/util/Recoil_gkx'); const initialGKValue = gkx('recoil_memory_managament_2020'); gkx.setPass('recoil_memory_managament_2020'); return () => { initialGKValue || gkx.setFail('recoil_memory_managament_2020'); }; }); let nextKey = 0; function atomRetainedBy( retainedBy: | void | RetentionZone | $TEMPORARY$string<'components'> | $TEMPORARY$array, ) { return atom({ key: `retention/${nextKey++}`, default: 0, retainedBy_UNSTABLE: retainedBy, }); } function switchComponent(defaultVisible: boolean) { let innerSetVisible = (_: boolean) => undefined; const setVisible = (v: boolean) => innerSetVisible(v); // acts like a ref basically /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function Switch({children}) { let visible; [visible, innerSetVisible] = useState(defaultVisible); return visible ? children : null; } return [Switch, setVisible]; } // Mounts a component that reads the given atom, sets its value, then unmounts it // and re-mounts it again. Checks whether the value of the atom that was written // is still observed. If otherChildren is provided, it will be mounted throughout this, // then at the end it will be unmounted and the atom expected to be released. function testWhetherAtomIsRetained( shouldBeRetained: boolean, node: RecoilState, /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ otherChildren = null, ): void { const [AtomSwitch, setAtomVisible] = switchComponent(false); const [OtherChildrenSwitch, setOtherChildrenVisible] = switchComponent(false); const [ReadsAtomComp, updateAtom] = componentThatReadsAndWritesAtom(node); const container = renderElements( <> {otherChildren} , ); expect(container.textContent).toEqual(''); act(() => { setAtomVisible(true); setOtherChildrenVisible(true); }); expect(container.textContent).toEqual('0'); act(() => updateAtom(1)); expect(container.textContent).toEqual('1'); act(() => setAtomVisible(false)); expect(container.textContent).toEqual(''); act(() => setAtomVisible(true)); if (shouldBeRetained) { expect(container.textContent).toEqual('1'); } else { expect(container.textContent).toEqual('0'); } if (otherChildren) { act(() => { setAtomVisible(false); setOtherChildrenVisible(false); }); expect(container.textContent).toEqual(''); act(() => setAtomVisible(true)); expect(container.textContent).toEqual('0'); // Not expected for root-retained but this doesn't occur in these tests } } describe('Default retention', () => { testRecoil( 'By default, atoms are retained for the lifetime of the root', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } testWhetherAtomIsRetained(true, atomRetainedBy(undefined)); }, ); }); describe('Component-level retention', () => { testRecoil( 'With retainedBy: components, atoms are released when not in use', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } testWhetherAtomIsRetained(false, atomRetainedBy('components')); }, ); testRecoil( 'An atom is retained by a component being subscribed to it', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } const anAtom = atomRetainedBy('components'); function Subscribes() { useRecoilValue(anAtom); return null; } testWhetherAtomIsRetained(true, anAtom, ); }, ); testRecoil( 'An atom is retained by a component retaining it explicitly', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } const anAtom = atomRetainedBy('components'); function Retains() { useRetain(anAtom); return null; } testWhetherAtomIsRetained(true, anAtom, ); }, ); }); describe('RetentionZone retention', () => { testRecoil('An atom can be retained via a retention zone', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } const zone = retentionZone(); const anAtom = atomRetainedBy(zone); function RetainsZone() { useRetain(zone); return null; } testWhetherAtomIsRetained(true, anAtom, ); }); }); describe('Retention of and via selectors', () => { testRecoil( 'An atom is retained when a depending selector is retained', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } const anAtom = atomRetainedBy('components'); const aSelector = selector({ key: '...', retainedBy_UNSTABLE: 'components', // $FlowFixMe[missing-local-annot] get: ({get}) => { return get(anAtom); }, }); function SubscribesToSelector() { useRecoilValue(aSelector); return null; } testWhetherAtomIsRetained(true, anAtom, ); }, ); const flushPromises = async () => await act(() => new Promise(window.setImmediate)); testRecoil( 'An async selector is not released when its only subscribed component suspends', async ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } let resolve; let evalCount = 0; const anAtom = atomRetainedBy('components'); const aSelector = selector({ key: '......', retainedBy_UNSTABLE: 'components', // $FlowFixMe[missing-local-annot] get: ({get}) => { evalCount++; get(anAtom); return new Promise(r => { resolve = r; }); }, }); function SubscribesToSelector() { // $FlowFixMe[incompatible-type] return useRecoilValue(aSelector); } // $FlowFixMe[incompatible-type-arg] const c = renderElements(); expect(c.textContent).toEqual('loading'); expect(evalCount).toBe(1); act(() => resolve(123)); // We need to let the selector promise resolve but NOT flush timeouts because // we do release after suspending after a timeout and we don't want that // to happen because we're testing what happens when it doesn't. await flushPromises(); await flushPromises(); expect(c.textContent).toEqual('123'); expect(evalCount).toBe(1); // Still in cache, hence wasn't released. }, ); testRecoil( 'An async selector ignores promises that settle after it is released', async ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } let resolve; let evalCount = 0; const anAtom = atomRetainedBy('components'); const aSelector = selector({ key: 'retention/asyncSettlesAfterRelease', retainedBy_UNSTABLE: 'components', // $FlowFixMe[missing-local-annot] get: ({get}) => { evalCount++; get(anAtom); return new Promise(r => { resolve = r; }); }, }); function SubscribesToSelector() { // Test without using Suspense to avoid complications with Jest promises // and timeouts when using Suspense. This doesn't affect what's under test. const l = useRecoilValueLoadable(aSelector); // $FlowFixMe[incompatible-type] return l.state === 'loading' ? 'loading' : l.getValue(); } const [Switch, setMounted] = switchComponent(true); const c = renderElements( {/* $FlowFixMe[incompatible-type-arg] */} , ); expect(c.textContent).toEqual('loading'); expect(evalCount).toBe(1); act(() => setMounted(false)); // release selector while promise is in flight act(() => resolve(123)); await flushPromises(); act(() => setMounted(true)); expect(evalCount).toBe(2); // selector must be re-evaluated because the resolved value is not in cache expect(c.textContent).toEqual('loading'); act(() => resolve(123)); await flushPromises(); expect(c.textContent).toEqual('123'); }, ); testRecoil( 'Selector changing deps releases old deps, retains new ones', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } const switchAtom = atom({ key: 'switch', default: false, }); const depA = atomRetainedBy('components'); const depB = atomRetainedBy('components'); const theSelector = selector({ key: 'sel', // $FlowFixMe[missing-local-annot] get: ({get}) => { if (get(switchAtom)) { return get(depB); } else { return get(depA); } }, retainedBy_UNSTABLE: 'components', }); let setup; function Setup() { setup = useRecoilCallback(({set}) => () => { set(depA, 123); set(depB, 456); }); return null; } function ReadsSelector() { useRecoilValue(theSelector); return null; } let depAValue; function ReadsDepA() { depAValue = useRecoilValue(depA); return null; } let depBValue; function ReadsDepB() { depBValue = useRecoilValue(depB); return null; } const [MountSwitch, setAtomsMountedDirectly] = switchComponent(true); function unmountAndRemount() { act(() => setAtomsMountedDirectly(false)); act(() => setAtomsMountedDirectly(true)); } const [ReadsSwitch, setDepSwitch] = componentThatReadsAndWritesAtom(switchAtom); renderElements( <> , ); act(() => { setup(); }); unmountAndRemount(); expect(depAValue).toBe(123); expect(depBValue).toBe(0); act(() => { setDepSwitch(true); }); unmountAndRemount(); expect(depAValue).toBe(0); act(() => { setup(); }); unmountAndRemount(); expect(depBValue).toBe(456); }, ); }); describe('Retention during a transaction', () => { testRecoil( 'Atoms are not released if unmounted and mounted within the same transaction', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } const anAtom = atomRetainedBy('components'); const [ReaderA, setAtom] = componentThatReadsAndWritesAtom(anAtom); const [ReaderB] = componentThatReadsAndWritesAtom(anAtom); const [SwitchA, setSwitchA] = switchComponent(true); const [SwitchB, setSwitchB] = switchComponent(false); const container = renderElements( <> , ); act(() => setAtom(123)); act(() => { setSwitchA(false); setSwitchB(true); }); expect(container.textContent).toEqual('123'); }, ); testRecoil( 'An atom is released when two zones retaining it are released at the same time', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } const zoneA = retentionZone(); const zoneB = retentionZone(); const anAtom = atomRetainedBy([zoneA, zoneB]); function RetainsZone({zone}: $TEMPORARY$object<{zone: RetentionZone}>) { useRetain(zone); return null; } // It's the no-longer-retained-when-unmounting-otherChildren part that is // important for this test. testWhetherAtomIsRetained( true, anAtom, <> , ); }, ); testRecoil( 'An atom is released when both direct-retainer and zone-retainer are released at the same time', ({strictMode}) => { // TODO Retention does not work properly in strict mode if (strictMode) { return; } const zone = retentionZone(); const anAtom = atomRetainedBy(zone); function RetainsZone() { useRetain(zone); return null; } function RetainsAtom() { useRetain(anAtom); return null; } // It's the no-longer-retained-when-unmounting-otherChildren part that is // important for this test. testWhetherAtomIsRetained( true, anAtom, <> , ); }, ); }); ================================================ FILE: packages/recoil/core/__tests__/Recoil_Snapshot-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Snapshot} from '../Recoil_Snapshot'; import type {StateID} from 'Recoil_Keys'; import type {RecoilState, RecoilValueReadOnly} from 'Recoil_RecoilValue'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, useState, useGotoRecoilSnapshot, useRecoilTransactionObserver, atom, constSelector, selector, ReadsAtom, flushPromisesAndTimers, asyncSelector, componentThatReadsAndWritesAtom, renderElements, freshSnapshot, RecoilRoot; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = React); ({act} = require('ReactTestUtils')); ({ useGotoRecoilSnapshot, useRecoilTransactionObserver, } = require('../../hooks/Recoil_SnapshotHooks')); atom = require('../../recoil_values/Recoil_atom'); constSelector = require('../../recoil_values/Recoil_constSelector'); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, flushPromisesAndTimers, asyncSelector, componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({freshSnapshot} = require('../Recoil_Snapshot')); ({RecoilRoot} = require('../Recoil_RecoilRoot')); }); // Use this to spread proxy results into an object for Jest's toMatchObject() function getInfo( snapshot: Snapshot, node: RecoilState | RecoilValueReadOnly, ) { return {...snapshot.getInfo_UNSTABLE(node)}; } // Test first since we are testing all registered nodes testRecoil('getNodes', () => { const snapshot = freshSnapshot(); const {getNodes_UNSTABLE} = snapshot; expect(Array.from(getNodes_UNSTABLE()).length).toEqual(0); expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual( 0, ); // expect(Array.from(getNodes_UNSTABLE({isSet: true})).length).toEqual(0); // Test atoms const myAtom = atom({key: 'snapshot getNodes atom', default: 'DEFAULT'}); expect(Array.from(getNodes_UNSTABLE()).length).toEqual(1); expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual( 0, ); expect(snapshot.getLoadable(myAtom).contents).toEqual('DEFAULT'); const nodesAfterGet = Array.from(getNodes_UNSTABLE()); expect(nodesAfterGet.length).toEqual(1); expect(nodesAfterGet[0]).toBe(myAtom); expect(snapshot.getLoadable(nodesAfterGet[0]).contents).toEqual('DEFAULT'); // Test selectors const mySelector = selector({ key: 'snapshot getNodes selector', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom) + '-SELECTOR', }); expect(Array.from(getNodes_UNSTABLE()).length).toEqual(2); expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual( 1, ); expect(snapshot.getLoadable(mySelector).contents).toEqual('DEFAULT-SELECTOR'); expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual( 2, ); // expect(Array.from(getNodes_UNSTABLE({types: ['atom']})).length).toEqual(1); // const selectorNodes = Array.from(getNodes_UNSTABLE({types: ['selector']})); // expect(selectorNodes.length).toEqual(1); // expect(selectorNodes[0]).toBe(mySelector); // Test dirty atoms expect(Array.from(getNodes_UNSTABLE()).length).toEqual(2); // expect(Array.from(getNodes_UNSTABLE({isSet: true})).length).toEqual(0); expect( Array.from(snapshot.getNodes_UNSTABLE({isModified: true})).length, ).toEqual(0); const updatedSnapshot = snapshot.map(({set}) => set(myAtom, 'SET')); expect( Array.from(snapshot.getNodes_UNSTABLE({isModified: true})).length, ).toEqual(0); expect( Array.from(updatedSnapshot.getNodes_UNSTABLE({isModified: true})).length, ).toEqual(1); // expect( // Array.from(snapshot.getNodes_UNSTABLE({isSet: true})).length, // ).toEqual(0); // expect( // Array.from(updatedSnapshot.getNodes_UNSTABLE({isSet: true})).length, // ).toEqual(1); const dirtyAtom = Array.from( updatedSnapshot.getNodes_UNSTABLE({isModified: true}), )[0]; expect(snapshot.getLoadable(dirtyAtom).contents).toEqual('DEFAULT'); expect(updatedSnapshot.getLoadable(dirtyAtom).contents).toEqual('SET'); // Test reset const resetSnapshot = updatedSnapshot.map(({reset}) => reset(myAtom)); expect( Array.from(resetSnapshot.getNodes_UNSTABLE({isModified: true})).length, ).toEqual(1); // expect( // Array.from(resetSnapshot.getNodes_UNSTABLE({isSet: true})).length, // ).toEqual(0); // TODO Test dirty selectors }); testRecoil( 'State ID after going to snapshot matches the ID of the snapshot', () => { const seenIDs = new Set(); const snapshots = []; let expectedSnapshotID = null; const myAtom = atom({key: 'Snapshot ID atom', default: 0}); const mySelector = constSelector(myAtom); // For read-only testing below const transactionObserver = ({ snapshot, }: { previousSnapshot: Snapshot, snapshot: Snapshot, }) => { const snapshotID = snapshot.getID(); if (expectedSnapshotID != null) { expect(seenIDs.has(snapshotID)).toBe(true); expect(snapshotID).toBe(expectedSnapshotID); } else { expect(seenIDs.has(snapshotID)).toBe(false); } seenIDs.add(snapshotID); snapshot.retain(); snapshots.push({snapshotID, snapshot}); }; function TransactionObserver() { useRecoilTransactionObserver(transactionObserver); return null; } let gotoSnapshot; function GotoSnapshot() { gotoSnapshot = useGotoRecoilSnapshot(); return null; } const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements( <> , ); expect(c.textContent).toBe('00'); // Test changing state produces a new state version act(() => setAtom(1)); act(() => setAtom(2)); expect(snapshots.length).toBe(2); expect(seenIDs.size).toBe(2); // Test going to a previous snapshot re-uses the state ID expectedSnapshotID = snapshots[0].snapshotID; act(() => gotoSnapshot(snapshots[0].snapshot)); // Test changing state after going to a previous snapshot uses a new version expectedSnapshotID = null; act(() => setAtom(3)); // Test mutating a snapshot creates a new version const transactionSnapshot = snapshots[0].snapshot.map(({set}) => { set(myAtom, 4); }); act(() => gotoSnapshot(transactionSnapshot)); expect(seenIDs.size).toBe(4); expect(snapshots.length).toBe(5); // Test that added read-only selector doesn't cause an issue getting the // current version to see the current deps of the selector since we mutated a // state after going to a snapshot, so that version may not be known by the store. // If there was a problem, then the component may throw an error when evaluating the selector. expect(c.textContent).toBe('44'); }, ); testRecoil('Read default loadable from snapshot', () => { const snapshot: Snapshot = freshSnapshot(); const myAtom = atom({ key: 'Snapshot Atom Default', default: 'DEFAULT', }); const atomLoadable = snapshot.getLoadable(myAtom); expect(atomLoadable.state).toEqual('hasValue'); expect(atomLoadable.contents).toEqual('DEFAULT'); const mySelector = constSelector(myAtom); const selectorLoadable = snapshot.getLoadable(mySelector); expect(selectorLoadable.state).toEqual('hasValue'); expect(selectorLoadable.contents).toEqual('DEFAULT'); }); testRecoil('Read async selector from snapshot', async () => { const snapshot = freshSnapshot(); const otherA = freshSnapshot(); const otherB = freshSnapshot(); const [asyncSel, resolve] = asyncSelector(); const nestSel = constSelector(asyncSel); expect(snapshot.getLoadable(asyncSel).state).toEqual('loading'); expect(snapshot.getLoadable(nestSel).state).toEqual('loading'); expect(otherA.getLoadable(nestSel).state).toEqual('loading'); const otherC = snapshot.map(() => {}); // eslint-disable-next-line jest/valid-expect const ptest = expect(snapshot.getPromise(asyncSel)).resolves.toEqual( 'SET VALUE', ); act(() => resolve('SET VALUE')); await ptest; await expect(snapshot.getPromise(asyncSel)).resolves.toEqual('SET VALUE'); expect(snapshot.getLoadable(asyncSel).contents).toEqual('SET VALUE'); await expect(snapshot.getPromise(nestSel)).resolves.toEqual('SET VALUE'); await expect(otherA.getPromise(nestSel)).resolves.toEqual('SET VALUE'); await expect(otherB.getPromise(nestSel)).resolves.toEqual('SET VALUE'); await expect(otherC.getPromise(nestSel)).resolves.toEqual('SET VALUE'); }); testRecoil('Sync map of snapshot', () => { const snapshot = freshSnapshot(); const myAtom = atom({ key: 'Snapshot Map Sync', default: 'DEFAULT', }); const mySelector = constSelector(myAtom); const atomLoadable = snapshot.getLoadable(myAtom); expect(atomLoadable.state).toEqual('hasValue'); expect(atomLoadable.contents).toEqual('DEFAULT'); const selectorLoadable = snapshot.getLoadable(mySelector); expect(selectorLoadable.state).toEqual('hasValue'); expect(selectorLoadable.contents).toEqual('DEFAULT'); const setSnapshot = snapshot.map(({set}) => { set(myAtom, 'SET'); }); const setAtomLoadable = setSnapshot.getLoadable(myAtom); expect(setAtomLoadable.state).toEqual('hasValue'); expect(setAtomLoadable.contents).toEqual('SET'); const setSelectorLoadable = setSnapshot.getLoadable(myAtom); expect(setSelectorLoadable.state).toEqual('hasValue'); expect(setSelectorLoadable.contents).toEqual('SET'); const resetSnapshot = snapshot.map(({reset}) => { reset(myAtom); }); const resetAtomLoadable = resetSnapshot.getLoadable(myAtom); expect(resetAtomLoadable.state).toEqual('hasValue'); expect(resetAtomLoadable.contents).toEqual('DEFAULT'); const resetSelectorLoadable = resetSnapshot.getLoadable(myAtom); expect(resetSelectorLoadable.state).toEqual('hasValue'); expect(resetSelectorLoadable.contents).toEqual('DEFAULT'); }); testRecoil('Async map of snapshot', async () => { const snapshot = freshSnapshot(); const myAtom = atom({ key: 'Snapshot Map Async', default: 'DEFAULT', }); const [beforeAsyncSel, resolveBeforeMap] = asyncSelector(); const [duringAsyncSel, resolveDuringMap] = asyncSelector(); const [afterAsyncSel, resolveAfterMap] = asyncSelector(); const depAsyncSel = selector({ key: 'snapshot asyncMap dep selector', get: () => afterAsyncSel, }); resolveBeforeMap('BEFORE'); const newSnapshotPromise = snapshot.asyncMap(async ({getPromise, set}) => { await expect(getPromise(beforeAsyncSel)).resolves.toBe('BEFORE'); await expect(getPromise(duringAsyncSel)).resolves.toBe('DURING'); // Test that depAsyncSel is first used while mapping the snapshot. // If the snapshot is auto-released too early the async selector will be // canceled. // $FlowFixMe[unused-promise] getPromise(depAsyncSel); // Test that mapped snapshot is not auto-released too early await flushPromisesAndTimers(); set(myAtom, 'VALUE'); }); resolveDuringMap('DURING'); const newSnapshot = await newSnapshotPromise; expect(newSnapshot.isRetained()).toBe(true); resolveAfterMap('AFTER'); await expect(newSnapshot.getPromise(myAtom)).resolves.toBe('VALUE'); await expect(newSnapshot.getPromise(beforeAsyncSel)).resolves.toBe('BEFORE'); await expect(newSnapshot.getPromise(duringAsyncSel)).resolves.toBe('DURING'); await expect(newSnapshot.getPromise(afterAsyncSel)).resolves.toBe('AFTER'); await expect(newSnapshot.getPromise(depAsyncSel)).resolves.toBe('AFTER'); }); testRecoil('getInfo', () => { const snapshot = freshSnapshot(); const myAtom = atom({ key: 'snapshot getInfo atom', default: 'DEFAULT', }); const selectorA = selector({ key: 'getInfo A', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom), }); const selectorB = selector({ key: 'getInfo B', // $FlowFixMe[missing-local-annot] get: ({get}) => get(selectorA) + get(myAtom), }); // Initial status expect(getInfo(snapshot, myAtom)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), isActive: false, isSet: false, isModified: false, type: 'atom', }); expect(Array.from(getInfo(snapshot, myAtom).deps)).toEqual([]); expect(Array.from(getInfo(snapshot, myAtom).subscribers.nodes)).toEqual([]); expect(getInfo(snapshot, selectorA)).toMatchObject({ loadable: undefined, isActive: false, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getInfo(snapshot, selectorA).deps)).toEqual([]); expect(Array.from(getInfo(snapshot, selectorA).subscribers.nodes)).toEqual( [], ); expect(getInfo(snapshot, selectorB)).toMatchObject({ loadable: undefined, isActive: false, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getInfo(snapshot, selectorB).deps)).toEqual([]); expect(Array.from(getInfo(snapshot, selectorB).subscribers.nodes)).toEqual( [], ); // After reading values snapshot.getLoadable(selectorB); expect(getInfo(snapshot, myAtom)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), isActive: true, isSet: false, isModified: false, type: 'atom', }); expect(Array.from(getInfo(snapshot, myAtom).deps)).toEqual([]); expect(Array.from(getInfo(snapshot, myAtom).subscribers.nodes)).toEqual( expect.arrayContaining([selectorA, selectorB]), ); expect(getInfo(snapshot, selectorA)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getInfo(snapshot, selectorA).deps)).toEqual( expect.arrayContaining([myAtom]), ); expect(Array.from(getInfo(snapshot, selectorA).subscribers.nodes)).toEqual( expect.arrayContaining([selectorB]), ); expect(getInfo(snapshot, selectorB)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULTDEFAULT', }), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getInfo(snapshot, selectorB).deps)).toEqual( expect.arrayContaining([myAtom, selectorA]), ); expect(Array.from(getInfo(snapshot, selectorB).subscribers.nodes)).toEqual( [], ); // After setting a value const setSnapshot = snapshot.map(({set}) => set(myAtom, 'SET')); setSnapshot.getLoadable(selectorB); // Read value to prime expect(getInfo(setSnapshot, myAtom)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}), isActive: true, isSet: true, isModified: true, type: 'atom', }); expect(Array.from(getInfo(setSnapshot, myAtom).deps)).toEqual([]); expect(Array.from(getInfo(setSnapshot, myAtom).subscribers.nodes)).toEqual( expect.arrayContaining([selectorA, selectorB]), ); expect(getInfo(setSnapshot, selectorA)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getInfo(setSnapshot, selectorA).deps)).toEqual( expect.arrayContaining([myAtom]), ); expect(Array.from(getInfo(setSnapshot, selectorA).subscribers.nodes)).toEqual( expect.arrayContaining([selectorB]), ); expect(getInfo(setSnapshot, selectorB)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'SETSET'}), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getInfo(setSnapshot, selectorB).deps)).toEqual( expect.arrayContaining([myAtom, selectorA]), ); expect(Array.from(getInfo(setSnapshot, selectorB).subscribers.nodes)).toEqual( [], ); // After reseting a value const resetSnapshot = setSnapshot.map(({reset}) => reset(myAtom)); resetSnapshot.getLoadable(selectorB); // prime snapshot expect(getInfo(resetSnapshot, myAtom)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), isActive: true, isSet: false, isModified: true, type: 'atom', }); expect(Array.from(getInfo(resetSnapshot, myAtom).deps)).toEqual([]); expect(Array.from(getInfo(resetSnapshot, myAtom).subscribers.nodes)).toEqual( expect.arrayContaining([selectorA, selectorB]), ); expect(getInfo(resetSnapshot, selectorA)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getInfo(resetSnapshot, selectorA).deps)).toEqual( expect.arrayContaining([myAtom]), ); expect( Array.from(getInfo(resetSnapshot, selectorA).subscribers.nodes), ).toEqual(expect.arrayContaining([selectorB])); expect(getInfo(resetSnapshot, selectorB)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULTDEFAULT', }), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getInfo(resetSnapshot, selectorB).deps)).toEqual( expect.arrayContaining([myAtom, selectorA]), ); expect( Array.from(getInfo(resetSnapshot, selectorB).subscribers.nodes), ).toEqual([]); }); describe('Retention', () => { testRecoil('auto-release', async () => { const snapshot = freshSnapshot(); expect(snapshot.isRetained()).toBe(true); await flushPromisesAndTimers(); expect(snapshot.isRetained()).toBe(false); const devStatus = window.__DEV__; window.__DEV__ = true; expect(() => snapshot.retain()).toThrow('released'); window.__DEV__ = false; expect(() => snapshot.retain()).not.toThrow('released'); window.__DEV__ = devStatus; // TODO enable when recoil_memory_managament_2020 is enforced // expect(() => snapshot.getID()).toThrow('release'); }); testRecoil('retain()', async () => { const snapshot = freshSnapshot(); expect(snapshot.isRetained()).toBe(true); const release2 = snapshot.retain(); await flushPromisesAndTimers(); expect(snapshot.isRetained()).toBe(true); release2(); expect(snapshot.isRetained()).toBe(false); }); }); describe('Atom effects', () => { testRecoil('Standalone snapshot', async ({gks}) => { let effectsRefCount = 0; const myAtom = atom({ key: 'snapshot effects standalone', default: 'DEFAULT', effects: [ ({setSelf}) => { effectsRefCount++; setSelf('INIT'); return () => { effectsRefCount--; }; }, ], }); expect(effectsRefCount).toBe(0); const fresh = freshSnapshot(); expect(fresh.getLoadable(myAtom).getValue()).toBe('INIT'); expect(effectsRefCount).toBe(1); // Auto-release snapshot await flushPromisesAndTimers(); expect(effectsRefCount).toBe(0); }); testRecoil('RecoilRoot Snapshot', () => { let effectsRefCount = 0; const myAtom = atom({ key: 'snapshot effects RecoilRoot', default: 'DEFAULT', effects: [ ({setSelf}) => { effectsRefCount++; setSelf('INIT'); return () => { effectsRefCount--; }; }, ], }); let setMount: boolean => void = () => { throw new Error('Test Error'); }; function Component() { const [mount, setState] = useState(false); setMount = setState; return mount ? ( ) : ( 'UNMOUNTED' ); } const c = renderElements(); expect(c.textContent).toBe('UNMOUNTED'); expect(effectsRefCount).toBe(0); act(() => setMount(true)); expect(c.textContent).toBe('"INIT"'); expect(effectsRefCount).toBe(1); act(() => setMount(false)); expect(c.textContent).toBe('UNMOUNTED'); expect(effectsRefCount).toBe(0); }); testRecoil('getStoreID()', () => { const myAtom = atom({ key: 'snapshot effects storeID', default: 'DEFAULT', effects: [ ({setSelf, storeID}) => { // $FlowFixMe[incompatible-call] setSelf(storeID); }, ], }); const testSnapshot = freshSnapshot(); expect(testSnapshot.getLoadable(myAtom).getValue()).toBe( testSnapshot.getStoreID(), ); }); testRecoil('Parent StoreID', () => { const myAtom = atom({ key: 'snapshot effects parentStoreID', effects: [ // $FlowFixMe[missing-local-annot] ({storeID, parentStoreID_UNSTABLE, setSelf}) => { setSelf({storeID, parentStoreID: parentStoreID_UNSTABLE}); }, ], }); const testSnapshot = freshSnapshot(); const mappedSnapshot = testSnapshot.map(() => {}); expect(mappedSnapshot.getLoadable(myAtom).getValue().storeID).toBe( mappedSnapshot.getStoreID(), ); expect(mappedSnapshot.getLoadable(myAtom).getValue().parentStoreID).toBe( testSnapshot.getStoreID(), ); }); }); ================================================ FILE: packages/recoil/core/__tests__/Recoil_batcher-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let unstable_batchedUpdates, batchUpdates, getBatcher, setBatcher; const testRecoil = getRecoilTestFn(() => { ({unstable_batchedUpdates} = require('ReactDOMLegacy_DEPRECATED')); ({batchUpdates, getBatcher, setBatcher} = require('../Recoil_Batching')); }); /** * Cleanup function that will reset the batcher back * to ReactDOM's resetBatcherToDefault. * * Call this at the end of a test that calls setBatcher * to maintain test purity. */ const resetBatcherToDefault = () => { setBatcher(unstable_batchedUpdates); }; describe('batcher', () => { testRecoil('default batcher is ReactDOM unstable_batchedUpdates', () => { expect(getBatcher()).toEqual(unstable_batchedUpdates); }); testRecoil('setBatcher sets the batcher function', () => { const batcherFn = jest.fn(); setBatcher(batcherFn); expect(getBatcher()).toEqual(batcherFn); resetBatcherToDefault(); }); testRecoil('batchUpdates calls the batcher', () => { const batcherFn = jest.fn(); setBatcher(batcherFn); batchUpdates(() => {}); expect(batcherFn).toHaveBeenCalledTimes(1); resetBatcherToDefault(); }); }); ================================================ FILE: packages/recoil/core/__tests__/Recoil_core-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let a, atom, store, nullthrows, getNodeLoadable, setNodeValue; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); atom = require('../../recoil_values/Recoil_atom'); nullthrows = require('recoil-shared/util/Recoil_nullthrows'); ({getNodeLoadable, setNodeValue} = require('../Recoil_FunctionalCore')); a = atom({key: 'a', default: 0}).key; store = makeStore(); }); testRecoil('read default value', () => { expect(getNodeLoadable(store, store.getState().currentTree, a)).toMatchObject( { state: 'hasValue', contents: 0, }, ); }); testRecoil('setNodeValue returns written value when writing atom', () => { const writes = setNodeValue(store, store.getState().currentTree, a, 1); expect(nullthrows(writes.get(a)).contents).toBe(1); }); ================================================ FILE: packages/recoil/core/__tests__/Recoil_perf-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall obviz */ 'use strict'; import type {Loadable, RecoilState, RecoilValue} from '../../Recoil_index'; const {atom, selector, selectorFamily} = require('../../Recoil_index'); const {waitForAll} = require('../../recoil_values/Recoil_WaitFor'); const { getRecoilValueAsLoadable, setRecoilValue, } = require('../Recoil_RecoilValueInterface'); const {performance} = require('perf_hooks'); const {makeStore} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const ITERATIONS = [1]; // Avoid iterating for automated testing // const ITERATIONS = [100]; // const ITERATIONS = [1000]; // const ITERATIONS = [10, 100, 1000]; // const ITERATIONS = [10, 100, 1000, 10000]; // const ITERATIONS = [10, 100, 1000, 10000, 100000]; function testPerf( name: string, fn: ({iterations: number, perf: (() => void) => void}) => void, ) { test.each(ITERATIONS)(name, iterations => { store = makeStore(); const perf = (cb: () => void) => { const BEGIN = performance.now(); cb(); const END = performance.now(); console.log(`${name}(${iterations})`, END - BEGIN); }; fn({iterations, perf}); }); } let store = makeStore(); function getNodeLoadable(recoilValue: RecoilValue): Loadable { return getRecoilValueAsLoadable(store, recoilValue); } function getNodeValue(recoilValue: RecoilValue): T { return getNodeLoadable(recoilValue).getValue(); } function setNode(recoilValue: RecoilState, value: mixed) { setRecoilValue(store, recoilValue, value); // $FlowFixMe[cannot-write] // $FlowFixMe[unsafe-arithmetic] store.getState().currentTree.version++; } let nextAtomKey = 0; function createAtoms(num: number): Array> { const atoms = Array(num); const atomKey = nextAtomKey++; for (let i = 0; i < num; i++) { atoms[i] = atom({ key: `PERF-${atomKey}-${i}`, default: 'DEFAULT', }); } return atoms; } const helpersSelector = () => selector({ key: `PERF-helpers-${nextAtomKey++}`, // $FlowFixMe[missing-local-annot] get: ({getCallback}) => ({ getSnapshot: getCallback( ({snapshot}) => () => snapshot, ), }), }); const getHelpers = () => getNodeValue(helpersSelector()); testPerf('create n atoms', ({iterations}) => { createAtoms(iterations); }); testPerf('get n atoms', ({iterations, perf}) => { const atoms = createAtoms(iterations); perf(() => { for (const node of atoms) { getNodeValue(node); } }); }); testPerf('set n atoms', ({iterations, perf}) => { const atoms = createAtoms(iterations); perf(() => { for (const node of atoms) { setNode(node, 'SET'); } }); }); testPerf('get n selectors', ({iterations, perf}) => { const atoms = createAtoms(iterations); const testFamily = selectorFamily({ key: 'PERF-getselectors', get: (id: number) => // $FlowFixMe[missing-local-annot] ({get}) => get(atoms[id]) + get(atoms[0]), }); perf(() => { for (let i = 0; i < iterations; i++) { getNodeValue(testFamily(i)); } }); }); testPerf('clone n snapshots', ({iterations, perf}) => { const atoms = createAtoms(iterations); const {getSnapshot} = getHelpers(); perf(() => { for (const node of atoms) { // Set node to avoid hitting cached snapshots setNode(node, 'SET'); const snapshot = getSnapshot(); expect(getNodeValue(node)).toBe('SET'); expect(snapshot.getLoadable(node).contents).toBe('SET'); } }); }); testPerf('get 1 selector with n dependencies', ({iterations, perf}) => { const atoms = createAtoms(iterations); perf(() => { getNodeValue(waitForAll(atoms)); }); }); testPerf('get 1 selector with n dependencies n times', ({iterations, perf}) => { const atoms = createAtoms(iterations); perf(() => { for (let i = 0; i < iterations; i++) { getNodeValue(waitForAll(atoms)); } }); }); testPerf('get n selectors n times', ({iterations, perf}) => { const atoms = createAtoms(iterations); const testFamily = selectorFamily({ key: 'PERF-getselectors', get: (id: number) => // $FlowFixMe[missing-local-annot] ({get}) => get(atoms[id]) + get(atoms[0]), }); perf(() => { for (let i = 0; i < iterations; i++) { for (let j = 0; j < iterations; j++) { getNodeValue(testFamily(i)); } } }); }); testPerf( 'get n selectors with n dependencies n times', ({iterations, perf}) => { const atoms = createAtoms(iterations); const testFamily = selectorFamily<_, number>({ key: 'PERF-getselectors', get: () => () => waitForAll(atoms), }); perf(() => { for (let i = 0; i < iterations; i++) { for (let j = 0; j < iterations; j++) { getNodeValue(testFamily(i)); } } }); }, ); ================================================ FILE: packages/recoil/core/__tests__/Recoil_useRecoilStoreID-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {StoreID as StoreIDType} from 'Recoil_Keys'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, renderElements, RecoilRoot, useRecoilStoreID; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({ renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({RecoilRoot, useRecoilStoreID} = require('../Recoil_RecoilRoot')); }); testRecoil('useRecoilStoreID', () => { const storeIDs: {[string]: StoreIDType} = {}; function StoreID({ rootKey, }: | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A1'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A2'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'B'>}>) { const storeID = useRecoilStoreID(); storeIDs[rootKey] = storeID; return null; } function MyApp() { return (
); } renderElements(); expect('A' in storeIDs).toEqual(true); expect('A1' in storeIDs).toEqual(true); expect('A2' in storeIDs).toEqual(true); expect('B' in storeIDs).toEqual(true); expect(storeIDs.A).not.toEqual(storeIDs.B); expect(storeIDs.A).not.toEqual(storeIDs.A1); expect(storeIDs.A).toEqual(storeIDs.A2); expect(storeIDs.B).not.toEqual(storeIDs.A1); expect(storeIDs.B).not.toEqual(storeIDs.A2); }); ================================================ FILE: packages/recoil/hooks/Recoil_Hooks.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type {DefaultValue} from '../core/Recoil_Node'; import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue'; import type {ComponentSubscription} from '../core/Recoil_RecoilValueInterface'; import type { NodeKey, StoreRef, StoreState, TreeState, } from '../core/Recoil_State'; const {batchUpdates} = require('../core/Recoil_Batching'); const {DEFAULT_VALUE} = require('../core/Recoil_Node'); const { currentRendererSupportsUseSyncExternalStore, reactMode, useSyncExternalStore, } = require('../core/Recoil_ReactMode'); const {useStoreRef} = require('../core/Recoil_RecoilRoot'); const {isRecoilValue} = require('../core/Recoil_RecoilValue'); const { AbstractRecoilValue, getRecoilValueAsLoadable, setRecoilValue, setUnvalidatedRecoilValue, subscribeToRecoilValue, } = require('../core/Recoil_RecoilValueInterface'); const useRetain = require('./Recoil_useRetain'); const {useCallback, useEffect, useMemo, useRef, useState} = require('react'); const {setByAddingToSet} = require('recoil-shared/util/Recoil_CopyOnWrite'); const differenceSets = require('recoil-shared/util/Recoil_differenceSets'); const {isSSR} = require('recoil-shared/util/Recoil_Environment'); const err = require('recoil-shared/util/Recoil_err'); const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation'); const gkx = require('recoil-shared/util/Recoil_gkx'); const isPromise = require('recoil-shared/util/Recoil_isPromise'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); const useComponentName = require('recoil-shared/util/Recoil_useComponentName'); function handleLoadable( loadable: Loadable, recoilValue: RecoilValue, storeRef: StoreRef, ): T { // We can't just throw the promise we are waiting on to Suspense. If the // upstream dependencies change it may produce a state in which the component // can render, but it would still be suspended on a Promise that may never resolve. if (loadable.state === 'hasValue') { return loadable.contents; } else if (loadable.state === 'loading') { const promise = new Promise(resolve => { const suspendedComponentResolvers = storeRef.current.getState().suspendedComponentResolvers; suspendedComponentResolvers.add(resolve); // SSR should clear out the wake-up resolver if the Promise is resolved // to avoid infinite loops. (See https://github.com/facebookexperimental/Recoil/pull/2073) if (isSSR && isPromise(loadable.contents)) { loadable.contents.finally(() => { suspendedComponentResolvers.delete(resolve); }); } }); // $FlowExpectedError Flow(prop-missing) for integrating with tools that inspect thrown promises @fb-only // @fb-only: promise.displayName = `Recoil State: ${recoilValue.key}`; throw promise; } else if (loadable.state === 'hasError') { throw loadable.contents; } else { throw err(`Invalid value of loadable atom "${recoilValue.key}"`); } } function validateRecoilValue( recoilValue: RecoilValue, hookName: | $TEMPORARY$string<'useRecoilState'> | $TEMPORARY$string<'useRecoilStateLoadable'> | $TEMPORARY$string<'useRecoilState_TRANSITION_SUPPORT_UNSTABLE'> | $TEMPORARY$string<'useRecoilValue'> | $TEMPORARY$string<'useRecoilValueLoadable'> | $TEMPORARY$string<'useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE'> | $TEMPORARY$string<'useRecoilValue_TRANSITION_SUPPORT_UNSTABLE'> | $TEMPORARY$string<'useResetRecoilState'> | $TEMPORARY$string<'useSetRecoilState'>, // $FlowFixMe[missing-local-annot] ) { if (!isRecoilValue(recoilValue)) { throw err( `Invalid argument to ${hookName}: expected an atom or selector but got ${String( recoilValue, )}`, ); } } export type SetterOrUpdater = ((T => T) | T) => void; export type Resetter = () => void; export type RecoilInterface = { getRecoilValue: (RecoilValue) => T, getRecoilValueLoadable: (RecoilValue) => Loadable, getRecoilState: (RecoilState) => [T, SetterOrUpdater], getRecoilStateLoadable: ( RecoilState, ) => [Loadable, SetterOrUpdater], getSetRecoilState: (RecoilState) => SetterOrUpdater, getResetRecoilState: (RecoilState) => Resetter, }; /** * Various things are broken with useRecoilInterface, particularly concurrent * mode, React strict mode, and memory management. They will not be fixed. * */ function useRecoilInterface_DEPRECATED(): RecoilInterface { const componentName = useComponentName(); const storeRef = useStoreRef(); // eslint-disable-next-line fb-www/react-no-unused-state-hook const [, forceUpdate] = useState(([]: Array<$FlowFixMe>)); const recoilValuesUsed = useRef<$ReadOnlySet>(new Set()); recoilValuesUsed.current = new Set(); // Track the RecoilValues used just during this render const previousSubscriptions = useRef<$ReadOnlySet>(new Set()); const subscriptions = useRef>(new Map()); const unsubscribeFrom = useCallback( (key: NodeKey) => { const sub = subscriptions.current.get(key); if (sub) { sub.release(); subscriptions.current.delete(key); } }, [subscriptions], ); const updateState = useCallback( (_state: TreeState | StoreState, key: NodeKey) => { if (subscriptions.current.has(key)) { forceUpdate([]); } }, [], ); // Effect to add/remove subscriptions as nodes are used useEffect(() => { const store = storeRef.current; differenceSets( recoilValuesUsed.current, previousSubscriptions.current, ).forEach(key => { if (subscriptions.current.has(key)) { expectationViolation(`Double subscription to RecoilValue "${key}"`); return; } const sub = subscribeToRecoilValue( store, new AbstractRecoilValue(key), state => updateState(state, key), componentName, ); subscriptions.current.set(key, sub); /** * Since we're subscribing in an effect we need to update to the latest * value of the atom since it may have changed since we rendered. We can * go ahead and do that now, unless we're in the middle of a batch -- * in which case we should do it at the end of the batch, due to the * following edge case: Suppose an atom is updated in another useEffect * of this same component. Then the following sequence of events occur: * 1. Atom is updated and subs fired (but we may not be subscribed * yet depending on order of effects, so we miss this) Updated value * is now in nextTree, but not currentTree. * 2. This effect happens. We subscribe and update. * 3. From the update we re-render and read currentTree, with old value. * 4. Batcher's effect sets currentTree to nextTree. * In this sequence we miss the update. To avoid that, add the update * to queuedComponentCallback if a batch is in progress. */ // FIXME delete queuedComponentCallbacks_DEPRECATED when deleting useInterface. const state = store.getState(); if (state.nextTree) { store.getState().queuedComponentCallbacks_DEPRECATED.push(() => { updateState(store.getState(), key); }); } else { updateState(store.getState(), key); } }); differenceSets( previousSubscriptions.current, recoilValuesUsed.current, ).forEach(key => { unsubscribeFrom(key); }); previousSubscriptions.current = recoilValuesUsed.current; }); // Effect to unsubscribe from all when unmounting useEffect(() => { const currentSubscriptions = subscriptions.current; // Restore subscriptions that were cleared due to StrictMode running this effect twice differenceSets( recoilValuesUsed.current, new Set(currentSubscriptions.keys()), ).forEach(key => { const sub = subscribeToRecoilValue( storeRef.current, new AbstractRecoilValue(key), state => updateState(state, key), componentName, ); currentSubscriptions.set(key, sub); }); return () => currentSubscriptions.forEach((_, key) => unsubscribeFrom(key)); }, [componentName, storeRef, unsubscribeFrom, updateState]); return useMemo(() => { // eslint-disable-next-line no-shadow function useSetRecoilState( recoilState: RecoilState, ): SetterOrUpdater { if (__DEV__) { validateRecoilValue(recoilState, 'useSetRecoilState'); } return ( newValueOrUpdater: (T => T | DefaultValue) | T | DefaultValue, ) => { setRecoilValue(storeRef.current, recoilState, newValueOrUpdater); }; } // eslint-disable-next-line no-shadow function useResetRecoilState(recoilState: RecoilState): Resetter { if (__DEV__) { validateRecoilValue(recoilState, 'useResetRecoilState'); } return () => setRecoilValue(storeRef.current, recoilState, DEFAULT_VALUE); } // eslint-disable-next-line no-shadow function useRecoilValueLoadable( recoilValue: RecoilValue, ): Loadable { if (__DEV__) { validateRecoilValue(recoilValue, 'useRecoilValueLoadable'); } if (!recoilValuesUsed.current.has(recoilValue.key)) { recoilValuesUsed.current = setByAddingToSet( recoilValuesUsed.current, recoilValue.key, ); } // TODO Restore optimization to memoize lookup const storeState = storeRef.current.getState(); return getRecoilValueAsLoadable( storeRef.current, recoilValue, reactMode().early ? storeState.nextTree ?? storeState.currentTree : storeState.currentTree, ); } // eslint-disable-next-line no-shadow function useRecoilValue(recoilValue: RecoilValue): T { if (__DEV__) { validateRecoilValue(recoilValue, 'useRecoilValue'); } const loadable = useRecoilValueLoadable(recoilValue); return handleLoadable(loadable, recoilValue, storeRef); } // eslint-disable-next-line no-shadow function useRecoilState( recoilState: RecoilState, ): [T, SetterOrUpdater] { if (__DEV__) { validateRecoilValue(recoilState, 'useRecoilState'); } return [useRecoilValue(recoilState), useSetRecoilState(recoilState)]; } // eslint-disable-next-line no-shadow function useRecoilStateLoadable( recoilState: RecoilState, ): [Loadable, SetterOrUpdater] { if (__DEV__) { validateRecoilValue(recoilState, 'useRecoilStateLoadable'); } return [ useRecoilValueLoadable(recoilState), useSetRecoilState(recoilState), ]; } return { getRecoilValue: useRecoilValue, getRecoilValueLoadable: useRecoilValueLoadable, getRecoilState: useRecoilState, getRecoilStateLoadable: useRecoilStateLoadable, getSetRecoilState: useSetRecoilState, getResetRecoilState: useResetRecoilState, }; }, [recoilValuesUsed, storeRef]); } const recoilComponentGetRecoilValueCount_FOR_TESTING = {current: 0}; function useRecoilValueLoadable_SYNC_EXTERNAL_STORE( recoilValue: RecoilValue, ): Loadable { const storeRef = useStoreRef(); const componentName = useComponentName(); const getSnapshot = useCallback(() => { if (__DEV__) { recoilComponentGetRecoilValueCount_FOR_TESTING.current++; } const store = storeRef.current; const storeState = store.getState(); const treeState = reactMode().early ? storeState.nextTree ?? storeState.currentTree : storeState.currentTree; const loadable = getRecoilValueAsLoadable(store, recoilValue, treeState); return {loadable, key: recoilValue.key}; }, [storeRef, recoilValue]); // Memoize the state to avoid unnecessary rerenders const memoizePreviousSnapshot = useCallback( (getState: () => {key: NodeKey, loadable: Loadable}) => { let prevState; return () => { const nextState = getState(); if ( prevState?.loadable.is(nextState.loadable) && prevState?.key === nextState.key ) { return prevState; } prevState = nextState; return nextState; }; }, [], ); const getMemoizedSnapshot = useMemo( () => memoizePreviousSnapshot(getSnapshot), [getSnapshot, memoizePreviousSnapshot], ); const subscribe = useCallback( (notify: () => void) => { const store = storeRef.current; const subscription = subscribeToRecoilValue( store, recoilValue, notify, componentName, ); return subscription.release; }, [storeRef, recoilValue, componentName], ); return useSyncExternalStore( subscribe, getMemoizedSnapshot, // getSnapshot() getMemoizedSnapshot, // getServerSnapshot() for SSR support ).loadable; } function useRecoilValueLoadable_TRANSITION_SUPPORT( recoilValue: RecoilValue, ): Loadable { const storeRef = useStoreRef(); const componentName = useComponentName(); // Accessors to get the current state const getLoadable = useCallback(() => { if (__DEV__) { recoilComponentGetRecoilValueCount_FOR_TESTING.current++; } const store = storeRef.current; const storeState = store.getState(); const treeState = reactMode().early ? storeState.nextTree ?? storeState.currentTree : storeState.currentTree; return getRecoilValueAsLoadable(store, recoilValue, treeState); }, [storeRef, recoilValue]); const getState = useCallback( () => ({loadable: getLoadable(), key: recoilValue.key}), [getLoadable, recoilValue.key], ); // Memoize state snapshots const updateState = useCallback( (prevState: {key: NodeKey, loadable: Loadable}) => { const nextState = getState(); return prevState.loadable.is(nextState.loadable) && prevState.key === nextState.key ? prevState : nextState; }, [getState], ); // Subscribe to Recoil state changes useEffect(() => { const subscription = subscribeToRecoilValue( storeRef.current, recoilValue, _state => { setState(updateState); }, componentName, ); // Update state in case we are using a different key setState(updateState); return subscription.release; }, [componentName, recoilValue, storeRef, updateState]); // Get the current state const [state, setState] = useState(getState); // If we changed keys, then return the state for the new key. // This is important in case the old key would cause the component to suspend. // We don't have to set the new state here since the subscribing effect above // will do that. return state.key !== recoilValue.key ? getState().loadable : state.loadable; } function useRecoilValueLoadable_LEGACY( recoilValue: RecoilValue, ): Loadable { const storeRef = useStoreRef(); // eslint-disable-next-line fb-www/react-no-unused-state-hook const [, forceUpdate] = useState(([]: Array<$FlowFixMe>)); const componentName = useComponentName(); const getLoadable = useCallback(() => { if (__DEV__) { recoilComponentGetRecoilValueCount_FOR_TESTING.current++; } const store = storeRef.current; const storeState = store.getState(); const treeState = reactMode().early ? storeState.nextTree ?? storeState.currentTree : storeState.currentTree; return getRecoilValueAsLoadable(store, recoilValue, treeState); }, [storeRef, recoilValue]); const loadable = getLoadable(); const prevLoadableRef = useRef(loadable); useEffect(() => { prevLoadableRef.current = loadable; }); useEffect(() => { const store = storeRef.current; const storeState = store.getState(); const subscription = subscribeToRecoilValue( store, recoilValue, _state => { if (!gkx('recoil_suppress_rerender_in_callback')) { return forceUpdate([]); } const newLoadable = getLoadable(); if (!prevLoadableRef.current?.is(newLoadable)) { // $FlowFixMe[incompatible-call] forceUpdate(newLoadable); } prevLoadableRef.current = newLoadable; }, componentName, ); /** * Since we're subscribing in an effect we need to update to the latest * value of the atom since it may have changed since we rendered. We can * go ahead and do that now, unless we're in the middle of a batch -- * in which case we should do it at the end of the batch, due to the * following edge case: Suppose an atom is updated in another useEffect * of this same component. Then the following sequence of events occur: * 1. Atom is updated and subs fired (but we may not be subscribed * yet depending on order of effects, so we miss this) Updated value * is now in nextTree, but not currentTree. * 2. This effect happens. We subscribe and update. * 3. From the update we re-render and read currentTree, with old value. * 4. Batcher's effect sets currentTree to nextTree. * In this sequence we miss the update. To avoid that, add the update * to queuedComponentCallback if a batch is in progress. */ if (storeState.nextTree) { store.getState().queuedComponentCallbacks_DEPRECATED.push(() => { // $FlowFixMe[incompatible-type] prevLoadableRef.current = null; forceUpdate([]); }); } else { if (!gkx('recoil_suppress_rerender_in_callback')) { return forceUpdate([]); } const newLoadable = getLoadable(); if (!prevLoadableRef.current?.is(newLoadable)) { // $FlowFixMe[incompatible-call] forceUpdate(newLoadable); } prevLoadableRef.current = newLoadable; } return subscription.release; }, [componentName, getLoadable, recoilValue, storeRef]); return loadable; } /** Like useRecoilValue(), but either returns the value if available or just undefined if not available for any reason, such as pending or error. */ function useRecoilValueLoadable(recoilValue: RecoilValue): Loadable { if (__DEV__) { validateRecoilValue(recoilValue, 'useRecoilValueLoadable'); } if (gkx('recoil_memory_managament_2020')) { // eslint-disable-next-line fb-www/react-hooks useRetain(recoilValue); } return { TRANSITION_SUPPORT: useRecoilValueLoadable_TRANSITION_SUPPORT, // Recoil will attemp to detect if `useSyncExternalStore()` is supported with // `reactMode()` before calling it. However, sometimes the host React // environment supports it but uses additional React renderers (such as with // `react-three-fiber`) which do not. While this is technically a user issue // by using a renderer with React 18+ that doesn't fully support React 18 we // don't want to break users if it can be avoided. As the current renderer can // change at runtime, we need to dynamically check and fallback if necessary. SYNC_EXTERNAL_STORE: currentRendererSupportsUseSyncExternalStore() ? useRecoilValueLoadable_SYNC_EXTERNAL_STORE : useRecoilValueLoadable_TRANSITION_SUPPORT, LEGACY: useRecoilValueLoadable_LEGACY, }[reactMode().mode](recoilValue); } /** Returns the value represented by the RecoilValue. If the value is pending, it will throw a Promise to suspend the component, if the value is an error it will throw it for the nearest React error boundary. This will also subscribe the component for any updates in the value. */ function useRecoilValue(recoilValue: RecoilValue): T { if (__DEV__) { validateRecoilValue(recoilValue, 'useRecoilValue'); } const storeRef = useStoreRef(); const loadable = useRecoilValueLoadable(recoilValue); return handleLoadable(loadable, recoilValue, storeRef); } /** Returns a function that allows the value of a RecoilState to be updated, but does not subscribe the component to changes to that RecoilState. */ function useSetRecoilState(recoilState: RecoilState): SetterOrUpdater { if (__DEV__) { validateRecoilValue(recoilState, 'useSetRecoilState'); } const storeRef = useStoreRef(); return useCallback( (newValueOrUpdater: (T => T | DefaultValue) | T | DefaultValue) => { setRecoilValue(storeRef.current, recoilState, newValueOrUpdater); }, [storeRef, recoilState], ); } /** Returns a function that will reset the value of a RecoilState to its default */ function useResetRecoilState(recoilState: RecoilState): Resetter { if (__DEV__) { validateRecoilValue(recoilState, 'useResetRecoilState'); } const storeRef = useStoreRef(); return useCallback(() => { setRecoilValue(storeRef.current, recoilState, DEFAULT_VALUE); }, [storeRef, recoilState]); } /** Equivalent to useState(). Allows the value of the RecoilState to be read and written. Subsequent updates to the RecoilState will cause the component to re-render. If the RecoilState is pending, this will suspend the component and initiate the retrieval of the value. If evaluating the RecoilState resulted in an error, this will throw the error so that the nearest React error boundary can catch it. */ function useRecoilState( recoilState: RecoilState, ): [T, SetterOrUpdater] { if (__DEV__) { validateRecoilValue(recoilState, 'useRecoilState'); } return [useRecoilValue(recoilState), useSetRecoilState(recoilState)]; } /** Like useRecoilState(), but does not cause Suspense or React error handling. Returns an object that indicates whether the RecoilState is available, pending, or unavailable due to an error. */ function useRecoilStateLoadable( recoilState: RecoilState, ): [Loadable, SetterOrUpdater] { if (__DEV__) { validateRecoilValue(recoilState, 'useRecoilStateLoadable'); } return [useRecoilValueLoadable(recoilState), useSetRecoilState(recoilState)]; } function useSetUnvalidatedAtomValues(): ( values: Map, transactionMetadata?: {...}, ) => void { const storeRef = useStoreRef(); return (values: Map, transactionMetadata: {...} = {}) => { batchUpdates(() => { storeRef.current.addTransactionMetadata(transactionMetadata); values.forEach((value, key) => setUnvalidatedRecoilValue( storeRef.current, new AbstractRecoilValue(key), value, ), ); }); }; } /** * Experimental variants of hooks with support for useTransition() */ function useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE( recoilValue: RecoilValue, ): Loadable { if (__DEV__) { validateRecoilValue( recoilValue, 'useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE', ); if (!reactMode().early) { recoverableViolation( 'Attepmt to use a hook with UNSTABLE_TRANSITION_SUPPORT in a rendering mode incompatible with concurrent rendering. Try enabling the recoil_sync_external_store or recoil_transition_support GKs.', 'recoil', ); } } if (gkx('recoil_memory_managament_2020')) { // eslint-disable-next-line fb-www/react-hooks useRetain(recoilValue); } return useRecoilValueLoadable_TRANSITION_SUPPORT(recoilValue); } function useRecoilValue_TRANSITION_SUPPORT_UNSTABLE( recoilValue: RecoilValue, ): T { if (__DEV__) { validateRecoilValue( recoilValue, 'useRecoilValue_TRANSITION_SUPPORT_UNSTABLE', ); } const storeRef = useStoreRef(); const loadable = useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(recoilValue); return handleLoadable(loadable, recoilValue, storeRef); } function useRecoilState_TRANSITION_SUPPORT_UNSTABLE( recoilState: RecoilState, ): [T, SetterOrUpdater] { if (__DEV__) { validateRecoilValue( recoilState, 'useRecoilState_TRANSITION_SUPPORT_UNSTABLE', ); } return [ useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(recoilState), useSetRecoilState(recoilState), ]; } module.exports = { recoilComponentGetRecoilValueCount_FOR_TESTING, useRecoilInterface: useRecoilInterface_DEPRECATED, useRecoilState, useRecoilStateLoadable, useRecoilValue, useRecoilValueLoadable, useResetRecoilState, useSetRecoilState, useSetUnvalidatedAtomValues, useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE, useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, useRecoilState_TRANSITION_SUPPORT_UNSTABLE, }; ================================================ FILE: packages/recoil/hooks/Recoil_SnapshotHooks.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {PersistenceType} from '../core/Recoil_Node'; import type {Snapshot} from '../core/Recoil_Snapshot'; import type {NodeKey, Store, TreeState} from '../core/Recoil_State'; const {batchUpdates} = require('../core/Recoil_Batching'); const {DEFAULT_VALUE, getNode, nodes} = require('../core/Recoil_Node'); const {useStoreRef} = require('../core/Recoil_RecoilRoot'); const { AbstractRecoilValue, setRecoilValueLoadable, } = require('../core/Recoil_RecoilValueInterface'); const {SUSPENSE_TIMEOUT_MS} = require('../core/Recoil_Retention'); const {cloneSnapshot} = require('../core/Recoil_Snapshot'); const {useCallback, useEffect, useRef, useState} = require('react'); const {isSSR} = require('recoil-shared/util/Recoil_Environment'); const filterMap = require('recoil-shared/util/Recoil_filterMap'); const filterSet = require('recoil-shared/util/Recoil_filterSet'); const mapMap = require('recoil-shared/util/Recoil_mapMap'); const mergeMaps = require('recoil-shared/util/Recoil_mergeMaps'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); const usePrevious = require('recoil-shared/util/Recoil_usePrevious'); function useTransactionSubscription(callback: Store => void) { const storeRef = useStoreRef(); useEffect(() => { const sub = storeRef.current.subscribeToTransactions(callback); return sub.release; }, [callback, storeRef]); } function externallyVisibleAtomValuesInState( state: TreeState, ): Map { const atomValues = state.atomValues.toMap(); const persistedAtomContentsValues = mapMap( filterMap(atomValues, (v, k) => { const node = getNode(k); const persistence = node.persistence_UNSTABLE; return ( persistence != null && persistence.type !== 'none' && v.state === 'hasValue' ); }), v => v.contents, ); // Merge in nonvalidated atoms; we may not have defs for them but they will // all have persistence on or they wouldn't be there in the first place. return mergeMaps( state.nonvalidatedAtoms.toMap(), persistedAtomContentsValues, ); } type ExternallyVisibleAtomInfo = { persistence_UNSTABLE: { type: PersistenceType, backButton: boolean, ... }, ... }; /** Calls the given callback after any atoms have been modified and the consequent component re-renders have been committed. This is intended for persisting the values of the atoms to storage. The stored values can then be restored using the useSetUnvalidatedAtomValues hook. The callback receives the following info: atomValues: The current value of every atom that is both persistable (persistence type not set to 'none') and whose value is available (not in an error or loading state). previousAtomValues: The value of every persistable and available atom before the transaction began. atomInfo: A map containing the persistence settings for each atom. Every key that exists in atomValues will also exist in atomInfo. modifiedAtoms: The set of atoms that were written to during the transaction. transactionMetadata: Arbitrary information that was added via the useSetUnvalidatedAtomValues hook. Useful for ignoring the useSetUnvalidatedAtomValues transaction, to avoid loops. */ function useTransactionObservation_DEPRECATED( callback: ({ atomValues: Map, previousAtomValues: Map, atomInfo: Map, modifiedAtoms: $ReadOnlySet, transactionMetadata: {[NodeKey]: mixed, ...}, }) => void, ) { useTransactionSubscription( useCallback( (store: Store) => { let previousTree = store.getState().previousTree; const currentTree = store.getState().currentTree; if (!previousTree) { recoverableViolation( 'Transaction subscribers notified without a previous tree being present -- this is a bug in Recoil', 'recoil', ); previousTree = store.getState().currentTree; // attempt to trundle on } const atomValues = externallyVisibleAtomValuesInState(currentTree); const previousAtomValues = externallyVisibleAtomValuesInState(previousTree); const atomInfo = mapMap(nodes, node => ({ persistence_UNSTABLE: { type: node.persistence_UNSTABLE?.type ?? 'none', backButton: node.persistence_UNSTABLE?.backButton ?? false, }, })); // Filter on existance in atomValues so that externally-visible rules // are also applied to modified atoms (specifically exclude selectors): const modifiedAtoms = filterSet( currentTree.dirtyAtoms, k => atomValues.has(k) || previousAtomValues.has(k), ); callback({ atomValues, previousAtomValues, atomInfo, modifiedAtoms, transactionMetadata: {...currentTree.transactionMetadata}, }); }, [callback], ), ); } function useRecoilTransactionObserver( callback: ({snapshot: Snapshot, previousSnapshot: Snapshot}) => void, ) { useTransactionSubscription( useCallback( (store: Store) => { const snapshot = cloneSnapshot(store, 'latest'); const previousSnapshot = cloneSnapshot(store, 'previous'); callback({ snapshot, previousSnapshot, }); }, [callback], ), ); } // Return a snapshot of the current state and subscribe to all state changes function useRecoilSnapshot(): Snapshot { const storeRef = useStoreRef(); const [snapshot, setSnapshot] = useState(() => cloneSnapshot(storeRef.current), ); const previousSnapshot = usePrevious(snapshot); const timeoutID = useRef(); const releaseRef = useRef void>(); useTransactionSubscription( useCallback((store: Store) => setSnapshot(cloneSnapshot(store)), []), ); // Retain snapshot for duration component is mounted useEffect(() => { const release = snapshot.retain(); // Release the retain from the rendering call if (timeoutID.current && !isSSR) { window.clearTimeout(timeoutID.current); timeoutID.current = null; releaseRef.current?.(); releaseRef.current = null; } return () => { // Defer the release. If "Fast Refresh"" is used then the component may // re-render with the same state. The previous cleanup will then run and // then the new effect will run. We don't want the snapshot to be released // by that cleanup before the new effect has a chance to retain it again. // Use timeout of 10 to workaround Firefox issue: https://github.com/facebookexperimental/Recoil/issues/1936 window.setTimeout(release, 10); }; }, [snapshot]); // Retain snapshot until above effect is run. // Release after a threshold in case component is suspended. if (previousSnapshot !== snapshot && !isSSR) { // Release the previous snapshot if (timeoutID.current) { window.clearTimeout(timeoutID.current); timeoutID.current = null; releaseRef.current?.(); releaseRef.current = null; } releaseRef.current = snapshot.retain(); timeoutID.current = window.setTimeout(() => { timeoutID.current = null; releaseRef.current?.(); releaseRef.current = null; }, SUSPENSE_TIMEOUT_MS); } return snapshot; } function gotoSnapshot(store: Store, snapshot: Snapshot): void { const storeState = store.getState(); const prev = storeState.nextTree ?? storeState.currentTree; const next = snapshot.getStore_INTERNAL().getState().currentTree; batchUpdates(() => { const keysToUpdate = new Set(); for (const keys of [prev.atomValues.keys(), next.atomValues.keys()]) { for (const key of keys) { if ( prev.atomValues.get(key)?.contents !== next.atomValues.get(key)?.contents && getNode(key).shouldRestoreFromSnapshots ) { keysToUpdate.add(key); } } } keysToUpdate.forEach(key => { setRecoilValueLoadable( store, new AbstractRecoilValue(key), next.atomValues.has(key) ? nullthrows(next.atomValues.get(key)) : DEFAULT_VALUE, ); }); store.replaceState(state => ({...state, stateID: snapshot.getID()})); }); } function useGotoRecoilSnapshot(): Snapshot => void { const storeRef = useStoreRef(); return useCallback( (snapshot: Snapshot) => gotoSnapshot(storeRef.current, snapshot), [storeRef], ); } module.exports = { useRecoilSnapshot, gotoSnapshot, useGotoRecoilSnapshot, useRecoilTransactionObserver, useTransactionObservation_DEPRECATED, useTransactionSubscription_DEPRECATED: useTransactionSubscription, }; ================================================ FILE: packages/recoil/hooks/Recoil_useGetRecoilValueInfo.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValueInfo} from '../core/Recoil_FunctionalCore'; import type {RecoilValue} from '../core/Recoil_RecoilValue'; const {peekNodeInfo} = require('../core/Recoil_FunctionalCore'); const {useStoreRef} = require('../core/Recoil_RecoilRoot'); function useGetRecoilValueInfo(): (RecoilValue) => RecoilValueInfo { const storeRef = useStoreRef(); // $FlowFixMe[incompatible-return] return ({key}): RecoilValueInfo => peekNodeInfo( storeRef.current, storeRef.current.getState().currentTree, key, ); } module.exports = useGetRecoilValueInfo; ================================================ FILE: packages/recoil/hooks/Recoil_useRecoilBridgeAcrossReactRoots.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {RecoilRoot, useStoreRef} = require('../core/Recoil_RecoilRoot'); const React = require('react'); const {useMemo} = require('react'); export type RecoilBridge = React.AbstractComponent<{children: React.Node}>; function useRecoilBridgeAcrossReactRoots(): RecoilBridge { const store = useStoreRef().current; return useMemo(() => { // eslint-disable-next-line no-shadow function RecoilBridge({ children, }: $TEMPORARY$object<{children: React.Node}>) { return {children}; } return RecoilBridge; }, [store]); } module.exports = useRecoilBridgeAcrossReactRoots; ================================================ FILE: packages/recoil/hooks/Recoil_useRecoilCallback.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {TransactionInterface} from '../core/Recoil_AtomicUpdates'; import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue'; import type {Snapshot} from '../core/Recoil_Snapshot'; import type {Store} from '../core/Recoil_State'; const {atomicUpdater} = require('../core/Recoil_AtomicUpdates'); const {batchUpdates} = require('../core/Recoil_Batching'); const {DEFAULT_VALUE} = require('../core/Recoil_Node'); const {useStoreRef} = require('../core/Recoil_RecoilRoot'); const { refreshRecoilValue, setRecoilValue, } = require('../core/Recoil_RecoilValueInterface'); const {cloneSnapshot} = require('../core/Recoil_Snapshot'); const {gotoSnapshot} = require('./Recoil_SnapshotHooks'); const {useCallback} = require('react'); const err = require('recoil-shared/util/Recoil_err'); const invariant = require('recoil-shared/util/Recoil_invariant'); const isPromise = require('recoil-shared/util/Recoil_isPromise'); const lazyProxy = require('recoil-shared/util/Recoil_lazyProxy'); export type RecoilCallbackInterface = $ReadOnly<{ set: (RecoilState, (T => T) | T) => void, reset: (RecoilState) => void, refresh: (RecoilValue) => void, snapshot: Snapshot, gotoSnapshot: Snapshot => void, transact_UNSTABLE: ((TransactionInterface) => void) => void, }>; class Sentinel {} const SENTINEL = new Sentinel(); function recoilCallback, Return, ExtraInterface>( store: Store, fn: ({...ExtraInterface, ...RecoilCallbackInterface}) => (...Args) => Return, args: Args, extraInterface?: ExtraInterface, ): Return { let ret: $FlowFixMe = SENTINEL; let releaseSnapshot; batchUpdates(() => { const errMsg = 'useRecoilCallback() expects a function that returns a function: ' + 'it accepts a function of the type (RecoilInterface) => (Args) => ReturnType ' + 'and returns a callback function (Args) => ReturnType, where RecoilInterface is ' + 'an object {snapshot, set, ...} and Args and ReturnType are the argument and return ' + 'types of the callback you want to create. Please see the docs ' + 'at recoiljs.org for details.'; if (typeof fn !== 'function') { throw err(errMsg); } // Clone the snapshot lazily to avoid overhead if the callback does not use it. // Note that this means the snapshot may represent later state from when // the callback was called if it first accesses the snapshot asynchronously. const callbackInterface: { ...ExtraInterface, ...RecoilCallbackInterface, } = lazyProxy( { ...(extraInterface ?? ({}: any)), // flowlint-line unclear-type:off // $FlowFixMe[missing-local-annot] set: (node: RecoilState, newValue: T | (T => T)) => setRecoilValue(store, node, newValue), // $FlowFixMe[missing-local-annot] reset: (node: RecoilState) => setRecoilValue(store, node, DEFAULT_VALUE), // $FlowFixMe[missing-local-annot] refresh: (node: RecoilValue) => refreshRecoilValue(store, node), gotoSnapshot: snapshot => gotoSnapshot(store, snapshot), transact_UNSTABLE: transaction => atomicUpdater(store)(transaction), }, { snapshot: () => { const snapshot = cloneSnapshot(store); releaseSnapshot = snapshot.retain(); return snapshot; }, }, ); const callback = fn(callbackInterface); if (typeof callback !== 'function') { throw err(errMsg); } ret = callback(...args); }); invariant( !(ret instanceof Sentinel), 'batchUpdates should return immediately', ); if (isPromise(ret)) { ret = ret.finally(() => { releaseSnapshot?.(); }); } else { releaseSnapshot?.(); } return (ret: Return); } function useRecoilCallback, Return>( fn: RecoilCallbackInterface => (...Args) => Return, deps?: $ReadOnlyArray, ): (...Args) => Return { const storeRef = useStoreRef(); return useCallback( // $FlowIssue[incompatible-call] (...args: Args): Return => { return recoilCallback(storeRef.current, fn, args); }, deps != null ? [...deps, storeRef] : undefined, // eslint-disable-line fb-www/react-hooks-deps ); } module.exports = {recoilCallback, useRecoilCallback}; ================================================ FILE: packages/recoil/hooks/Recoil_useRecoilRefresher.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValue} from '../core/Recoil_RecoilValue'; const {useStoreRef} = require('../core/Recoil_RecoilRoot'); const {refreshRecoilValue} = require('../core/Recoil_RecoilValueInterface'); const {useCallback} = require('react'); function useRecoilRefresher(recoilValue: RecoilValue): () => void { const storeRef = useStoreRef(); return useCallback(() => { const store = storeRef.current; refreshRecoilValue(store, recoilValue); }, [recoilValue, storeRef]); } module.exports = useRecoilRefresher; ================================================ FILE: packages/recoil/hooks/Recoil_useRecoilTransaction.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {TransactionInterface} from '../core/Recoil_AtomicUpdates'; const {atomicUpdater} = require('../core/Recoil_AtomicUpdates'); const {useStoreRef} = require('../core/Recoil_RecoilRoot'); const {useMemo} = require('react'); function useRecoilTransaction>( fn: TransactionInterface => (...Arguments) => void, deps?: $ReadOnlyArray, ): (...Arguments) => void { const storeRef = useStoreRef(); return useMemo( () => (...args: Arguments): void => { const atomicUpdate = atomicUpdater(storeRef.current); atomicUpdate(transactionInterface => { fn(transactionInterface)(...args); }); }, deps != null ? [...deps, storeRef] : undefined, // eslint-disable-line fb-www/react-hooks-deps ); } module.exports = useRecoilTransaction; ================================================ FILE: packages/recoil/hooks/Recoil_useRetain.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValue} from '../core/Recoil_RecoilValue'; const {useStoreRef} = require('../core/Recoil_RecoilRoot'); const {SUSPENSE_TIMEOUT_MS} = require('../core/Recoil_Retention'); const {updateRetainCount} = require('../core/Recoil_Retention'); const {RetentionZone} = require('../core/Recoil_RetentionZone'); const {useEffect, useRef} = require('react'); const {isSSR} = require('recoil-shared/util/Recoil_Environment'); const gkx = require('recoil-shared/util/Recoil_gkx'); const shallowArrayEqual = require('recoil-shared/util/Recoil_shallowArrayEqual'); const usePrevious = require('recoil-shared/util/Recoil_usePrevious'); // I don't see a way to avoid the any type here because we want to accept readable // and writable values with any type parameter, but normally with writable ones // RecoilState is not a subtype of RecoilState. type ToRetain = | RecoilValue // flowlint-line unclear-type:off | RetentionZone | $ReadOnlyArray | RetentionZone>; // flowlint-line unclear-type:off function useRetain(toRetain: ToRetain): void { if (!gkx('recoil_memory_managament_2020')) { return; } // eslint-disable-next-line fb-www/react-hooks return useRetain_ACTUAL(toRetain); } function useRetain_ACTUAL(toRetain: ToRetain): void { const array = Array.isArray(toRetain) ? toRetain : [toRetain]; const retainables = array.map(a => (a instanceof RetentionZone ? a : a.key)); const storeRef = useStoreRef(); useEffect(() => { if (!gkx('recoil_memory_managament_2020')) { return; } const store = storeRef.current; if (timeoutID.current && !isSSR) { // Already performed a temporary retain on render, simply cancel the release // of that temporary retain. window.clearTimeout(timeoutID.current); timeoutID.current = null; } else { for (const r of retainables) { updateRetainCount(store, r, 1); } } return () => { for (const r of retainables) { updateRetainCount(store, r, -1); } }; // eslint-disable-next-line fb-www/react-hooks-deps }, [storeRef, ...retainables]); // We want to retain if the component suspends. This is terrible but the Suspense // API affords us no better option. If we suspend and never commit after some // seconds, then release. The 'actual' retain/release in the effect above // cancels this. const timeoutID = useRef(); const previousRetainables = usePrevious(retainables); if ( !isSSR && (previousRetainables === undefined || !shallowArrayEqual(previousRetainables, retainables)) ) { const store = storeRef.current; for (const r of retainables) { updateRetainCount(store, r, 1); } if (previousRetainables) { for (const r of previousRetainables) { updateRetainCount(store, r, -1); } } if (timeoutID.current) { window.clearTimeout(timeoutID.current); } timeoutID.current = window.setTimeout(() => { timeoutID.current = null; for (const r of retainables) { updateRetainCount(store, r, -1); } }, SUSPENSE_TIMEOUT_MS); } } module.exports = useRetain; ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_Hooks_TRANSITION_SUPPORT_UNSTABLE-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; // Sanity tests for *_TRANSITION_SUPPORT_UNSTABLE() hooks. The actual tests // for useTransition() support are in Recoil_useTransition-test.js const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, selector, stringAtom, asyncSelector, flushPromisesAndTimers, renderElements, useRecoilState, useRecoilState_TRANSITION_SUPPORT_UNSTABLE, useRecoilValue, useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, useRecoilValueLoadable, useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE, useSetRecoilState, reactMode; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({act} = require('ReactTestUtils')); selector = require('../../recoil_values/Recoil_selector'); ({ stringAtom, asyncSelector, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({reactMode} = require('../../core/Recoil_ReactMode')); ({ useRecoilState, useRecoilState_TRANSITION_SUPPORT_UNSTABLE, useRecoilValue, useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, useRecoilValueLoadable, useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE, useSetRecoilState, } = require('../Recoil_Hooks')); }); testRecoil('useRecoilValue_TRANSITION_SUPPORT_UNSTABLE', async () => { if (!reactMode().early) { return; } const myAtom = stringAtom(); const [mySelector, resolve] = asyncSelector(); let setAtom; function Component() { setAtom = useSetRecoilState(myAtom); return [ useRecoilValue(myAtom), useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(myAtom), useRecoilValue(mySelector), useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(mySelector), ].join(' '); } const c = renderElements(); expect(c.textContent).toBe('loading'); act(() => resolve('RESOLVE')); await flushPromisesAndTimers(); expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE'); act(() => setAtom('SET')); expect(c.textContent).toBe('SET SET RESOLVE RESOLVE'); }); testRecoil('useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE', async () => { if (!reactMode().early) { return; } const myAtom = stringAtom(); const [mySelector, resolve] = asyncSelector(); let setAtom; function Component() { setAtom = useSetRecoilState(myAtom); return [ useRecoilValueLoadable(myAtom).getValue(), useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(myAtom).getValue(), useRecoilValueLoadable(mySelector).getValue(), useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(mySelector).getValue(), ].join(' '); } const c = renderElements(); expect(c.textContent).toBe('loading'); act(() => resolve('RESOLVE')); await flushPromisesAndTimers(); expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE'); act(() => setAtom('SET')); expect(c.textContent).toBe('SET SET RESOLVE RESOLVE'); }); testRecoil('useRecoilState_TRANSITION_SUPPORT_UNSTABLE', async () => { if (!reactMode().early) { return; } const myAtom = stringAtom(); const [myAsyncSelector, resolve] = asyncSelector(); const mySelector = selector({ key: 'useRecoilState_TRANSITION_SUPPORT_UNSTABLE selector', get: () => myAsyncSelector, // $FlowFixMe[missing-local-annot] set: ({set}, newValue) => set(myAtom, newValue), }); let setAtom, setSelector; function Component() { const [v1] = useRecoilState(myAtom); const [v2, setAtomValue] = useRecoilState_TRANSITION_SUPPORT_UNSTABLE(myAtom); setAtom = setAtomValue; const [v3] = useRecoilState(mySelector); const [v4, setSelectorValue] = useRecoilState_TRANSITION_SUPPORT_UNSTABLE(mySelector); setSelector = setSelectorValue; return [v1, v2, v3, v4].join(' '); } const c = renderElements(); expect(c.textContent).toBe('loading'); act(() => resolve('RESOLVE')); await flushPromisesAndTimers(); expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE'); act(() => setAtom('SET')); expect(c.textContent).toBe('SET SET RESOLVE RESOLVE'); act(() => setSelector('SETS')); expect(c.textContent).toBe('SETS SETS RESOLVE RESOLVE'); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_PublicHooks-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ /* eslint-disable fb-www/react-no-useless-fragment */ 'use strict'; import type { RecoilState, RecoilValue, RecoilValueReadOnly, } from '../../core/Recoil_RecoilValue'; import type {PersistenceSettings} from '../../recoil_values/Recoil_atom'; import type {Node} from 'react'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useEffect, useState, Profiler, act, Queue, batchUpdates, atom, selector, selectorFamily, ReadsAtom, renderElements, renderUnwrappedElements, recoilComponentGetRecoilValueCount_FOR_TESTING, useRecoilState, useRecoilStateLoadable, useRecoilValue, useSetRecoilState, reactMode, invariant; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useEffect, useState, Profiler} = require('react')); ({act} = require('ReactTestUtils')); Queue = require('../../adt/Recoil_Queue'); ({batchUpdates} = require('../../core/Recoil_Batching')); atom = require('../../recoil_values/Recoil_atom'); selector = require('../../recoil_values/Recoil_selector'); selectorFamily = require('../../recoil_values/Recoil_selectorFamily'); ({ ReadsAtom, renderElements, renderUnwrappedElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({reactMode} = require('../../core/Recoil_ReactMode')); ({ recoilComponentGetRecoilValueCount_FOR_TESTING, useRecoilState, useRecoilStateLoadable, useRecoilValue, useSetRecoilState, } = require('../Recoil_Hooks')); invariant = require('recoil-shared/util/Recoil_invariant'); }); let nextID = 0; function counterAtom(persistence?: PersistenceSettings) { return atom({ key: `atom${nextID++}`, default: 0, persistence_UNSTABLE: persistence, }); } function plusOneSelector(dep: RecoilValue) { const fn = jest.fn(x => x + 1); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => fn(get(dep)), }); return [sel, fn]; } function plusOneAsyncSelector( dep: RecoilValue, ): [RecoilValueReadOnly, (number) => void] { let nextTimeoutAmount = 100; const fn = jest.fn(x => { return new Promise(resolve => { setTimeout(() => { resolve(x + 1); }, nextTimeoutAmount); }); }); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => fn(get(dep)), }); return [ sel, x => { nextTimeoutAmount = x; }, ]; } function additionSelector( depA: RecoilValue, depB: RecoilValue, ) { const fn = jest.fn((a, b) => a + b); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => fn(get(depA), get(depB)), }); return [sel, fn]; } function componentThatReadsAndWritesAtom( recoilState: RecoilState, ): [React.AbstractComponent<{...}>, ((T => T) | T) => void] { let updateValue; const Component = jest.fn(() => { const [value, _updateValue] = useRecoilState(recoilState); updateValue = _updateValue; return value; }); // flowlint-next-line unclear-type:off return [(Component: any), (...args) => updateValue(...args)]; } function componentThatWritesAtom( recoilState: RecoilState, // flowlint-next-line unclear-type:off ): [any, ((T => T) | T) => void] { let updateValue; const Component = jest.fn(() => { updateValue = useSetRecoilState(recoilState); return null; }); // flowlint-next-line unclear-type:off return [(Component: any), x => updateValue(x)]; } function componentThatReadsTwoAtoms( one: RecoilState, two: RecoilState | RecoilValueReadOnly, ) { return (jest.fn(function ReadTwoAtoms() { return `${useRecoilValue(one)},${useRecoilValue(two)}`; }): any); // flowlint-line unclear-type:off } function componentThatReadsAtomWithCommitCount( recoilState: RecoilState | RecoilValueReadOnly, ) { const commit = jest.fn(() => {}); function ReadAtom() { return ( // $FlowFixMe[invalid-tuple-arity] {useRecoilValue(recoilState)} ); } return [ReadAtom, commit]; } function componentThatToggles(a: Node, b: null) { const toggle = {current: () => invariant(false, 'bug in test code')}; const Toggle = () => { const [value, setValue] = useState(false); // $FlowFixMe[incompatible-type] toggle.current = () => setValue(v => !v); return value ? b : a; }; return [Toggle, toggle]; } function baseRenderCount(gks: Array): number { return reactMode().mode === 'LEGACY' && !gks.includes('recoil_suppress_rerender_in_callback') ? 1 : 0; } testRecoil('Component throws error when passing invalid node', async () => { function Component() { try { // $FlowExpectedError[incompatible-call] useRecoilValue('foo'); } catch (error) { expect(error.message).toEqual(expect.stringContaining('useRecoilValue')); return 'CAUGHT'; } return 'INVALID'; } const container = renderElements(); expect(container.textContent).toEqual('CAUGHT'); }); testRecoil('Components are re-rendered when atoms change', async () => { const anAtom = counterAtom(); const [Component, updateValue] = componentThatReadsAndWritesAtom(anAtom); const container = renderElements(); expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('1'); }); describe('Render counts', () => { testRecoil( 'Component subscribed to atom is rendered just once', ({gks, strictMode}) => { const BASE_CALLS = baseRenderCount(gks); const sm = strictMode ? 2 : 1; const anAtom = counterAtom(); const [Component, updateValue] = componentThatReadsAndWritesAtom(anAtom); renderElements( <> , ); expect(Component).toHaveBeenCalledTimes((BASE_CALLS + 1) * sm); act(() => updateValue(1)); expect(Component).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm); }, ); testRecoil('Write-only components are not subscribed', ({strictMode}) => { const anAtom = counterAtom(); const [Component, updateValue] = componentThatWritesAtom(anAtom); renderElements( <> , ); expect(Component).toHaveBeenCalledTimes(strictMode ? 2 : 1); act(() => updateValue(1)); expect(Component).toHaveBeenCalledTimes(strictMode ? 2 : 1); }); testRecoil( 'Component that depends on atom in multiple ways is rendered just once', ({gks, strictMode}) => { const BASE_CALLS = baseRenderCount(gks); const sm = strictMode ? 2 : 1; const anAtom = counterAtom(); const [aSelector, _] = plusOneSelector(anAtom); const [WriteComp, updateValue] = componentThatWritesAtom(anAtom); const ReadComp = componentThatReadsTwoAtoms(anAtom, aSelector); renderElements( <> , ); expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 1) * sm); act(() => updateValue(1)); expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm); }, ); testRecoil( 'Component that depends on multiple atoms via selector is rendered just once', ({gks}) => { const BASE_CALLS = baseRenderCount(gks); const atomA = counterAtom(); const atomB = counterAtom(); const [aSelector, _] = additionSelector(atomA, atomB); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); const [ReadComp, commit] = componentThatReadsAtomWithCommitCount(aSelector); renderElements( <> , ); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); act(() => { batchUpdates(() => { updateValueA(1); updateValueB(1); }); }); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); }, ); testRecoil( 'Component that depends on multiple atoms directly is rendered just once', ({gks, strictMode}) => { const BASE_CALLS = baseRenderCount(gks); const sm = strictMode ? 2 : 1; const atomA = counterAtom(); const atomB = counterAtom(); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); const ReadComp = componentThatReadsTwoAtoms(atomA, atomB); renderElements( <> , ); expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 1) * sm); act(() => { batchUpdates(() => { updateValueA(1); updateValueB(1); }); }); expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm); }, ); testRecoil( 'Component is rendered just once when atom is changed twice', ({gks}) => { const BASE_CALLS = baseRenderCount(gks); const atomA = counterAtom(); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ReadComp, commit] = componentThatReadsAtomWithCommitCount(atomA); renderElements( <> , ); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); act(() => { batchUpdates(() => { updateValueA(1); updateValueA(2); }); }); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); }, ); testRecoil( 'Component does not re-read atom when rendered due to another atom changing, parent re-render, or other state change', () => { // useSyncExternalStore() will always call getSnapshot() to see if it has // mutated between render and commit. if ( reactMode().mode === 'LEGACY' || reactMode().mode === 'SYNC_EXTERNAL_STORE' ) { return; } const atomA = counterAtom(); const atomB = counterAtom(); let _, setLocal; let _a, setA; let _b, _setB; function Component() { [_, setLocal] = useState(0); [_a, setA] = useRecoilState(atomA); [_b, _setB] = useRecoilState(atomB); return null; } let __, setParentLocal; function Parent() { [__, setParentLocal] = useState(0); return ; } renderElements(); const initialCalls = recoilComponentGetRecoilValueCount_FOR_TESTING.current; expect(initialCalls).toBeGreaterThan(0); // No re-read when setting local state on the component: act(() => { setLocal(1); }); expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe( initialCalls, ); // No re-read when setting local state on its parent causing it to re-render: act(() => { setParentLocal(1); }); expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe( initialCalls, ); // Setting an atom causes a re-read for that atom only, not others: act(() => { setA(1); }); expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe( initialCalls + 1, ); }, ); testRecoil( 'Components re-render only one time if selectorFamily changed', ({gks, strictMode}) => { const BASE_CALLS = baseRenderCount(gks); const sm = strictMode ? 2 : 1; const atomA = counterAtom(); const selectAFakeId = selectorFamily({ key: 'selectItem', get: (_id: number) => // $FlowFixMe[missing-local-annot] ({get}) => get(atomA), }); const Component = (jest.fn(function ReadFromSelector({id}) { return useRecoilValue(selectAFakeId(id)); }): ({id: number}) => React.Node); let increment; const App = () => { const [state, setState] = useRecoilState(atomA); increment = () => setState(s => s + 1); return ; }; const container = renderElements(); let baseCalls = BASE_CALLS; expect(container.textContent).toEqual('0'); expect(Component).toHaveBeenCalledTimes((baseCalls + 1) * sm); act(() => increment()); if ( (reactMode().mode === 'LEGACY' && !gks.includes('recoil_suppress_rerender_in_callback')) || reactMode().mode === 'TRANSITION_SUPPORT' ) { baseCalls += 1; } expect(container.textContent).toEqual('1'); expect(Component).toHaveBeenCalledTimes((baseCalls + 2) * sm); }, ); }); describe('Component Subscriptions', () => { testRecoil( 'Can subscribe to and also change an atom in the same batch', () => { const anAtom = counterAtom(); let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(false); setVisible = mySetVisible; return visible ? children : null; } const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); expect(container.textContent).toEqual(''); act(() => { batchUpdates(() => { setVisible(true); updateValue(1337); }); }); expect(container.textContent).toEqual('1337'); }, ); testRecoil('Atom values are retained when atom has no subscribers', () => { const anAtom = counterAtom(); let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(true); setVisible = mySetVisible; return visible ? children : null; } const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); act(() => updateValue(1337)); expect(container.textContent).toEqual('1337'); act(() => setVisible(false)); expect(container.textContent).toEqual(''); act(() => setVisible(true)); expect(container.textContent).toEqual('1337'); }); testRecoil( 'Components unsubscribe from atoms when rendered without using them', ({gks, strictMode}) => { const BASE_CALLS = baseRenderCount(gks); const sm = strictMode ? 2 : 1; const atomA = counterAtom(); const atomB = counterAtom(); const [WriteA, updateValueA] = componentThatWritesAtom(atomA); const [WriteB, updateValueB] = componentThatWritesAtom(atomB); const Component = (jest.fn(function Read({state}) { const [value] = useRecoilState(state); return value; }): any); // flowlint-line unclear-type:off let toggleSwitch; const Switch = () => { const [value, setValue] = useState(false); toggleSwitch = () => setValue(true); return value ? ( ) : ( ); }; const container = renderElements( <> , ); let baseCalls = BASE_CALLS; expect(container.textContent).toEqual('0'); expect(Component).toHaveBeenCalledTimes((baseCalls + 1) * sm); act(() => updateValueA(1)); expect(container.textContent).toEqual('1'); expect(Component).toHaveBeenCalledTimes((baseCalls + 2) * sm); if ( (reactMode().mode === 'LEGACY' && !gks.includes('recoil_suppress_rerender_in_callback')) || reactMode().mode === 'TRANSITION_SUPPORT' ) { baseCalls += 1; } act(() => toggleSwitch()); expect(container.textContent).toEqual('0'); expect(Component).toHaveBeenCalledTimes((baseCalls + 3) * sm); // Now update the atom that it used to be subscribed to but should be no longer: act(() => updateValueA(2)); expect(container.textContent).toEqual('0'); expect(Component).toHaveBeenCalledTimes((baseCalls + 3) * sm); // Important part: same as before // It is subscribed to the atom that it switched to: act(() => updateValueB(3)); expect(container.textContent).toEqual('3'); expect(Component).toHaveBeenCalledTimes((baseCalls + 4) * sm); }, ); testRecoil( 'Selectors unsubscribe from upstream when they have no subscribers', () => { const atomA = counterAtom(); const atomB = counterAtom(); const [WriteA, updateValueA] = componentThatWritesAtom(atomA); // Do two layers of selectors to test that the unsubscribing is recursive: const selectorMapFn1 = jest.fn(x => x); const sel1 = selector({ key: 'selUpstream', // $FlowFixMe[missing-local-annot] get: ({get}) => selectorMapFn1(get(atomA)), }); const selectorMapFn2 = jest.fn(x => x); const sel2 = selector({ key: 'selDownstream', // $FlowFixMe[missing-local-annot] get: ({get}) => selectorMapFn2(get(sel1)), }); let toggleSwitch; const Switch = () => { const [value, setValue] = useState(false); toggleSwitch = () => setValue(true); return value ? : ; }; const container = renderElements( <> , ); expect(container.textContent).toEqual('0'); expect(selectorMapFn1).toHaveBeenCalledTimes(1); expect(selectorMapFn2).toHaveBeenCalledTimes(1); act(() => updateValueA(1)); expect(container.textContent).toEqual('1'); expect(selectorMapFn1).toHaveBeenCalledTimes(2); expect(selectorMapFn2).toHaveBeenCalledTimes(2); act(() => toggleSwitch()); expect(container.textContent).toEqual('0'); expect(selectorMapFn1).toHaveBeenCalledTimes(2); expect(selectorMapFn2).toHaveBeenCalledTimes(2); act(() => updateValueA(2)); expect(container.textContent).toEqual('0'); expect(selectorMapFn1).toHaveBeenCalledTimes(2); expect(selectorMapFn2).toHaveBeenCalledTimes(2); }, ); testRecoil( 'Unsubscribes happen in case of unmounting of a suspended component', () => { const anAtom = counterAtom(); const [aSelector, _selFn] = plusOneSelector(anAtom); const [_asyncSel, _adjustTimeout] = plusOneAsyncSelector(aSelector); // FIXME to implement }, ); testRecoil( 'Selectors stay up to date if deps are changed while they have no subscribers', () => { const anAtom = counterAtom(); const [aSelector, _] = plusOneSelector(anAtom); let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(true); setVisible = mySetVisible; return visible ? children : null; } const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); act(() => updateValue(1)); expect(container.textContent).toEqual('2'); act(() => setVisible(false)); expect(container.textContent).toEqual(''); act(() => updateValue(2)); expect(container.textContent).toEqual(''); act(() => setVisible(true)); expect(container.textContent).toEqual('3'); }, ); testRecoil( 'Selector subscriptions are correct when a selector is unsubscribed the second time', async () => { // This regression test would fail by an exception being thrown because subscription refcounts // would would fall below zero. const anAtom = counterAtom(); const [sel, _] = plusOneSelector(anAtom); const [Toggle, toggle] = componentThatToggles( , null, ); const container = renderElements( <> , ); expect(container.textContent).toEqual('1'); act(() => toggle.current()); expect(container.textContent).toEqual(''); act(() => toggle.current()); expect(container.textContent).toEqual('1'); act(() => toggle.current()); expect(container.textContent).toEqual(''); }, ); }); testRecoil('Can set an atom during rendering', () => { const anAtom = counterAtom(); function SetsDuringRendering() { const [value, setValue] = useRecoilState(anAtom); if (value !== 1) { setValue(1); } return null; } const container = renderElements( <> , ); expect(container.textContent).toEqual('1'); }); testRecoil( 'Does not re-create "setter" function after setting a value', ({strictMode, concurrentMode}) => { const sm = strictMode && concurrentMode ? 2 : 1; const anAtom = counterAtom(); const anotherAtom = counterAtom(); let useRecoilStateCounter = 0; let useRecoilStateErrorStatesCounter = 0; let useTwoAtomsCounter = 0; function Component1() { const [_, setValue] = useRecoilState(anAtom); useEffect(() => { setValue(1); useRecoilStateCounter += 1; }, [setValue]); return null; } function Component2() { const [_, setValue] = useRecoilStateLoadable(anAtom); useEffect(() => { setValue(2); useRecoilStateErrorStatesCounter += 1; }, [setValue]); return null; } // It is important to test here that the component will re-render with the // new setValue() function for a new atom, even if the value of the new // atom is the same as the previous value of the previous atom. function Component3() { const a = useTwoAtomsCounter > 0 ? anotherAtom : anAtom; // setValue fn should change when we use a different atom. const [, setValue] = useRecoilState(a); useEffect(() => { setValue(1); useTwoAtomsCounter += 1; }, [setValue]); return null; } renderElements( <> , ); expect(useRecoilStateCounter).toBe(1 * sm); expect(useRecoilStateErrorStatesCounter).toBe(1 * sm); // Component3's effect is ran twice because the atom changes and we get a new setter. // StrictMode renders twice, but we only change atoms once. So, only one extra count. expect(useTwoAtomsCounter).toBe(strictMode && concurrentMode ? 3 : 2); }, ); testRecoil( 'Can set atom during post-atom-setting effect (NOT during initial render)', async () => { const anAtom = counterAtom(); let done = false; function SetsDuringEffect() { const setValue = useSetRecoilState(anAtom); useEffect(() => { Queue.enqueueExecution('SetsDuringEffect', () => { if (!done) { setValue(1); done = true; } }); }); return null; } const container = renderElements( <> , ); expect(container.textContent).toEqual('1'); }, ); testRecoil( 'Can set atom during post-atom-setting effect regardless of effect order', async ({concurrentMode}) => { // TODO Test doesn't work in ConcurrentMode. Haven't investigated why, // but it seems fragile with the Queue for enforcing order. if (concurrentMode) { return; } function testWithOrder( order: $TEMPORARY$array< $TEMPORARY$string<'Batcher'> | $TEMPORARY$string<'SetsDuringEffect'>, >, ) { const anAtom = counterAtom(); let q: Array<[string, () => mixed]> = []; let seen = false; const original = Queue.enqueueExecution; try { Queue.enqueueExecution = (s, f) => { if (s === order[0] || seen) { seen = true; f(); q.forEach(([_, g]) => g()); } else { q.push([s, f]); } }; function SetsDuringEffect() { const [value, setValue] = useRecoilState(anAtom); useEffect(() => { Queue.enqueueExecution('SetsDuringEffect', () => { if (value !== 1) { setValue(1); } }); }); return null; } const [Comp, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); q = []; seen = false; // Thus it appears that it only breaks on the initial render. act(() => { updateValue(0); }); expect(container.textContent).toEqual('1'); } finally { Queue.enqueueExecution = original; } } testWithOrder(['SetsDuringEffect', 'Batcher']); testWithOrder(['Batcher', 'SetsDuringEffect']); }, ); testRecoil('Hooks cannot be used outside of RecoilRoot', () => { const myAtom = atom({key: 'hook outside RecoilRoot', default: 'INVALID'}); function Test() { useRecoilValue(myAtom); return 'TEST'; } // Make sure there is a friendly error message mentioning expect(() => renderUnwrappedElements()).toThrow(''); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_React-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React; let useState; let flushSync; let act; let atom; let renderElements; let useRecoilState; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); // @fb-only: ({flushSync} = require('ReactDOMComet')); ({flushSync} = require('react-dom')); // @oss-only ({act} = require('ReactTestUtils')); atom = require('../../recoil_values/Recoil_atom'); ({ renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({useRecoilState} = require('../Recoil_Hooks')); }); testRecoil('Sync React and Recoil state changes', () => { const myAtom = atom({key: 'sync react recoil', default: 0}); let setReact, setRecoil; function Component() { const [reactState, setReactState] = useState(0); const [recoilState, setRecoilState] = useRecoilState(myAtom); setReact = setReactState; setRecoil = setRecoilState; expect(reactState).toBe(recoilState); return `${reactState} - ${recoilState}`; } const c = renderElements(); expect(c.textContent).toBe('0 - 0'); // Set both React and Recoil state in the same batch and ensure the component // render always seems consistent picture of both state changes. act(() => { flushSync(() => { setReact(1); setRecoil(1); }); }); expect(c.textContent).toBe('1 - 1'); }); testRecoil('React and Recoil state change ordering', () => { const myAtom = atom({key: 'sync react recoil', default: 0}); let setReact, setRecoil; function Component() { const [reactState, setReactState] = useState(0); const [recoilState, setRecoilState] = useRecoilState(myAtom); setReact = setReactState; setRecoil = setRecoilState; // State changes may not be atomic. However, render functions should // still see state changes in the order in which they were made. expect(reactState).toBeGreaterThanOrEqual(recoilState); return `${reactState} - ${recoilState}`; } const c = renderElements(); expect(c.textContent).toBe('0 - 0'); // Test that changing React state before Recoil is seen in order act(() => { setReact(1); setRecoil(1); }); expect(c.textContent).toBe('1 - 1'); // Test that changing Recoil state before React is seen in order act(() => { setRecoil(0); setReact(0); }); expect(c.textContent).toBe('0 - 0'); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useGetRecoilValueInfo-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilState, RecoilValueReadOnly} from 'Recoil_RecoilValue'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, atom, selector, ReadsAtom, componentThatReadsAndWritesAtom, renderElements, useGetRecoilValueInfo; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({act} = require('ReactTestUtils')); atom = require('../../recoil_values/Recoil_atom'); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); useGetRecoilValueInfo = require('../Recoil_useGetRecoilValueInfo'); }); testRecoil('useGetRecoilValueInfo', ({gks}) => { const myAtom = atom({ key: 'useGetRecoilValueInfo atom', default: 'DEFAULT', }); const selectorA = selector({ key: 'useGetRecoilValueInfo A', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom), }); const selectorB = selector({ key: 'useGetRecoilValueInfo B', // $FlowFixMe[missing-local-annot] get: ({get}) => get(selectorA) + get(myAtom), }); let getNodeInfo = (_: RecoilState | RecoilValueReadOnly) => { expect(false).toBe(true); throw new Error('getRecoilValue not set'); }; function GetRecoilValueInfo() { const getRecoilValueInfo = useGetRecoilValueInfo(); // $FlowFixMe[incompatible-type] getNodeInfo = node => ({...getRecoilValueInfo(node)}); return null; } // Initial status renderElements(); expect(getNodeInfo(myAtom)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULT', }), isActive: false, isSet: false, isModified: false, type: 'atom', }); expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]); expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual([]); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([]); } expect(getNodeInfo(selectorA)).toMatchObject({ loadable: undefined, isActive: false, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getNodeInfo(selectorA).deps)).toEqual([]); expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual([]); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual( [], ); } expect(getNodeInfo(selectorB)).toMatchObject({ loadable: undefined, isActive: false, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getNodeInfo(selectorB).deps)).toEqual([]); expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual( [], ); } // After reading values const [ReadWriteAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT""DEFAULTDEFAULT"'); expect(getNodeInfo(myAtom)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULT', }), isActive: true, isSet: false, isModified: false, type: 'atom', }); expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]); expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual( expect.arrayContaining([selectorA, selectorB]), ); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([ {name: 'ReadsAndWritesAtom'}, ]); } expect(getNodeInfo(selectorA)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULT', }), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getNodeInfo(selectorA).deps)).toEqual( expect.arrayContaining([myAtom]), ); expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual( expect.arrayContaining([selectorB]), ); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual( [], ); } expect(getNodeInfo(selectorB)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULTDEFAULT', }), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getNodeInfo(selectorB).deps)).toEqual( expect.arrayContaining([myAtom, selectorA]), ); expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([ {name: 'ReadsAtom'}, ]); } // After setting a value act(() => setAtom('SET')); expect(getNodeInfo(myAtom)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}), isActive: true, isSet: true, isModified: true, type: 'atom', }); expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]); expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual( expect.arrayContaining([selectorA, selectorB]), ); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([ {name: 'ReadsAndWritesAtom'}, ]); } expect(getNodeInfo(selectorA)).toMatchObject({ loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getNodeInfo(selectorA).deps)).toEqual( expect.arrayContaining([myAtom]), ); expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual( expect.arrayContaining([selectorB]), ); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual( [], ); } expect(getNodeInfo(selectorB)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'SETSET', }), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getNodeInfo(selectorB).deps)).toEqual( expect.arrayContaining([myAtom, selectorA]), ); expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([ {name: 'ReadsAtom'}, ]); } // After reseting a value act(resetAtom); expect(getNodeInfo(myAtom)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULT', }), isActive: true, isSet: false, isModified: true, type: 'atom', }); expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]); expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual( expect.arrayContaining([selectorA, selectorB]), ); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([ {name: 'ReadsAndWritesAtom'}, ]); } expect(getNodeInfo(selectorA)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULT', }), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getNodeInfo(selectorA).deps)).toEqual( expect.arrayContaining([myAtom]), ); expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual( expect.arrayContaining([selectorB]), ); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual( [], ); } expect(getNodeInfo(selectorB)).toMatchObject({ loadable: expect.objectContaining({ state: 'hasValue', contents: 'DEFAULTDEFAULT', }), isActive: true, isSet: false, isModified: false, type: 'selector', }); expect(Array.from(getNodeInfo(selectorB).deps)).toEqual( expect.arrayContaining([myAtom, selectorA]), ); expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]); if (gks.includes('recoil_infer_component_names')) { expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([ {name: 'ReadsAtom'}, ]); } }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useGotoRecoilSnapshot-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useState, act, freshSnapshot, useGotoRecoilSnapshot, useRecoilCallback, useRecoilValue, atom, constSelector, selector, ReadsAtom, asyncSelector, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); ({act} = require('ReactTestUtils')); ({freshSnapshot} = require('../../core/Recoil_Snapshot')); ({useRecoilValue} = require('../../hooks/Recoil_Hooks')); ({useGotoRecoilSnapshot} = require('../../hooks/Recoil_SnapshotHooks')); ({useRecoilCallback} = require('../../hooks/Recoil_useRecoilCallback')); atom = require('../../recoil_values/Recoil_atom'); constSelector = require('../../recoil_values/Recoil_constSelector'); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, asyncSelector, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); }); testRecoil('Goto mapped snapshot', async () => { const snapshot = freshSnapshot(); snapshot.retain(); const myAtom = atom({ key: 'Goto Snapshot Atom', default: 'DEFAULT', }); const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const mySelector = constSelector(myAtom); const updatedSnapshot = snapshot.map(({set}) => { set(myAtom, 'SET IN SNAPSHOT'); }); updatedSnapshot.retain(); let gotoRecoilSnapshot; function GotoRecoilSnapshot() { gotoRecoilSnapshot = useGotoRecoilSnapshot(); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET IN CURRENT')); expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"'); await expect(updatedSnapshot.getPromise(myAtom)).resolves.toEqual( 'SET IN SNAPSHOT', ); act(() => gotoRecoilSnapshot(updatedSnapshot)); expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"'); act(() => setAtom('SET AGAIN IN CURRENT')); expect(c.textContent).toEqual('"SET AGAIN IN CURRENT""SET AGAIN IN CURRENT"'); // Test that atoms set after snapshot were created are reset act(() => gotoRecoilSnapshot(snapshot)); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); }); testRecoil('Goto callback snapshot', () => { const myAtom = atom({ key: 'Goto Snapshot From Callback', default: 'DEFAULT', }); const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const mySelector = constSelector(myAtom); let cb; function RecoilCallback() { const gotoSnapshot = useGotoRecoilSnapshot(); cb = useRecoilCallback(({snapshot}) => () => { const updatedSnapshot = snapshot.map(({set}) => { set(myAtom, 'SET IN SNAPSHOT'); }); gotoSnapshot(updatedSnapshot); }); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET IN CURRENT')); expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"'); act(cb); expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"'); }); testRecoil('Goto snapshot with dependent async selector', async () => { const snapshot = freshSnapshot(); snapshot.retain(); const myAtom = atom({ key: 'atom for dep async snapshot', default: 'DEFAULT', }); const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const mySelector = selector({ key: 'selector for async snapshot', // $FlowFixMe[missing-local-annot] get: ({get}) => { const dep = get(myAtom); return Promise.resolve(dep); }, }); const updatedSnapshot = snapshot.map(({set}) => { set(myAtom, 'SET IN SNAPSHOT'); }); updatedSnapshot.retain(); let gotoRecoilSnapshot; function GotoRecoilSnapshot() { gotoRecoilSnapshot = useGotoRecoilSnapshot(); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('loading'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET IN CURRENT')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"'); await expect(updatedSnapshot.getPromise(myAtom)).resolves.toEqual( 'SET IN SNAPSHOT', ); act(() => gotoRecoilSnapshot(updatedSnapshot)); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"'); }); testRecoil('Goto snapshot with async selector', async () => { const snapshot = freshSnapshot(); snapshot.retain(); const [mySelector, resolve] = asyncSelector(); let gotoRecoilSnapshot; function GotoRecoilSnapshot() { gotoRecoilSnapshot = useGotoRecoilSnapshot(); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('loading'); act(() => resolve('RESOLVE')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"RESOLVE"'); act(() => gotoRecoilSnapshot(snapshot)); expect(c.textContent).toEqual('"RESOLVE"'); }); // Test that going to a snapshot where an atom was not yet initialized will // not cause the atom to be re-initialized when used again. testRecoil( 'Effects going to previous snapshot', ({strictMode, concurrentMode}) => { const sm = strictMode && concurrentMode ? 2 : 1; let init = 0; const myAtom = atom({ key: 'gotoSnapshot effect', default: 'DEFAULT', effects: [ () => { init++; }, ], }); let forceUpdate; function ReadAtom() { const [_, setValue] = useState<{}>({}); forceUpdate = () => setValue({}); return useRecoilValue(myAtom); } let gotoRecoilSnapshot; function GotoRecoilSnapshot() { gotoRecoilSnapshot = useGotoRecoilSnapshot(); return null; } expect(init).toEqual(0); renderElements( <> , ); expect(init).toEqual(1 * sm); act(forceUpdate); expect(init).toEqual(1 * sm); act(() => gotoRecoilSnapshot?.(freshSnapshot())); expect(init).toEqual(1 * sm); act(forceUpdate); expect(init).toEqual(1 * sm); act(forceUpdate); expect(init).toEqual(1 * sm); }, ); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilBridgeAcrossReactRoots-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {StoreID} from '../../core/Recoil_Keys'; import type {MutableSnapshot} from 'Recoil_Snapshot'; import type {Node} from 'react'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, renderElements, renderUnwrappedElements, useEffect, useRef, act, RecoilRoot, useRecoilStoreID, atom, componentThatReadsAndWritesAtom, useRecoilBridgeAcrossReactRoots; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useEffect, useRef} = React); ({act} = require('ReactTestUtils')); ({ renderElements, renderUnwrappedElements, componentThatReadsAndWritesAtom, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({RecoilRoot, useRecoilStoreID} = require('../../core/Recoil_RecoilRoot')); atom = require('../../recoil_values/Recoil_atom'); useRecoilBridgeAcrossReactRoots = require('../Recoil_useRecoilBridgeAcrossReactRoots'); }); function NestedReactRoot({children}: $TEMPORARY$object<{children: Node}>) { const ref = useRef(); const RecoilBridge = useRecoilBridgeAcrossReactRoots(); useEffect(() => { renderUnwrappedElements( {children}, ref.current, ); }, [RecoilBridge, children]); return
; } testRecoil( 'useRecoilBridgeAcrossReactRoots - create a context bridge', async () => { const myAtom = atom({ key: 'useRecoilBridgeAcrossReactRoots - context bridge', default: 'DEFAULT', }); function initializeState({set, getLoadable}: MutableSnapshot) { expect(getLoadable(myAtom).contents).toEqual('DEFAULT'); set(myAtom, 'INITIALIZE'); expect(getLoadable(myAtom).contents).toEqual('INITIALIZE'); } const [ReadWriteAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"'); act(() => setAtom('SET')); expect(container.textContent).toEqual('"SET""SET"'); }, ); testRecoil('StoreID matches bridged store', () => { function RecoilStoreID({storeIDRef}: {storeIDRef: {current: ?StoreID}}) { storeIDRef.current = useRecoilStoreID(); return null; } const rootStoreIDRef = {current: null}; const nestedStoreIDRef = {current: null}; const c = renderElements( <> RENDER , ); expect(c.textContent).toEqual('RENDER'); expect(rootStoreIDRef.current).toBe(nestedStoreIDRef.current); expect(rootStoreIDRef.current).not.toBe(null); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilCallback-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Snapshot} from '../../core/Recoil_Snapshot'; import type {RecoilCallbackInterface} from '../Recoil_useRecoilCallback'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useRef, useState, useEffect, act, useStoreRef, atom, atomFamily, selector, useRecoilCallback, useRecoilValue, useRecoilState, useSetRecoilState, useResetRecoilState, ReadsAtom, flushPromisesAndTimers, renderElements, stringAtom, invariant; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useRef, useState, useEffect} = require('react')); ({act} = require('ReactTestUtils')); ({useStoreRef} = require('../../core/Recoil_RecoilRoot')); ({ atom, atomFamily, selector, useRecoilCallback, useSetRecoilState, useResetRecoilState, useRecoilValue, useRecoilState, } = require('../../Recoil_index')); ({ ReadsAtom, flushPromisesAndTimers, renderElements, stringAtom, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); invariant = require('recoil-shared/util/Recoil_invariant'); }); testRecoil('Reads Recoil values', async () => { const anAtom = atom({key: 'atom1', default: 'DEFAULT'}); let pTest: ?Promise = Promise.reject( new Error("Callback didn't resolve"), ); let cb; function Component() { cb = useRecoilCallback(({snapshot}) => () => { // eslint-disable-next-line jest/valid-expect pTest = expect(snapshot.getPromise(anAtom)).resolves.toBe('DEFAULT'); }); return null; } renderElements(); act(() => void cb()); await pTest; }); testRecoil('Can read Recoil values without throwing', async () => { const anAtom = atom({key: 'atom2', default: 123}); const asyncSelector = selector({ key: 'sel', get: () => { return new Promise(() => undefined); }, }); let didRun = false; let cb; function Component() { cb = useRecoilCallback(({snapshot}) => () => { expect(snapshot.getLoadable(anAtom)).toMatchObject({ state: 'hasValue', contents: 123, }); expect(snapshot.getLoadable(asyncSelector)).toMatchObject({ state: 'loading', }); didRun = true; // ensure these assertions do get made }); return null; } renderElements(); act(() => void cb()); expect(didRun).toBe(true); }); testRecoil('Sets Recoil values (by queueing them)', async () => { const anAtom = atom({key: 'atom3', default: 'DEFAULT'}); let cb; let pTest: ?Promise = Promise.reject( new Error("Callback didn't resolve"), ); function Component() { // $FlowFixMe[missing-local-annot] cb = useRecoilCallback(({snapshot, set}) => value => { set(anAtom, value); // eslint-disable-next-line jest/valid-expect pTest = expect(snapshot.getPromise(anAtom)).resolves.toBe('DEFAULT'); }); return null; } const container = renderElements( <> , ); expect(container.textContent).toBe('"DEFAULT"'); act(() => void cb(123)); expect(container.textContent).toBe('123'); await pTest; }); testRecoil('Reset Recoil values', async () => { const anAtom = atom({key: 'atomReset', default: 'DEFAULT'}); let setCB, resetCB; function Component() { setCB = useRecoilCallback( ({set}) => // $FlowFixMe[missing-local-annot] value => set(anAtom, value), ); resetCB = useRecoilCallback( ({reset}) => () => reset(anAtom), ); return null; } const container = renderElements( <> , ); expect(container.textContent).toBe('"DEFAULT"'); act(() => void setCB(123)); expect(container.textContent).toBe('123'); act(() => void resetCB()); expect(container.textContent).toBe('"DEFAULT"'); }); testRecoil('Sets Recoil values from async callback', async () => { const anAtom = atom({key: 'set async callback', default: 'DEFAULT'}); let cb; const pTest = []; function Component() { // $FlowFixMe[missing-local-annot] cb = useRecoilCallback(({snapshot, set}) => async value => { set(anAtom, value); pTest.push( // eslint-disable-next-line jest/valid-expect expect(snapshot.getPromise(anAtom)).resolves.toBe( value === 123 ? 'DEFAULT' : 123, ), ); }); return null; } const container = renderElements([ , , ]); expect(container.textContent).toBe('"DEFAULT"'); act(() => void cb(123)); expect(container.textContent).toBe('123'); act(() => void cb(456)); expect(container.textContent).toBe('456'); for (const aTest of pTest) { await aTest; } }); testRecoil('Reads from a snapshot created at callback call time', async () => { const anAtom = atom({key: 'atom4', default: 123}); let cb; let setter; let seenValue = null; let delay = () => new Promise(r => r()); // no delay initially function Component() { setter = useSetRecoilState(anAtom); cb = useRecoilCallback(({snapshot}) => async () => { snapshot.retain(); await delay(); seenValue = await snapshot.getPromise(anAtom); }); return null; } // It sees an update flushed after the cb is created: renderElements(); act(() => setter(345)); act(() => void cb()); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(seenValue).toBe(345); // But does not see an update flushed while the cb is in progress: seenValue = null; let resumeCallback: () => void = () => invariant(false, 'must be initialized'); delay = () => { return new Promise(resolve => { resumeCallback = resolve; }); }; act(() => void cb()); act(() => setter(678)); resumeCallback(); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(seenValue).toBe(345); }); testRecoil('Setter updater sees latest state', () => { const myAtom = atom({key: 'useRecoilCallback updater', default: 'DEFAULT'}); let setAtom; let cb; function Component() { setAtom = useSetRecoilState(myAtom); // $FlowFixMe[missing-local-annot] cb = useRecoilCallback(({snapshot, set}) => prevValue => { // snapshot sees a snapshot with the latest set state expect(snapshot.getLoadable(myAtom).contents).toEqual(prevValue); // Test that callback sees value updates from within the same transaction set(myAtom, value => { expect(value).toEqual(prevValue); return 'UPDATE'; }); set(myAtom, value => { expect(value).toEqual('UPDATE'); return 'UPDATE AGAIN'; }); }); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT"'); // Set and callback in the same transaction act(() => { setAtom('SET'); cb('SET'); cb('UPDATE AGAIN'); }); expect(c.textContent).toEqual('"UPDATE AGAIN"'); }); testRecoil('Snapshot from effect uses rendered state', () => { const myAtom = stringAtom(); let setState, actCallback, effectCallback, actCallbackValue, effectCallbackValue, effectValue; function Component() { setState = useSetRecoilState(myAtom); const value = useRecoilValue(myAtom); effectCallback = useRecoilCallback( ({snapshot}) => () => { effectCallbackValue = snapshot.getLoadable(myAtom).getValue(); }, [], ); actCallback = useRecoilCallback( ({snapshot}) => () => { actCallbackValue = snapshot.getLoadable(myAtom).getValue(); }, [], ); useEffect(() => { effectValue = value; effectCallback(); }, [value]); return null; } renderElements(); act(() => { setState('SET'); actCallback(); }); expect(actCallbackValue).toBe('SET'); expect(effectValue).toBe('SET'); expect(effectCallbackValue).toBe('SET'); }); testRecoil('goes to snapshot', async () => { const myAtom = atom({ key: 'Goto Snapshot From Callback', default: 'DEFAULT', }); let cb; function RecoilCallback() { cb = useRecoilCallback(({snapshot, gotoSnapshot}) => () => { const updatedSnapshot = snapshot.map(({set}) => { set(myAtom, 'SET IN SNAPSHOT'); }); expect(updatedSnapshot.getLoadable(myAtom).contents).toEqual( 'SET IN SNAPSHOT', ); gotoSnapshot(updatedSnapshot); }); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT"'); act(() => void cb()); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"SET IN SNAPSHOT"'); }); testRecoil('Updates are batched', () => { const family = atomFamily<_, number>({ key: 'useRecoilCallback/batching/family', default: 0, }); let cb; function RecoilCallback() { cb = useRecoilCallback(({set}) => () => { for (let i = 0; i < 100; i++) { set(family(i), 1); } }); return null; } let store: any; // flowlint-line unclear-type:off function GetStore() { store = useStoreRef().current; return null; } renderElements( <> , ); invariant(store, 'store should be initialized'); const originalReplaceState = store.replaceState; store.replaceState = jest.fn(originalReplaceState); expect(store.replaceState).toHaveBeenCalledTimes(0); act(() => cb()); expect(store.replaceState).toHaveBeenCalledTimes(1); store.replaceState = originalReplaceState; }); // Test that we always get a consistent instance of the callback function // from useRecoilCallback() when it is memoizaed testRecoil('Consistent callback function', () => { let setIteration; const Component = () => { const [iteration, _setIteration] = useState(0); setIteration = _setIteration; const callback = useRecoilCallback(() => () => {}); const callbackRef = useRef(callback); iteration ? expect(callback).not.toBe(callbackRef.current) : expect(callback).toBe(callbackRef.current); const callbackMemoized = useRecoilCallback(() => () => {}, []); const callbackMemoizedRef = useRef(callbackMemoized); expect(callbackMemoized).toBe(callbackMemoizedRef.current); return iteration; }; const out = renderElements(); expect(out.textContent).toBe('0'); act(() => setIteration(1)); // Force a re-render of the Component expect(out.textContent).toBe('1'); }); describe('Atom Effects', () => { testRecoil( 'Atom effects are initialized twice if first seen on snapshot and then on root store', ({strictMode, concurrentMode}) => { const sm = strictMode ? 1 : 0; let numTimesEffectInit = 0; const atomWithEffect = atom({ key: 'atomWithEffect', default: 0, effects: [ () => { numTimesEffectInit++; }, ], }); // StrictMode will render the component twice let renderCount = 0; const Component = () => { const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => { snapshot.getLoadable(atomWithEffect); }); readAtomFromSnapshot(); // first initialization expect(numTimesEffectInit).toBe(1 + sm * renderCount); useRecoilValue(atomWithEffect); // second initialization expect(numTimesEffectInit).toBe(2); renderCount++; return null; }; const c = renderElements(); expect(c.textContent).toBe(''); // Confirm no failures from rendering expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2); }, ); testRecoil( 'Atom effects are initialized once if first seen on root store and then on snapshot', ({strictMode, concurrentMode}) => { let numTimesEffectInit = 0; const atomWithEffect = atom({ key: 'atomWithEffect2', default: 0, effects: [ () => { numTimesEffectInit++; }, ], }); const Component = () => { const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => { snapshot.getLoadable(atomWithEffect); }); useRecoilValue(atomWithEffect); // first initialization expect(numTimesEffectInit).toBe(1); /** * should not re-initialize b/c snapshot should inherit from latest state, * wherein atom was already initialized */ readAtomFromSnapshot(); expect(numTimesEffectInit).toBe(1); return null; }; const c = renderElements(); expect(c.textContent).toBe(''); // Confirm no failures from rendering expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 2 : 1); }, ); testRecoil('onSet() called when atom initialized with snapshot', () => { const setValues = []; const myAtom = atom({ key: 'useRecoilCallback - atom effect - onSet', default: 0, effects: [ ({onSet, setSelf}) => { onSet(value => { setValues.push(value); // Confirm setSelf() still valid when initialized from snapshot setSelf(value + 1); }); }, ], }); let setAtom; const Component = () => { const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => { snapshot.getLoadable(myAtom); }); // First initialization with snapshot readAtomFromSnapshot(); // Second initialization with hook let value; [value, setAtom] = useRecoilState(myAtom); return value; }; const c = renderElements(); expect(c.textContent).toBe('0'); expect(setValues).toEqual([]); act(() => setAtom(1)); expect(setValues).toEqual([1]); expect(c.textContent).toBe('2'); }); }); describe('Selector Cache', () => { testRecoil('Refresh selector cache - transitive', () => { const getA = jest.fn(() => 'A'); // $FlowFixMe[incompatible-call] const selectorA = selector({ key: 'useRecoilCallback refresh ancestors A', get: getA, }); const getB = jest.fn(({get}) => get(selectorA) + 'B'); const selectorB = selector({ key: 'useRecoilCallback refresh ancestors B', get: getB, }); const getC = jest.fn(({get}) => get(selectorB) + 'C'); const selectorC = selector({ key: 'useRecoilCallback refresh ancestors C', get: getC, }); let refreshSelector; function Component() { refreshSelector = useRecoilCallback(({refresh}) => () => { refresh(selectorC); }); return useRecoilValue(selectorC); } const container = renderElements(); expect(container.textContent).toBe('ABC'); expect(getC).toHaveBeenCalledTimes(1); expect(getB).toHaveBeenCalledTimes(1); expect(getA).toHaveBeenCalledTimes(1); act(() => refreshSelector()); expect(container.textContent).toBe('ABC'); expect(getC).toHaveBeenCalledTimes(2); expect(getB).toHaveBeenCalledTimes(2); expect(getA).toHaveBeenCalledTimes(2); }); testRecoil('Refresh selector cache - clears entire cache', async () => { const myatom = atom({ key: 'useRecoilCallback refresh entire cache atom', default: 'a', }); let i = 0; const myselector = selector({ key: 'useRecoilCallback refresh entire cache selector', // $FlowFixMe[missing-local-annot] get: ({get}) => [get(myatom), i++], }); let setMyAtom; let refreshSelector; function Component() { const [atomValue, iValue] = useRecoilValue(myselector); refreshSelector = useRecoilCallback(({refresh}) => () => { refresh(myselector); }); setMyAtom = useSetRecoilState(myatom); return `${atomValue}-${iValue}`; } const container = renderElements(); expect(container.textContent).toBe('a-0'); act(() => setMyAtom('b')); expect(container.textContent).toBe('b-1'); act(() => refreshSelector()); expect(container.textContent).toBe('b-2'); act(() => setMyAtom('a')); expect(container.textContent).toBe('a-3'); }); }); describe('Snapshot', () => { testRecoil('Snapshot is retained for async callbacks', async ({gks}) => { let callback, callbackSnapshot, resolveSelector, resolveSelector2, resolveCallback; const myAtom = stringAtom(); const mySelector1 = selector({ key: 'useRecoilCallback snapshot retain 1', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await new Promise(resolve => { resolveSelector = resolve; }); return get(myAtom); }, }); const mySelector2 = selector({ key: 'useRecoilCallback snapshot retain 2', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await new Promise(resolve => { resolveSelector2 = resolve; }); return get(myAtom); }, }); function Component() { callback = useRecoilCallback(({snapshot}) => async () => { callbackSnapshot = snapshot; return new Promise(resolve => { resolveCallback = resolve; }); }); return null; } renderElements(); // $FlowFixMe[unused-promise] callback?.(); const selector1Promise = callbackSnapshot?.getPromise(mySelector1); const selector2Promise = callbackSnapshot?.getPromise(mySelector2); // Wait to allow callback snapshot to auto-release after clock tick. // It should still be retained for duration of callback, though. await flushPromisesAndTimers(); // Selectors resolving before callback is resolved should not be canceled act(() => resolveSelector()); await expect(selector1Promise).resolves.toBe('DEFAULT'); // Selectors resolving after callback is resolved should be canceled if (gks.includes('recoil_memory_managament_2020')) { act(() => resolveCallback()); act(() => resolveSelector2()); await expect(selector2Promise).rejects.toEqual({}); } }); testRecoil('Access snapshot asynchronously', async () => { const myAtom = stringAtom(); let setAtom; function Component() { const childFunction = async ( args: RecoilCallbackInterface, newValue: string, ) => { const oldValue = await args.snapshot.getPromise(myAtom); args.set(myAtom, newValue); return oldValue; }; const parentFunction = async ( args: RecoilCallbackInterface, newValue: string, ) => { await Promise.resolve(); return childFunction(args, newValue); }; setAtom = useRecoilCallback( // $FlowFixMe[missing-local-annot] args => async newValue => parentFunction(args, newValue), ); return useRecoilValue(myAtom); } const c = renderElements(); expect(c.textContent).toBe('DEFAULT'); let oldValue; await act(async () => (oldValue = await setAtom('SET'))); expect(oldValue).toBe('DEFAULT'); expect(c.textContent).toBe('SET'); await act(async () => (oldValue = await setAtom('SET2'))); expect(oldValue).toBe('SET'); expect(c.textContent).toBe('SET2'); }); testRecoil('Snapshot is cached', () => { const myAtom = stringAtom(); let getSnapshot; let setMyAtom, resetMyAtom; function Component() { getSnapshot = useRecoilCallback( ({snapshot}) => () => snapshot, ); setMyAtom = useSetRecoilState(myAtom); resetMyAtom = useResetRecoilState(myAtom); return null; } renderElements(); const getAtom = (snapshot: void | Snapshot) => snapshot?.getLoadable(myAtom).getValue(); const initialSnapshot = getSnapshot?.(); expect(getAtom(initialSnapshot)).toEqual('DEFAULT'); // If there are no state changes, the snapshot should be cached const nextSnapshot = getSnapshot?.(); expect(getAtom(nextSnapshot)).toEqual('DEFAULT'); expect(nextSnapshot).toBe(initialSnapshot); // With a state change, there is a new snapshot act(() => setMyAtom('SET')); const setSnapshot = getSnapshot?.(); expect(getAtom(setSnapshot)).toEqual('SET'); expect(setSnapshot).not.toBe(initialSnapshot); const nextSetSnapshot = getSnapshot?.(); expect(getAtom(nextSetSnapshot)).toEqual('SET'); expect(nextSetSnapshot).toBe(setSnapshot); act(() => setMyAtom('SET2')); const set2Snapshot = getSnapshot?.(); expect(getAtom(set2Snapshot)).toEqual('SET2'); expect(set2Snapshot).not.toBe(initialSnapshot); expect(set2Snapshot).not.toBe(setSnapshot); const nextSet2Snapshot = getSnapshot?.(); expect(getAtom(nextSet2Snapshot)).toEqual('SET2'); expect(nextSet2Snapshot).toBe(set2Snapshot); act(() => resetMyAtom()); const resetSnapshot = getSnapshot?.(); expect(getAtom(resetSnapshot)).toEqual('DEFAULT'); expect(resetSnapshot).not.toBe(initialSnapshot); expect(resetSnapshot).not.toBe(setSnapshot); const nextResetSnapshot = getSnapshot?.(); expect(getAtom(nextResetSnapshot)).toEqual('DEFAULT'); expect(nextResetSnapshot).toBe(resetSnapshot); }); testRecoil('cached snapshot is invalidated if not retained', async () => { const myAtom = stringAtom(); let getSnapshot; let setMyAtom; function Component() { getSnapshot = useRecoilCallback( ({snapshot}) => () => snapshot, ); setMyAtom = useSetRecoilState(myAtom); return null; } renderElements(); const getAtom = (snapshot: void | Snapshot) => snapshot?.getLoadable(myAtom).getValue(); act(() => setMyAtom('SET')); const setSnapshot = getSnapshot?.(); expect(getAtom(setSnapshot)).toEqual('SET'); // If cached snapshot is released, a new snapshot is provided await flushPromisesAndTimers(); const nextSetSnapshot = getSnapshot?.(); expect(nextSetSnapshot).not.toBe(setSnapshot); expect(getAtom(nextSetSnapshot)).toEqual('SET'); act(() => setMyAtom('SET2')); const set2Snapshot = getSnapshot?.(); expect(getAtom(set2Snapshot)).toEqual('SET2'); expect(set2Snapshot).not.toBe(setSnapshot); // If cached snapshot is retained, then it is used again set2Snapshot?.retain(); await flushPromisesAndTimers(); const nextSet2Snapshot = getSnapshot?.(); expect(getAtom(nextSet2Snapshot)).toEqual('SET2'); expect(nextSet2Snapshot).toBe(set2Snapshot); }); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilInterface-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilInterface} from 'Recoil_Hooks'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useRef, useState, act, atom, counterAtom, renderElements, useRecoilInterface; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useRef, useState} = require('react')); ({act} = require('ReactTestUtils')); atom = require('../../recoil_values/Recoil_atom'); ({ renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({useRecoilInterface} = require('../Recoil_Hooks')); counterAtom = atom({ key: `counterAtom`, default: 0, }); }); testRecoil('Interface for non-react code - useRecoilState', () => { function nonReactCode(recoilInterface: RecoilInterface) { return recoilInterface.getRecoilState(counterAtom); } let updateValue; const Component = () => { const recoilInterface = useRecoilInterface(); const [value, _updateValue] = nonReactCode(recoilInterface); updateValue = _updateValue; return value; }; const container = renderElements(); expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('1'); }); testRecoil('Interface for non-react code - useRecoilStateNoThrow', () => { function nonReactCode(recoilInterface: RecoilInterface) { const [loadable, setValue] = recoilInterface.getRecoilStateLoadable(counterAtom); const value = loadable.state === 'hasValue' ? loadable.contents : null; return [value, setValue]; } let updateValue; const Component = () => { const recoilInterface = useRecoilInterface(); const [value, _updateValue] = nonReactCode(recoilInterface); updateValue = _updateValue; return value; }; const container = renderElements(); expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('1'); }); testRecoil( 'Interface for non-react code - useRecoilValue, useSetRecoilState', () => { function nonReactCode(recoilInterface: RecoilInterface) { return [ recoilInterface.getRecoilValue(counterAtom), recoilInterface.getSetRecoilState(counterAtom), ]; } let updateValue; const Component = () => { const recoilInterface = useRecoilInterface(); const [value, _updateValue] = nonReactCode(recoilInterface); updateValue = _updateValue; return value; }; const container = renderElements(); expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('1'); }, ); testRecoil('Interface for non-react code - useRecoilValueNoThrow', () => { function nonReactCode(recoilInterface: RecoilInterface) { const value = recoilInterface .getRecoilValueLoadable(counterAtom) .valueMaybe(); const setValue = recoilInterface.getSetRecoilState(counterAtom); return [value, setValue]; } let updateValue; const Component = () => { const recoilInterface = useRecoilInterface(); const [value, _updateValue] = nonReactCode(recoilInterface); updateValue = _updateValue; return value; }; const container = renderElements(); expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('1'); }); // Test that we always get a consistent instance of the interface object and // hooks from useRecoilInterface() (at least for a given store) testRecoil('Consistent interface object', () => { let setValue; const Component = () => { const [value, _setValue] = useState(0); const recoilInterface = useRecoilInterface(); const recoilInterfaceRef = useRef(recoilInterface); expect(recoilInterface).toBe(recoilInterfaceRef.current); expect(recoilInterface.getRecoilState).toBe(recoilInterface.getRecoilState); setValue = _setValue; return value; }; const out = renderElements(); expect(out.textContent).toBe('0'); act(() => setValue(1)); // Force a re-render of the Component expect(out.textContent).toBe('1'); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilRefresher-test.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, atom, selector, renderElements, useRecoilValue, useSetRecoilState, useRecoilRefresher; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({act} = require('ReactTestUtils')); atom = require('../../recoil_values/Recoil_atom'); selector = require('../../recoil_values/Recoil_selector'); ({ renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); useRecoilRefresher = require('../Recoil_useRecoilRefresher'); ({useRecoilValue, useSetRecoilState} = require('../Recoil_Hooks')); }); testRecoil('useRecoilRefresher - no-op for atom', async () => { const myAtom = atom({ key: 'useRecoilRefresher no-op', default: 'default', }); let refresh; function Component() { const value = useRecoilValue(myAtom); refresh = useRecoilRefresher(myAtom); return value; } const container = renderElements(); expect(container.textContent).toBe('default'); act(() => refresh()); expect(container.textContent).toBe('default'); }); testRecoil('useRecoilRefresher - re-executes selector', async () => { let i = 0; const myselector = selector({ key: 'useRecoilRefresher re-execute', get: () => i++, }); let refresh; function Component() { const value = useRecoilValue(myselector); refresh = useRecoilRefresher(myselector); return value; } const container = renderElements(); expect(container.textContent).toBe('0'); act(() => refresh()); expect(container.textContent).toBe('1'); }); testRecoil('useRecoilRefresher - clears entire cache', async () => { const myatom = atom({ key: 'useRecoilRefresher entire cache atom', default: 'a', }); let i = 0; const myselector = selector({ key: 'useRecoilRefresher entire cache selector', // $FlowFixMe[missing-local-annot] get: ({get}) => [get(myatom), i++], }); let setMyAtom; let refresh; function Component() { const [atomValue, iValue] = useRecoilValue(myselector); refresh = useRecoilRefresher(myselector); setMyAtom = useSetRecoilState(myatom); return `${atomValue}-${iValue}`; } const container = renderElements(); expect(container.textContent).toBe('a-0'); act(() => setMyAtom('b')); expect(container.textContent).toBe('b-1'); act(() => refresh()); expect(container.textContent).toBe('b-2'); act(() => setMyAtom('a')); expect(container.textContent).toBe('a-3'); }); testRecoil('useRecoilRefresher - clears ancestor selectors', async () => { const getA = jest.fn(() => 'A'); // $FlowFixMe[incompatible-call] const selectorA = selector({ key: 'useRecoilRefresher ancestors A', get: getA, }); const getB = jest.fn(({get}) => get(selectorA) + 'B'); const selectorB = selector({ key: 'useRecoilRefresher ancestors B', get: getB, }); const getC = jest.fn(({get}) => get(selectorB) + 'C'); const selectorC = selector({ key: 'useRecoilRefresher ancestors C', get: getC, }); let refresh; function Component() { refresh = useRecoilRefresher(selectorC); return useRecoilValue(selectorC); } const container = renderElements(); expect(container.textContent).toBe('ABC'); expect(getC).toHaveBeenCalledTimes(1); expect(getB).toHaveBeenCalledTimes(1); expect(getA).toHaveBeenCalledTimes(1); act(() => refresh()); expect(container.textContent).toBe('ABC'); expect(getC).toHaveBeenCalledTimes(2); expect(getB).toHaveBeenCalledTimes(2); expect(getA).toHaveBeenCalledTimes(2); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilSnapshot-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useEffect, useState, act, freshSnapshot, atom, constSelector, selector, ReadsAtom, asyncSelector, stringAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, useGotoRecoilSnapshot, useRecoilSnapshot; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useEffect, useState} = React); ({act} = require('ReactTestUtils')); ({freshSnapshot} = require('../../core/Recoil_Snapshot')); atom = require('../../recoil_values/Recoil_atom'); constSelector = require('../../recoil_values/Recoil_constSelector'); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, asyncSelector, stringAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({ useGotoRecoilSnapshot, useRecoilSnapshot, } = require('../Recoil_SnapshotHooks')); }); testRecoil('useRecoilSnapshot - subscribe to updates', ({strictMode}) => { if (strictMode) { return; } const myAtom = stringAtom(); const [ReadsAndWritesAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const mySelector = constSelector(myAtom); const snapshots = []; function RecoilSnapshotAndSubscribe() { const snapshot = useRecoilSnapshot(); snapshot.retain(); snapshots.push(snapshot); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET IN CURRENT')); expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"'); act(resetAtom); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); expect(snapshots.length).toEqual(3); expect(snapshots[0].getLoadable(myAtom).contents).toEqual('DEFAULT'); expect(snapshots[1].getLoadable(myAtom).contents).toEqual('SET IN CURRENT'); expect(snapshots[1].getLoadable(mySelector).contents).toEqual( 'SET IN CURRENT', ); expect(snapshots[2].getLoadable(myAtom).contents).toEqual('DEFAULT'); }); testRecoil('useRecoilSnapshot - goto snapshots', ({strictMode}) => { if (strictMode) { return; } const atomA = atom({ key: 'useRecoilSnapshot - goto A', default: 'DEFAULT', }); const [ReadsAndWritesAtomA, setAtomA] = componentThatReadsAndWritesAtom(atomA); const atomB = atom({ key: 'useRecoilSnapshot - goto B', default: 'DEFAULT', }); const [ReadsAndWritesAtomB, setAtomB] = componentThatReadsAndWritesAtom(atomB); const snapshots = []; let gotoSnapshot; function RecoilSnapshotAndSubscribe() { gotoSnapshot = useGotoRecoilSnapshot(); const snapshot = useRecoilSnapshot(); snapshot.retain(); snapshots.push(snapshot); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); // $FlowFixMe[incompatible-call] act(() => setAtomA(1)); expect(c.textContent).toEqual('1"DEFAULT"'); act(() => setAtomB(2)); expect(c.textContent).toEqual('12'); expect(snapshots.length).toEqual(3); act(() => gotoSnapshot(snapshots[1])); expect(c.textContent).toEqual('1"DEFAULT"'); act(() => gotoSnapshot(snapshots[0])); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => gotoSnapshot(snapshots[2].map(({set}) => set(atomB, 3)))); expect(c.textContent).toEqual('13'); }); testRecoil( 'useRecoilSnapshot - async selector', async ({strictMode, concurrentMode}) => { const [mySelector, resolve] = asyncSelector(); const snapshots = []; function RecoilSnapshotAndSubscribe() { const snapshot = useRecoilSnapshot(); snapshot.retain(); useEffect(() => { snapshots.push(snapshot); }, [snapshot]); return null; } renderElements(); expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1); act(() => resolve('RESOLVE')); expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1); // On the first request the selector is unresolved and returns the promise await expect( snapshots[0].getLoadable(mySelector).contents, ).resolves.toEqual('RESOLVE'); // On the second request the resolved value is cached. expect(snapshots[0].getLoadable(mySelector).contents).toEqual('RESOLVE'); }, ); testRecoil( 'useRecoilSnapshot - cloned async selector', async ({strictMode, concurrentMode}) => { const [mySelector, resolve] = asyncSelector(); const snapshots = []; function RecoilSnapshotAndSubscribe() { const snapshot = useRecoilSnapshot(); snapshot.retain(); useEffect(() => { snapshots.push(snapshot); }); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('loading'); expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1); act(() => resolve('RESOLVE')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"RESOLVE"'); expect(snapshots.length).toEqual(strictMode && concurrentMode ? 3 : 2); // Snapshot contains cached result since it was cloned after resolved expect(snapshots[0].getLoadable(mySelector).contents).toEqual('RESOLVE'); }, ); testRecoil('Subscriptions', async () => { const myAtom = atom({ key: 'useRecoilSnapshot Subscriptions atom', default: 'ATOM', }); const selectorA = selector({ key: 'useRecoilSnapshot Subscriptions A', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom), }); const selectorB = selector({ key: 'useRecoilSnapshot Subscriptions B', // $FlowFixMe[missing-local-annot] get: ({get}) => get(selectorA) + get(myAtom), }); const selectorC = selector({ key: 'useRecoilSnapshot Subscriptions C', // $FlowFixMe[missing-local-annot] get: async ({get}) => { const ret = get(selectorA) + get(selectorB); await Promise.resolve(); return ret; }, }); let snapshot = freshSnapshot(); function RecoilSnapshot() { snapshot = useRecoilSnapshot(); return null; } const c = renderElements( <> , ); await flushPromisesAndTimers(); expect(c.textContent).toBe('"ATOMATOMATOM"'); expect( Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes).length, ).toBe(3); expect( Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes), ).toEqual(expect.arrayContaining([selectorA, selectorB, selectorC])); expect( Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes).length, ).toBe(2); expect( Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes), ).toEqual(expect.arrayContaining([selectorB, selectorC])); expect( Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes).length, ).toBe(1); expect( Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes), ).toEqual(expect.arrayContaining([selectorC])); expect( Array.from(snapshot.getInfo_UNSTABLE(selectorC).subscribers.nodes).length, ).toBe(0); expect( Array.from(snapshot.getInfo_UNSTABLE(selectorC).subscribers.nodes), ).toEqual(expect.arrayContaining([])); }); describe('Snapshot Retention', () => { testRecoil('Retained for duration component is mounted', async () => { let retainedDuringEffect = false; let setMount; let checkRetention; function UseRecoilSnapshot() { const snapshot = useRecoilSnapshot(); expect(snapshot.isRetained()).toBe(true); useEffect(() => { retainedDuringEffect = snapshot.isRetained(); }); checkRetention = () => snapshot.isRetained(); return null; } function Component() { const [mount, setMountState] = useState(false); setMount = setMountState; return mount ? : null; } renderElements(); expect(retainedDuringEffect).toBe(false); act(() => setMount(true)); expect(retainedDuringEffect).toBe(true); expect(checkRetention?.()).toBe(true); act(() => setMount(false)); await flushPromisesAndTimers(); expect(checkRetention?.()).toBe(false); }); testRecoil('Snapshot auto-release', async ({gks}) => { let rootFirstCnt = 0; const rootFirstAtom = atom({ key: 'useRecoilSnapshot auto-release root-first', default: 'DEFAULT', effects: [ ({setSelf}) => { rootFirstCnt++; setSelf('ROOT'); return () => { rootFirstCnt--; }; }, ], }); let snapshotFirstCnt = 0; const snapshotFirstAtom = atom({ key: 'useRecoilSnapshot auto-release snapshot-first', default: 'DEFAULT', effects: [ ({setSelf}) => { snapshotFirstCnt++; setSelf('SNAPSHOT FIRST'); return () => { snapshotFirstCnt--; }; }, ], }); let snapshotOnlyCnt = 0; const snapshotOnlyAtom = atom({ key: 'useRecoilSnapshot auto-release snapshot-only', default: 'DEFAULT', effects: [ ({setSelf}) => { snapshotOnlyCnt++; setSelf('SNAPSHOT ONLY'); return () => { snapshotOnlyCnt--; }; }, ], }); let rootOnlyCnt = 0; const rootOnlyAtom = atom({ key: 'useRecoilSnapshot auto-release root-only', default: 'DEFAULT', effects: [ ({setSelf}) => { rootOnlyCnt++; setSelf('RETAIN'); return () => { rootOnlyCnt--; }; }, ], }); let setMount: boolean => void = () => { throw new Error('Test Error'); }; function UseRecoilSnapshot() { const snapshot = useRecoilSnapshot(); return ( snapshot.getLoadable(snapshotFirstAtom).getValue() + snapshot.getLoadable(snapshotOnlyAtom).getValue() ); } function Component() { const [mount, setState] = useState(false); setMount = setState; return mount ? ( <> ) : ( ); } const c = renderElements(); expect(c.textContent).toBe('"RETAIN"'); expect(rootOnlyCnt).toBe(1); expect(snapshotOnlyCnt).toBe(0); expect(rootFirstCnt).toBe(0); expect(snapshotFirstCnt).toBe(0); act(() => setMount(true)); expect(c.textContent).toBe( '"RETAIN""ROOT"SNAPSHOT FIRSTSNAPSHOT ONLY"SNAPSHOT FIRST"', ); await flushPromisesAndTimers(); expect(rootOnlyCnt).toBe(1); expect(snapshotOnlyCnt).toBe(1); expect(rootFirstCnt).toBe(1); expect(snapshotFirstCnt).toBe(2); // Confirm snapshot isn't released until component is unmounted await flushPromisesAndTimers(); expect(rootOnlyCnt).toBe(1); expect(snapshotOnlyCnt).toBe(1); expect(rootFirstCnt).toBe(1); expect(snapshotFirstCnt).toBe(2); // Auto-release snapshot act(() => setMount(false)); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RETAIN"'); expect(rootOnlyCnt).toBe(1); expect(snapshotOnlyCnt).toBe(0); if (gks.includes('recoil_memory_management_2020')) { expect(rootFirstCnt).toBe(0); expect(snapshotFirstCnt).toBe(0); } }); }); testRecoil('useRecoilSnapshot - re-render', () => { const myAtom = stringAtom(); const [ReadsAndWritesAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const snapshots = []; let forceUpdate; function RecoilSnapshotAndSubscribe() { const [, setState] = useState(([]: Array<$FlowFixMe>)); forceUpdate = () => setState([]); const snapshot = useRecoilSnapshot(); snapshots.push(snapshot); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT"'); expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe( 'DEFAULT', ); act(forceUpdate); expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe( 'DEFAULT', ); act(() => setAtom('SET')); expect(c.textContent).toEqual('"SET"'); expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe( 'SET', ); act(forceUpdate); expect(c.textContent).toEqual('"SET"'); expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe( 'SET', ); act(resetAtom); expect(c.textContent).toEqual('"DEFAULT"'); expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe( 'DEFAULT', ); act(forceUpdate); expect(c.textContent).toEqual('"DEFAULT"'); expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe( 'DEFAULT', ); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilStateReset-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, atom, atomFamily, selector, selectorFamily, asyncSelector, componentThatReadsAndWritesAtom, renderElements; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({act} = require('ReactTestUtils')); atom = require('../../recoil_values/Recoil_atom'); atomFamily = require('../../recoil_values/Recoil_atomFamily'); selector = require('../../recoil_values/Recoil_selector'); selectorFamily = require('../../recoil_values/Recoil_selectorFamily'); ({ asyncSelector, componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); }); testRecoil('useRecoilValueReset - value default', () => { const myAtom = atom({ key: 'useResetRecoilState/atom', default: 'default', }); const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements(); expect(container.textContent).toBe('"default"'); act(() => setValue('set value')); expect(container.textContent).toBe('"set value"'); act(() => resetValue()); expect(container.textContent).toBe('"default"'); }); testRecoil('useResetRecoilState - sync selector default', () => { const mySelector = selector({ key: 'useResetRecoilState/sync_selector/default', get: () => 'fallback', }); const myAtom = atom({ key: 'useResetRecoilState/sync_selector', default: mySelector, }); const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements(); expect(container.textContent).toBe('"fallback"'); act(() => setValue('set value')); expect(container.textContent).toBe('"set value"'); act(() => resetValue()); expect(container.textContent).toBe('"fallback"'); }); // Test resetting an atom to a fallback selector with a pending async value testRecoil('useResetRecoilState - async selector default', () => { const [mySelector, resolve] = asyncSelector(); const myAtom = atom({ key: 'useResetRecoilState/async_selector', default: mySelector, }); const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements(); expect(container.textContent).toBe('loading'); act(() => setValue('set value')); act(() => jest.runAllTimers()); // Hmm, interesting this is required expect(container.textContent).toBe('"set value"'); act(() => resetValue()); expect(container.textContent).toBe('loading'); act(() => resolve('resolved fallback')); act(() => jest.runAllTimers()); expect(container.textContent).toBe('"resolved fallback"'); }); // Test resetting an atom to a fallback selector with a pending async value testRecoil('useResetRecoilState - scoped atom', () => { return; // @oss-only const myAtom = atom({ key: 'useResetRecoilState/scoped_atom', default: 'default', scopeRules_APPEND_ONLY_READ_THE_DOCS: [ [atom({key: 'useResetRecoilState/scoped_atom/scope_rule', default: 0})], ], }); const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements(); expect(container.textContent).toBe('"default"'); act(() => setValue('set value')); expect(container.textContent).toBe('"set value"'); act(() => resetValue()); expect(container.textContent).toBe('"default"'); // TODO test resetting a scoped atom that was upgraded with a new rule }); // Test resetting an atom to a fallback selector with a pending async value testRecoil('useResetRecoilState - atom family', () => { const myAtom = atomFamily< _, {default: string} | {default: string, secondParam: string}, >({ key: 'useResetRecoilState/atomFamily', // $FlowFixMe[missing-local-annot] default: ({default: def}) => def, }); const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom( myAtom({default: 'default'}), ); const [ComponentB, setValueB, resetValueB] = componentThatReadsAndWritesAtom( myAtom({default: 'default', secondParam: 'superset'}), ); const container = renderElements( <> , ); expect(container.textContent).toBe('"default""default"'); // $FlowFixMe[incompatible-call] act(() => setValue('set value')); expect(container.textContent).toBe('"set value""default"'); act(() => resetValue()); expect(container.textContent).toBe('"default""default"'); // $FlowFixMe[incompatible-call] act(() => setValue('set value A')); expect(container.textContent).toBe('"set value A""default"'); // $FlowFixMe[incompatible-call] act(() => setValueB('set value B')); expect(container.textContent).toBe('"set value A""set value B"'); act(() => resetValueB()); expect(container.textContent).toBe('"set value A""default"'); }); testRecoil('useResetRecoilState - selector', () => { const myAtom = atom({ key: 'useResetRecoilState/selector/atom', default: 'default', }); const mySelector = selector({ key: 'useResetRecoilState/selector', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom), // $FlowFixMe[missing-local-annot] set: ({set}, value) => set(myAtom, value), }); const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(mySelector); const container = renderElements(); expect(container.textContent).toBe('"default"'); act(() => setValue('set value')); expect(container.textContent).toBe('"set value"'); act(() => resetValue()); expect(container.textContent).toBe('"default"'); }); testRecoil('useResetRecoilState - parameterized selector', () => { const myAtom = atom({ key: 'useResetRecoilState/parameterized_selector/atom', default: 'default', }); const mySelector = selectorFamily<_, string>({ key: 'useResetRecoilState/parameterized_selector', get: () => // $FlowFixMe[missing-local-annot] ({get}) => get(myAtom), set: () => // $FlowFixMe[missing-local-annot] ({set}, value) => set(myAtom, value), }); const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom( mySelector('parameter'), ); const container = renderElements(); expect(container.textContent).toBe('"default"'); act(() => setValue('set value')); expect(container.textContent).toBe('"set value"'); act(() => resetValue()); expect(container.textContent).toBe('"default"'); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilTransaction-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {act} = require('ReactTestUtils'); const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useState, useEffect, atom, useRecoilValue, useRecoilState, useRecoilTransaction, useRecoilSnapshot, renderElements, flushPromisesAndTimers, ReadsAtom; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState, useEffect} = React); ({ atom, useRecoilValue, useRecoilState, useRecoilTransaction_UNSTABLE: useRecoilTransaction, useRecoilSnapshot, } = require('../../Recoil_index')); ({ renderElements, flushPromisesAndTimers, ReadsAtom, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); }); describe('Atoms', () => { testRecoil('Get with transaction', () => { const myAtom = atom({ key: 'useRecoilTransaction atom get', default: 'DEFAULT', }); let readAtom; let ranTransaction = false; function Component() { readAtom = useRecoilTransaction(({get}) => () => { expect(get(myAtom)).toEqual('DEFAULT'); ranTransaction = true; }); return null; } renderElements(); expect(ranTransaction).toBe(false); act(readAtom); expect(ranTransaction).toBe(true); }); testRecoil('Set with transaction', () => { const myAtom = atom({ key: 'useRecoilTransaction atom set', default: 'DEFAULT', }); function Component() { // $FlowFixMe[missing-local-annot] const transact = useRecoilTransaction(({set, get}) => value => { set(myAtom, 'TMP'); expect(get(myAtom)).toEqual('TMP'); set(myAtom, old => { expect(old).toEqual('TMP'); return value; }); expect(get(myAtom)).toEqual(value); }); useEffect(() => { transact('TRANSACT'); }); return null; } const c = renderElements( <> , ); expect(c.textContent).toEqual('"TRANSACT"'); }); testRecoil('Dirty atoms', async () => { const beforeAtom = atom({ key: 'useRecoilTransaction dirty before', default: 'DEFAULT', }); const duringAtomA = atom({ key: 'useRecoilTransaction dirty during A', default: 'DEFAULT', }); const duringAtomB = atom({ key: 'useRecoilTransaction dirty during B', default: 'DEFAULT', }); const afterAtom = atom({ key: 'useRecoilTransaction dirty after', default: 'DEFAULT', }); let snapshot; let firstEffect = true; function Component() { const [beforeValue, setBefore] = useState('INITIAL'); const [beforeAtomValue, setBeforeAtom] = useRecoilState(beforeAtom); const duringAValue = useRecoilValue(duringAtomA); const duringBValue = useRecoilValue(duringAtomB); const [afterAtomValue, setAfterAtom] = useRecoilState(afterAtom); const [afterValue, setAfter] = useState('INITIAL'); const transaction = useRecoilTransaction(({set, get}) => () => { expect(get(beforeAtom)).toEqual('BEFORE'); expect(get(duringAtomA)).toEqual('DEFAULT'); expect(get(duringAtomB)).toEqual('DEFAULT'); expect(get(afterAtom)).toEqual('DEFAULT'); set(duringAtomA, 'DURING_A'); set(duringAtomB, 'DURING_B'); }); snapshot = useRecoilSnapshot(); useEffect(() => { setTimeout(() => { act(() => { if (firstEffect) { setBefore('BEFORE'); setBeforeAtom('BEFORE'); transaction(); setAfterAtom('AFTER'); setAfter('AFTER'); } firstEffect = false; }); }, 0); }); return [ beforeValue, beforeAtomValue, duringAValue, duringBValue, afterAtomValue, afterValue, ].join(','); } const c = renderElements(); expect(c.textContent).toBe( 'INITIAL,DEFAULT,DEFAULT,DEFAULT,DEFAULT,INITIAL', ); expect( Array.from(snapshot?.getNodes_UNSTABLE({isModified: true}) ?? []), ).toEqual([]); await flushPromisesAndTimers(); expect(c.textContent).toBe('BEFORE,BEFORE,DURING_A,DURING_B,AFTER,AFTER'); expect( Array.from(snapshot?.getNodes_UNSTABLE({isModified: true}) ?? []).map( ({key}) => key, ), ).toEqual([ 'useRecoilTransaction dirty before', 'useRecoilTransaction dirty during A', 'useRecoilTransaction dirty during B', 'useRecoilTransaction dirty after', ]); }); }); describe('Atom Effects', () => { testRecoil( 'Atom effects are run when first get from a transaction', async () => { let numTimesEffectInit = 0; const atomWithEffect = atom({ key: 'atom effect first get transaction', default: 'DEFAULT', effects: [ ({trigger}) => { expect(trigger).toEqual('get'); numTimesEffectInit++; }, ], }); let getAtomWithTransaction; let ranTransaction = false; const Component = () => { getAtomWithTransaction = useRecoilTransaction(({get}) => () => { expect(get(atomWithEffect)).toEqual('DEFAULT'); ranTransaction = true; }); return null; }; renderElements(); act(() => getAtomWithTransaction()); expect(ranTransaction).toBe(true); expect(numTimesEffectInit).toBe(1); }, ); testRecoil( 'Atom effects are run when first set with a transaction', async ({strictMode, concurrentMode}) => { let numTimesEffectInit = 0; const atomWithEffect = atom({ key: 'atom effect first set transaction', default: 'DEFAULT', effects: [ ({trigger}) => { expect(trigger).toEqual('set'); numTimesEffectInit++; }, ], }); let setAtomWithTransaction; const Component = () => { setAtomWithTransaction = useRecoilTransaction(({set}) => () => { set(atomWithEffect, 'SET'); }); useEffect(() => { act(setAtomWithTransaction); }); return null; }; renderElements(); expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 2 : 1); }, ); testRecoil('Atom effects can initialize for a transaction', async () => { let numTimesEffectInit = 0; const atomWithEffect = atom({ key: 'atom effect init transaction', default: 'DEFAULT', effects: [ ({setSelf}) => { setSelf('INIT'); numTimesEffectInit++; }, ], }); let initAtomWithTransaction; let ranTransaction = false; const Component = () => { initAtomWithTransaction = useRecoilTransaction(({get}) => () => { expect(get(atomWithEffect)).toEqual('INIT'); ranTransaction = true; }); return null; }; renderElements(); act(() => initAtomWithTransaction()); expect(ranTransaction).toBe(true); expect(numTimesEffectInit).toBe(1); }); testRecoil( 'Atom effects are initialized once if first seen on transaction and then on root store', ({strictMode, concurrentMode}) => { const sm = strictMode && concurrentMode ? 2 : 1; let numTimesEffectInit = 0; const atomWithEffect = atom({ key: 'useRecoilTransaction effect first get transaction', default: 0, effects: [ () => { numTimesEffectInit++; }, ], }); const Component = () => { const readAtomFromSnapshot = useRecoilTransaction(({get}) => () => { get(atomWithEffect); }); readAtomFromSnapshot(); // first initialization expect(numTimesEffectInit).toBeGreaterThanOrEqual(1); const effectsRan = numTimesEffectInit; /** * Transactions do not use a snapshot under the hood, so any initialized * effects from a transaction will be reflected in root store */ useRecoilValue(atomWithEffect); expect(numTimesEffectInit).toBe(effectsRan); return 'RENDERED'; }; const c = renderElements(); expect(c.textContent).toBe('RENDERED'); expect(numTimesEffectInit).toBe(1 * sm); }, ); testRecoil( 'Atom effects are initialized once if first seen on root store and then on snapshot', ({strictMode, concurrentMode}) => { const sm = strictMode && concurrentMode ? 2 : 1; let numTimesEffectInit = 0; const atomWithEffect = atom({ key: 'atom effect first get root', default: 0, effects: [ () => { numTimesEffectInit++; }, ], }); const Component = () => { const readAtomFromSnapshot = useRecoilTransaction(({get}) => () => { get(atomWithEffect); }); useRecoilValue(atomWithEffect); // first initialization expect(numTimesEffectInit).toBeGreaterThanOrEqual(1); const effectsRan = numTimesEffectInit; /** * Transactions do not use a snapshot under the hood, so any initialized * effects from a transaction will be reflected in root store */ readAtomFromSnapshot(); expect(numTimesEffectInit).toBe(effectsRan); return 'RENDERED'; }; const c = renderElements(); expect(c.textContent).toBe('RENDERED'); expect(numTimesEffectInit).toBe(1 * sm); }, ); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilTransactionObserver-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall perf_viz */ 'use strict'; import type {Snapshot} from 'Recoil_Snapshot'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, freshSnapshot, atom, atomFamily, selector, ReadsAtom, asyncSelector, componentThatReadsAndWritesAtom, renderElements, useRecoilTransactionObserver; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({act} = require('ReactTestUtils')); ({freshSnapshot} = require('../../core/Recoil_Snapshot')); atom = require('../../recoil_values/Recoil_atom'); atomFamily = require('../../recoil_values/Recoil_atomFamily'); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, asyncSelector, componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({useRecoilTransactionObserver} = require('../Recoil_SnapshotHooks')); }); function TransactionObserver({ callback, }: $TEMPORARY$object<{ callback: ({previousSnapshot: Snapshot, snapshot: Snapshot}) => void, }>) { useRecoilTransactionObserver(callback); return null; } // Run test first since it deals with all registered atoms testRecoil('getNodes', () => { let snapshot = freshSnapshot(); function UseRecoilTransactionObserver() { useRecoilTransactionObserver(p => { p.snapshot.retain(); snapshot = p.snapshot; }); return null; } const atoms = atomFamily({ key: 'useRecoilTransactionObserver getNodes atom', default: x => x, }); const [ReadsAtomA, setAtomA, resetAtomA] = componentThatReadsAndWritesAtom( atoms('A'), ); const [ReadsAtomB, setAtomB] = componentThatReadsAndWritesAtom(atoms('B')); const selectorA = selector({ key: 'useRecoilTransactionObserver getNodes selector', // $FlowFixMe[missing-local-annot] get: ({get}) => get(atoms('A')) + '-SELECTOR', }); const c = renderElements( <> , ); expect(c.textContent).toEqual('"A""B""A-SELECTOR"'); expect( Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true})).length, ).toEqual(0); act(() => setAtomA('A')); // >= 3 because we expect at least nodes for atom's A and B from // the family and selectorA. In reality we could get more due to internal // helper selectors and default fallback atoms. expect( Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true})).length, ).toBeGreaterThanOrEqual(3); const nodes = Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true})); expect(nodes).toEqual( expect.arrayContaining([atoms('A'), atoms('B'), selectorA]), ); // Test atom A is set const aDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true})); expect(aDirty.length).toEqual(1); expect(snapshot.getLoadable(aDirty[0]).contents).toEqual('A'); // Test atom B is set act(() => setAtomB('B')); const bDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true})); expect(bDirty.length).toEqual(1); expect(snapshot.getLoadable(bDirty[0]).contents).toEqual('B'); // Test atoms const atomNodes = Array.from( snapshot.getNodes_UNSTABLE({isInitialized: true}), ); expect(atomNodes.map(atom => snapshot.getLoadable(atom).contents)).toEqual( expect.arrayContaining(['A', 'B']), ); // Test selector const selectorNodes = Array.from( snapshot.getNodes_UNSTABLE({isInitialized: true}), ); expect( selectorNodes.map(atom => snapshot.getLoadable(atom).contents), ).toEqual(expect.arrayContaining(['A-SELECTOR'])); // Test Reset act(resetAtomA); const resetDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true})); expect(resetDirty.length).toEqual(1); expect(resetDirty[0]).toBe(aDirty[0]); // TODO Test dirty selectors }); testRecoil('Can observe atom value', async () => { const atomA = atom({ key: 'Observe Atom A', default: 'DEFAULT A', }); const atomB = atom({ key: 'Observe Atom B', default: 'DEFAULT B', }); const [WriteAtomA, setAtomA, resetA] = componentThatReadsAndWritesAtom(atomA); const [WriteAtomB, setAtomB] = componentThatReadsAndWritesAtom(atomB); const transactions = []; renderElements( <> { snapshot.retain(); previousSnapshot.retain(); transactions.push({snapshot, previousSnapshot}); }} /> , ); act(() => setAtomB('SET B')); expect(transactions.length).toEqual(1); await expect(transactions[0].snapshot.getPromise(atomA)).resolves.toEqual( 'DEFAULT A', ); await expect( transactions[0].previousSnapshot.getPromise(atomA), ).resolves.toEqual('DEFAULT A'); await expect(transactions[0].snapshot.getPromise(atomB)).resolves.toEqual( 'SET B', ); await expect( transactions[0].previousSnapshot.getPromise(atomB), ).resolves.toEqual('DEFAULT B'); act(() => setAtomA('SET A')); expect(transactions.length).toEqual(2); await expect(transactions[1].snapshot.getPromise(atomA)).resolves.toEqual( 'SET A', ); await expect( transactions[1].previousSnapshot.getPromise(atomA), ).resolves.toEqual('DEFAULT A'); await expect(transactions[1].snapshot.getPromise(atomB)).resolves.toEqual( 'SET B', ); await expect( transactions[1].previousSnapshot.getPromise(atomB), ).resolves.toEqual('SET B'); act(() => resetA()); expect(transactions.length).toEqual(3); await expect(transactions[2].snapshot.getPromise(atomA)).resolves.toEqual( 'DEFAULT A', ); await expect( transactions[2].previousSnapshot.getPromise(atomA), ).resolves.toEqual('SET A'); await expect(transactions[2].snapshot.getPromise(atomB)).resolves.toEqual( 'SET B', ); await expect( transactions[2].previousSnapshot.getPromise(atomB), ).resolves.toEqual('SET B'); }); testRecoil('Can observe selector value', async () => { const atomA = atom({ key: 'Observe Atom for Selector', default: 'DEFAULT', }); const selectorA = selector({ key: 'Observer Selector As', // $FlowFixMe[missing-local-annot] get: ({get}) => `SELECTOR ${get(atomA)}`, }); const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(atomA); const transactions = []; renderElements( <> { snapshot.retain(); previousSnapshot.retain(); transactions.push({snapshot, previousSnapshot}); }} /> , ); act(() => setAtom('SET')); expect(transactions.length).toEqual(1); await expect(transactions[0].snapshot.getPromise(atomA)).resolves.toEqual( 'SET', ); await expect( transactions[0].previousSnapshot.getPromise(atomA), ).resolves.toEqual('DEFAULT'); await expect(transactions[0].snapshot.getPromise(selectorA)).resolves.toEqual( 'SELECTOR SET', ); await expect( transactions[0].previousSnapshot.getPromise(selectorA), ).resolves.toEqual('SELECTOR DEFAULT'); }); testRecoil('Can observe async selector value', async () => { const atomA = atom({ key: 'Observe Atom for Async Selector', default: 'DEFAULT', }); const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(atomA); const [selectorA, resolveA] = asyncSelector(); const transactions = []; renderElements( <> { snapshot.retain(); previousSnapshot.retain(); transactions.push({snapshot, previousSnapshot}); }} /> , ); act(() => setAtom('SET')); expect(transactions.length).toEqual(1); expect(transactions[0].snapshot.getLoadable(selectorA).state).toEqual( 'loading', ); act(() => resolveA('RESOLVE')); expect(transactions.length).toEqual(1); await expect(transactions[0].snapshot.getPromise(selectorA)).resolves.toEqual( 'RESOLVE', ); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useRecoilValueLoadable-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, selector, constSelector, errorSelector, asyncSelector, renderElements, useRecoilValueLoadable, flushPromisesAndTimers; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({act} = require('ReactTestUtils')); constSelector = require('../../recoil_values/Recoil_constSelector'); errorSelector = require('../../recoil_values/Recoil_errorSelector'); selector = require('../../recoil_values/Recoil_selector'); ({ asyncSelector, renderElements, flushPromisesAndTimers, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({useRecoilValueLoadable} = require('../Recoil_Hooks')); }); // These tests should cover the Loadable interface returned by useRecoilValueLoadable. // It is also used by useRecoilStateNoThrow, waitForNone, and waitForAny testRecoil('useRecoilValueLoadable - loadable with value', async () => { const valueSel = constSelector('VALUE'); let promise; function ReadLoadable() { const loadable = useRecoilValueLoadable(valueSel); expect(loadable.state).toBe('hasValue'); expect(loadable.contents).toBe('VALUE'); expect(loadable.getValue()).toBe('VALUE'); // eslint-disable-next-line jest/valid-expect promise = expect(loadable.toPromise()).resolves.toBe('VALUE'); expect(loadable.valueMaybe()).toBe('VALUE'); expect(loadable.valueOrThrow()).toBe('VALUE'); expect(loadable.errorMaybe()).toBe(undefined); expect(() => loadable.errorOrThrow()).toThrow(Error); expect(loadable.promiseMaybe()).toBe(undefined); expect(() => loadable.promiseOrThrow()).toThrow(Error); return loadable.valueOrThrow(); } const c = renderElements(); expect(c.textContent).toEqual('VALUE'); await promise; }); testRecoil('useRecoilValueLoadable - loadable with error', async () => { const valueSel = errorSelector<$FlowFixMe>('ERROR'); let promise; function ReadLoadable() { const loadable = useRecoilValueLoadable(valueSel); expect(loadable.state).toBe('hasError'); expect(loadable.contents).toBeInstanceOf(Error); expect(() => loadable.getValue()).toThrow('ERROR'); // eslint-disable-next-line jest/valid-expect promise = expect(loadable.toPromise()).rejects.toBeInstanceOf(Error); expect(loadable.valueMaybe()).toBe(undefined); expect(() => loadable.valueOrThrow()).toThrow(Error); expect(String(loadable.errorMaybe() ?? {})).toContain('ERROR'); expect(loadable.errorOrThrow()).toBeInstanceOf(Error); expect(String(loadable.errorOrThrow())).toContain('ERROR'); expect(loadable.promiseMaybe()).toBe(undefined); expect(() => loadable.promiseOrThrow()).toThrow(Error); return 'VALUE'; } const c = renderElements(); expect(c.textContent).toEqual('VALUE'); await promise; }); testRecoil('useRecoilValueLoadable - loading loadable', async () => { const [valueSel, resolve] = asyncSelector(); let resolved = false; const promises = []; function ReadLoadable() { const loadable = useRecoilValueLoadable(valueSel); if (!resolved) { expect(loadable.state).toBe('loading'); expect(loadable.contents).toBeInstanceOf(Promise); expect(() => loadable.getValue()).toThrow(); try { loadable.getValue(); } catch (promise) { promises.push(promise); } promises.push(loadable.toPromise()); expect(loadable.valueMaybe()).toBe(undefined); expect(() => loadable.valueOrThrow()).toThrow(Error); expect(loadable.errorMaybe()).toBe(undefined); expect(() => loadable.errorOrThrow()).toThrow(Error); expect(loadable.promiseMaybe()).toBeInstanceOf(Promise); promises.push(loadable.promiseMaybe()); return 'LOADING'; } else { expect(loadable.state).toBe('hasValue'); expect(loadable.contents).toBe('VALUE'); expect(loadable.getValue()).toBe('VALUE'); promises.push(loadable.toPromise()); expect(loadable.valueMaybe()).toBe('VALUE'); expect(loadable.valueOrThrow()).toBe('VALUE'); expect(loadable.errorMaybe()).toBe(undefined); expect(() => loadable.errorOrThrow()).toThrow(Error); expect(loadable.promiseMaybe()).toBe(undefined); expect(() => loadable.promiseOrThrow()).toThrow(Error); return loadable.valueOrThrow(); } } const c = renderElements(); expect(c.textContent).toEqual('LOADING'); resolve('VALUE'); resolved = true; act(() => jest.runAllTimers()); expect(c.textContent).toEqual('VALUE'); await Promise.all( promises.map(async promise => { if (!(promise instanceof Promise)) { // for flow throw new Error('Expected a promise'); } const res = await promise; const val = typeof res === 'string' ? res : res.__value; expect(val).toBe('VALUE'); }), ); }); testRecoil( 'useRecoilValueLoadable() with an async throwing selector results in a loadable in error state', async () => { const asyncError = selector({ key: 'asyncError', get: async () => { throw new Error('Test Error'); }, }); const Test = () => { const loadable = useRecoilValueLoadable(asyncError); return (

{loadable?.state === 'hasError' ? 'Has error' : 'No error'}

); }; const c = renderElements(); await act(() => flushPromisesAndTimers()); expect(c.textContent).toEqual('Has error'); }, ); // Test that an async selector can depend on an async selector dependency // and include async post-processing. testRecoil('two level async', async () => { const level2 = selector({ key: 'useRecoilValueLoadable async level2', get: () => new Promise(resolve => setTimeout(() => resolve('level2'))), }); const level1 = selector({ key: 'useRecoilValueLoadable async level1', // $FlowFixMe[missing-local-annot] get: async ({get}) => { const level2Value = get(level2); return await new Promise(resolve => setTimeout(() => resolve(`level1 + ${level2Value}`)), ); }, }); const promises = []; function ReadPromise() { const loadable = useRecoilValueLoadable(level1); promises.push(loadable.toPromise()); // $FlowFixMe[incompatible-type] return loadable.getValue(); } // $FlowFixMe[incompatible-type-arg] const c = renderElements(); expect(c.textContent).toEqual('loading'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(c.textContent).toEqual('level1 + level2'); await Promise.all( promises.map(promise => expect(promise).resolves.toBe('level1 + level2')), ); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useTransactionObservation_DEPRECATED-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { RecoilState, RecoilValue, RecoilValueReadOnly, } from '../../core/Recoil_RecoilValue'; import type {PersistenceSettings} from '../../recoil_values/Recoil_atom'; import type {NodeKey} from 'Recoil_Keys'; import type {Node} from 'react'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useState, act, atom, selector, ReadsAtom, flushPromisesAndTimers, renderElements, renderUnwrappedElements, useRecoilState, useRecoilValue, useSetRecoilState, useSetUnvalidatedAtomValues, useTransactionObservation_DEPRECATED; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); ({act} = require('ReactTestUtils')); atom = require('../../recoil_values/Recoil_atom'); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, flushPromisesAndTimers, renderElements, renderUnwrappedElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({ useRecoilState, useRecoilValue, useSetRecoilState, useSetUnvalidatedAtomValues, } = require('../Recoil_Hooks')); ({useTransactionObservation_DEPRECATED} = require('../Recoil_SnapshotHooks')); }); let nextID = 0; function counterAtom(persistence?: PersistenceSettings) { return atom({ key: `atom${nextID++}`, default: 0, persistence_UNSTABLE: persistence, }); } function plusOneSelector(dep: RecoilValue) { const fn = jest.fn(x => x + 1); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => fn(get(dep)), }); return [sel, fn]; } function plusOneAsyncSelector( dep: RecoilValue, ): [RecoilValueReadOnly, (number) => void] { let nextTimeoutAmount = 100; const fn = jest.fn(x => { return new Promise(resolve => { setTimeout(() => { resolve(x + 1); }, nextTimeoutAmount); }); }); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => fn(get(dep)), }); return [ sel, x => { nextTimeoutAmount = x; }, ]; } function componentThatWritesAtom( recoilState: RecoilState, // flowlint-next-line unclear-type:off ): [any, ((T => T) | T) => void] { let updateValue; const Component = jest.fn(() => { updateValue = useSetRecoilState(recoilState); return null; }); // flowlint-next-line unclear-type:off return [(Component: any), x => updateValue(x)]; } /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function ObservesTransactions({fn}) { useTransactionObservation_DEPRECATED(fn); return null; } testRecoil( 'useTransactionObservation_DEPRECATED: Transaction dirty atoms are set', async () => { const anAtom = counterAtom({ type: 'url', validator: x => (x: any), // flowlint-line unclear-type:off }); const [aSelector, _] = plusOneSelector(anAtom); const [anAsyncSelector, __] = plusOneAsyncSelector(aSelector); const [Component, updateValue] = componentThatWritesAtom(anAtom); const modifiedAtomsList = []; renderElements( <> { modifiedAtomsList.push(modifiedAtoms); }} /> , ); await flushPromisesAndTimers(); await flushPromisesAndTimers(); act(() => updateValue(1)); await flushPromisesAndTimers(); expect(modifiedAtomsList.length).toBe(3); expect(modifiedAtomsList[1].size).toBe(1); expect(modifiedAtomsList[1].has(anAtom.key)).toBe(true); for (const modifiedAtoms of modifiedAtomsList) { expect(modifiedAtoms.has(aSelector.key)).toBe(false); expect(modifiedAtoms.has(anAsyncSelector.key)).toBe(false); } }, ); testRecoil( 'Can restore persisted values before atom def code is loaded', () => { let theAtom = null; let setUnvalidatedAtomValues; function SetsUnvalidatedAtomValues() { setUnvalidatedAtomValues = useSetUnvalidatedAtomValues(); return null; } let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(false); setVisible = mySetVisible; return visible ? children : null; } function MyReadsAtom({ getAtom, }: $TEMPORARY$object<{getAtom: () => null | RecoilState}>) { const [value] = useRecoilState((getAtom(): any)); // flowlint-line unclear-type:off return value; } const container = renderElements( <> theAtom} /> , ); act(() => { setUnvalidatedAtomValues( new Map().set('notDefinedYetAtom', 123), ); }); const validator = jest.fn(() => 789); // $FlowFixMe[incompatible-call] theAtom = atom({ key: 'notDefinedYetAtom', default: 456, persistence_UNSTABLE: { type: 'url', validator, }, }); act(() => { setVisible(true); }); // $FlowFixMe[invalid-tuple-index] expect(validator.mock.calls[0][0]).toBe(123); expect(container.textContent).toBe('789'); }, ); testRecoil( 'useTransactionObservation_DEPRECATED: Nonvalidated atoms are included in transaction observation', () => { const anAtom = counterAtom({ type: 'url', validator: x => (x: any), // flowlint-line unclear-type:off }); const [Component, updateValue] = componentThatWritesAtom(anAtom); let setUnvalidatedAtomValues; function SetsUnvalidatedAtomValues() { setUnvalidatedAtomValues = useSetUnvalidatedAtomValues(); return null; } let values: Map = new Map(); renderElements( <> { values = atomValues; }} /> , ); act(() => { setUnvalidatedAtomValues( new Map().set('someNonvalidatedAtom', 123), ); }); values = new Map(); act(() => updateValue(1)); expect(values.size).toBe(2); expect(values.get('someNonvalidatedAtom')).toBe(123); }, ); testRecoil('Hooks cannot be used outside of RecoilRoot', () => { const myAtom = atom({key: 'hook outside RecoilRoot', default: 'INVALID'}); function Test() { useRecoilValue(myAtom); return 'TEST'; } // Make sure there is a friendly error message mentioning expect(() => renderUnwrappedElements()).toThrow(''); }); ================================================ FILE: packages/recoil/hooks/__tests__/Recoil_useTransition-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useState, useTransition, act, useRecoilValue, useRecoilState, useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, useRecoilState_TRANSITION_SUPPORT_UNSTABLE, atom, selectorFamily, renderElements, reactMode, flushPromisesAndTimers; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState, useTransition} = React); ({act} = require('ReactTestUtils')); ({ useRecoilValue, useRecoilState, useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, useRecoilState_TRANSITION_SUPPORT_UNSTABLE, atom, selectorFamily, } = require('../../Recoil_index')); ({ renderElements, flushPromisesAndTimers, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({reactMode} = require('../../core/Recoil_ReactMode')); }); let nextID = 0; testRecoil('Works with useTransition', async ({concurrentMode}) => { if (!reactMode().concurrent || !concurrentMode) { return; } const indexAtom = atom({ key: `index${nextID++}`, default: 0, }); // Basic implementation of a cache that suspends: const cache = new Map< number, | {promise: null, state: string, value: string} | {promise: Promise, state: string, value: null}, >(); const resolvers = []; function getItem(index: number) { if (cache.has(index) && cache.get(index)?.state === 'ready') { return cache.get(index)?.value; } else if (cache.has(index)) { throw cache.get(index)?.promise; } else { const promise = new Promise(resolve => { const onComplete = () => { cache.set(index, { state: 'ready', value: `v${index}`, promise: null, }); resolve(); }; resolvers.push(onComplete); }); const newEntry = { state: 'loading', value: null, promise, }; // $FlowFixMe[incompatible-call] cache.set(index, newEntry); throw promise; } } function ItemContents({index}: $TEMPORARY$object<{index: number}>) { const item = getItem(index); return (
Item {index} = {item}
); } function Item({index}: $TEMPORARY$object<{index: number}>) { return ( ); } let incrementIndex; function Main() { const [index, setIndex] = useRecoilState(indexAtom); const [isPending, startTransition] = useTransition(); incrementIndex = () => { startTransition(() => { setIndex(x => x + 1); }); }; return (
Index: {index} - {isPending && 'In transition - '}
); } const c = renderElements(
); // Initial load: expect(c.textContent).toEqual('Index: 0 - Suspended'); act(() => resolvers[0]()); await flushPromisesAndTimers(); expect(c.textContent).toEqual('Index: 0 - Item 0 = v0'); // Increment index a single time; see previous item in transition, then once // the new item resolves, see the new item: act(() => incrementIndex()); expect(c.textContent).toEqual('Index: 0 - In transition - Item 0 = v0'); act(() => resolvers[1]()); await flushPromisesAndTimers(); expect(c.textContent).toEqual('Index: 1 - Item 1 = v1'); // Increment index a second time during transition; see previous item in // transition, then once the new _second_ item resolves, see that new item: act(() => incrementIndex()); expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1'); act(() => incrementIndex()); expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1'); act(() => resolvers[2]()); await flushPromisesAndTimers(); expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1'); act(() => resolvers[3]()); await flushPromisesAndTimers(); expect(c.textContent).toEqual('Index: 3 - Item 3 = v3'); }); testRecoil('useRecoilValue()', async ({concurrentMode}) => { if (useTransition == null) { return; } const myAtom = atom({key: 'useRecoilValue atom', default: 0}); let resolvers: Array<(result: Promise | string) => void> = []; function resolveSelectors() { resolvers.forEach(resolve => resolve('RESOLVED')); resolvers = []; } const query = selectorFamily({ key: 'useRecoilValue selector', get: ( // $FlowFixMe[missing-local-annot] param, ) => // $FlowFixMe[missing-local-annot] ({get}) => { const value = get(myAtom); return new Promise(resolve => { resolvers.push(resolve); // $FlowFixMe[incompatible-type] }).then(str => `${param} ${value} ${str}`); }, }); function Component({index}: $TEMPORARY$object<{index: number}>) { const value = useRecoilValue(query(index)); return ( <> {index} {value} ); } let startReactTransition, startRecoilTransition, startBothTransition; function Main() { const [reactState, setReactState] = useState(0); const [recoilState, setRecoilState] = useRecoilState(myAtom); const [inTransition, startTransition] = useTransition(); startReactTransition = () => { startTransition(() => { setReactState(x => x + 1); }); }; startRecoilTransition = () => { startTransition(() => { setRecoilState(x => x + 1); }); }; startBothTransition = () => { startTransition(() => { setReactState(x => x + 1); setRecoilState(x => x + 1); }); }; return ( <> React:{reactState} Recoil:{recoilState}{' '} {inTransition ? '[IN TRANSITION] ' : ''}|{' '} ); } const c = renderElements(
); expect(c.textContent).toBe('React:0 Recoil:0 | LOADING'); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:0 Recoil:0 | 0 0 0 RESOLVED'); // Transition changing React State act(startReactTransition); expect(c.textContent).toBe( concurrentMode ? 'React:0 Recoil:0 [IN TRANSITION] | 0 0 0 RESOLVED' : 'React:1 Recoil:0 | LOADING', ); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:1 Recoil:0 | 1 1 0 RESOLVED'); // Transition changing Recoil State act(startRecoilTransition); expect(c.textContent).toBe( concurrentMode && reactMode().concurrent ? 'React:1 Recoil:0 [IN TRANSITION] | 1 1 0 RESOLVED' : 'React:1 Recoil:1 | LOADING', ); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:1 Recoil:1 | 1 1 1 RESOLVED'); // Second transition changing Recoil State act(startRecoilTransition); expect(c.textContent).toBe( concurrentMode && reactMode().concurrent ? 'React:1 Recoil:1 [IN TRANSITION] | 1 1 1 RESOLVED' : 'React:1 Recoil:2 | LOADING', ); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:1 Recoil:2 | 1 1 2 RESOLVED'); // Transition with both React and Recoil state act(startBothTransition); expect(c.textContent).toBe( concurrentMode && reactMode().concurrent ? 'React:1 Recoil:2 [IN TRANSITION] | 1 1 2 RESOLVED' : 'React:2 Recoil:3 | LOADING', ); act(resolveSelectors); await flushPromisesAndTimers(); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:2 Recoil:3 | 2 2 3 RESOLVED'); }); testRecoil( 'useRecoilValue_TRANSITION_SUPPORT_UNSTABLE()', async ({concurrentMode}) => { if (useTransition == null) { return; } const myAtom = atom({key: 'TRANSITION_SUPPORT_UNSTABLE atom', default: 0}); let resolvers: Array<(result: Promise | string) => void> = []; function resolveSelectors() { resolvers.forEach(resolve => resolve('RESOLVED')); resolvers = []; } const query = selectorFamily({ key: 'TRANSITION_SUPPORT_UNSTABLE selector', get: ( // $FlowFixMe[missing-local-annot] param, ) => // $FlowFixMe[missing-local-annot] ({get}) => { const value = get(myAtom); return new Promise(resolve => { resolvers.push(resolve); // $FlowFixMe[incompatible-type] }).then(str => `${param} ${value} ${str}`); }, }); function Component({index}: $TEMPORARY$object<{index: number}>) { const value = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(query(index)); return ( <> {index} {value} ); } let startReactTransition, startRecoilTransition, startBothTransition; function Main() { const [reactState, setReactState] = useState(0); const [recoilState, setRecoilState] = useRecoilState_TRANSITION_SUPPORT_UNSTABLE(myAtom); const [inTransition, startTransition] = useTransition(); startReactTransition = () => { startTransition(() => { setReactState(x => x + 1); }); }; startRecoilTransition = () => { startTransition(() => { setRecoilState(x => x + 1); }); }; startBothTransition = () => { startTransition(() => { setReactState(x => x + 1); setRecoilState(x => x + 1); }); }; return ( <> React:{reactState} Recoil:{recoilState}{' '} {inTransition ? '[IN TRANSITION] ' : ''}|{' '} ); } const c = renderElements(
); expect(c.textContent).toBe('React:0 Recoil:0 | LOADING'); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:0 Recoil:0 | 0 0 0 RESOLVED'); // Transition changing React State act(startReactTransition); expect(c.textContent).toBe( concurrentMode ? 'React:0 Recoil:0 [IN TRANSITION] | 0 0 0 RESOLVED' : 'React:1 Recoil:0 | LOADING', ); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:1 Recoil:0 | 1 1 0 RESOLVED'); // Transition changing Recoil State act(startRecoilTransition); expect(c.textContent).toBe( concurrentMode && reactMode().early ? 'React:1 Recoil:0 [IN TRANSITION] | 1 1 0 RESOLVED' : 'React:1 Recoil:1 | LOADING', ); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:1 Recoil:1 | 1 1 1 RESOLVED'); // Second transition changing Recoil State act(startRecoilTransition); expect(c.textContent).toBe( concurrentMode && reactMode().early ? 'React:1 Recoil:1 [IN TRANSITION] | 1 1 1 RESOLVED' : 'React:1 Recoil:2 | LOADING', ); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:1 Recoil:2 | 1 1 2 RESOLVED'); // Transition with both React and Recoil State act(startBothTransition); expect(c.textContent).toBe( concurrentMode ? 'React:1 Recoil:2 [IN TRANSITION] | 1 1 2 RESOLVED' : 'React:2 Recoil:3 | LOADING', ); act(resolveSelectors); await flushPromisesAndTimers(); act(resolveSelectors); await flushPromisesAndTimers(); expect(c.textContent).toBe('React:2 Recoil:3 | 2 2 3 RESOLVED'); }, ); ================================================ FILE: packages/recoil/package-for-release.json ================================================ { "name": "recoil", "version": "0.7.7", "description": "Recoil - A state management library for React", "main": "cjs/index.js", "module": "es/index.js", "react-native": "native/index.js", "unpkg": "umd/index.js", "types": "index.d.ts", "files": ["umd", "es", "cjs", "native", "index.d.ts"], "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT", "dependencies": { "hamt_plus": "1.0.2" }, "peerDependencies": { "react": ">=16.13.1" }, "peerDependenciesMeta": { "react-dom": { "optional": true }, "react-native": { "optional": true } } } ================================================ FILE: packages/recoil/package.json ================================================ { "name": "recoil-oss", "description": "This is the internal package.json enabling CommonJS module", "main": "Recoil_index.js", "haste_commonjs": true, "files": [ "Recoil_index.js" ], "directories": { "": "./" }, "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT" } ================================================ FILE: packages/recoil/recoil_values/Recoil_WaitFor.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type { RecoilValue, RecoilValueReadOnly, } from '../core/Recoil_RecoilValue'; import type {GetRecoilValue} from './Recoil_callbackTypes'; const { loadableWithError, loadableWithPromise, loadableWithValue, } = require('../adt/Recoil_Loadable'); const selector = require('./Recoil_selector'); const selectorFamily = require('./Recoil_selectorFamily'); const isPromise = require('recoil-shared/util/Recoil_isPromise'); ///////////////// // TRUTH TABLE ///////////////// // Dependencies waitForNone waitForAny waitForAll waitForAllSettled // [loading, loading] [Promise, Promise] Promise Promise Promise // [value, loading] [value, Promise] [value, Promise] Promise Promise // [value, value] [value, value] [value, value] [value, value] [value, value] // // [error, loading] [Error, Promise] [Error, Promise] Error Promise // [error, error] [Error, Error] [Error, Error] Error [error, error] // [value, error] [value, Error] [value, Error] Error [value, error] // Issue parallel requests for all dependencies and return the current // status if they have results, have some error, or are still pending. function concurrentRequests( getRecoilValue: GetRecoilValue, deps: $ReadOnlyArray>, ) { const results = Array(deps.length).fill(undefined); const exceptions = Array(deps.length).fill(undefined); for (const [i, dep] of deps.entries()) { try { results[i] = getRecoilValue(dep); } catch (e) { // exceptions can either be Promises of pending results or real errors exceptions[i] = e; } } return [results, exceptions]; } function isError(exp: $FlowFixMe) { return exp != null && !isPromise(exp); } function unwrapDependencies( dependencies: | $ReadOnlyArray> | {+[string]: RecoilValueReadOnly}, ): $ReadOnlyArray> { return Array.isArray(dependencies) ? dependencies : Object.getOwnPropertyNames(dependencies).map(key => dependencies[key]); } function wrapResults( dependencies: | $ReadOnlyArray> | {+[string]: RecoilValueReadOnly}, /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ results, ) { return Array.isArray(dependencies) ? results : // Object.getOwnPropertyNames() has consistent key ordering with ES6 Object.getOwnPropertyNames(dependencies).reduce( (out, key, idx) => ({...out, [(key: string)]: results[idx]}), {}, ); } function wrapLoadables( dependencies: | $ReadOnlyArray> | {+[string]: RecoilValueReadOnly}, results: Array<$FlowFixMe>, exceptions: Array<$FlowFixMe>, ) { const output = exceptions.map((exception, idx) => exception == null ? loadableWithValue(results[idx]) : isPromise(exception) ? loadableWithPromise(exception) : loadableWithError(exception), ); return wrapResults(dependencies, output); } function combineAsyncResultsWithSyncResults( syncResults: Array, asyncResults: Array, ): Array { return asyncResults.map((result, idx) => /** * it's important we use === undefined as opposed to == null, because the * resolved value of the async promise could be `null`, in which case we * don't want to use syncResults[idx], which would be undefined. If async * promise resolves to `undefined`, that's ok because `syncResults[idx]` * will also be `undefined`. That's a little hacky, but it works. */ result === undefined ? syncResults[idx] : result, ); } // Selector that requests all dependencies in parallel and immediately returns // current results without waiting. const waitForNone: < RecoilValues: | $ReadOnlyArray> | $ReadOnly<{[string]: RecoilValueReadOnly, ...}>, >( RecoilValues, ) => RecoilValueReadOnly< $ReadOnlyArray> | $ReadOnly<{[string]: Loadable, ...}>, > = selectorFamily({ key: '__waitForNone', get: ( dependencies: | $ReadOnly<{[string]: RecoilValueReadOnly}> | $ReadOnlyArray>, ) => ({get}) => { // Issue requests for all dependencies in parallel. const deps = unwrapDependencies(dependencies); const [results, exceptions] = concurrentRequests(get, deps); // Always return the current status of the results; never block. return wrapLoadables(dependencies, results, exceptions); }, dangerouslyAllowMutability: true, }); // Selector that requests all dependencies in parallel and waits for at least // one to be available before returning results. It will only error if all // dependencies have errors. const waitForAny: < RecoilValues: | $ReadOnlyArray> | $ReadOnly<{[string]: RecoilValueReadOnly, ...}>, >( RecoilValues, ) => RecoilValueReadOnly< $ReadOnlyArray | $ReadOnly<{[string]: mixed, ...}>, > = selectorFamily({ key: '__waitForAny', get: ( dependencies: | $ReadOnly<{[string]: RecoilValueReadOnly}> | $ReadOnlyArray>, ) => ({get}) => { // Issue requests for all dependencies in parallel. // Exceptions can either be Promises of pending results or real errors const deps = unwrapDependencies(dependencies); const [results, exceptions] = concurrentRequests(get, deps); // If any results are available, value or error, return the current status if (exceptions.some(exp => !isPromise(exp))) { return wrapLoadables(dependencies, results, exceptions); } // Otherwise, return a promise that will resolve when the next result is // available, whichever one happens to be next. But, if all pending // dependencies end up with errors, then reject the promise. return new Promise(resolve => { for (const [i, exp] of exceptions.entries()) { if (isPromise(exp)) { exp .then(result => { results[i] = result; exceptions[i] = undefined; resolve(wrapLoadables(dependencies, results, exceptions)); }) .catch(error => { exceptions[i] = error; resolve(wrapLoadables(dependencies, results, exceptions)); }); } } }); }, dangerouslyAllowMutability: true, }); // Selector that requests all dependencies in parallel and waits for all to be // available before returning a value. It will error if any dependencies error. const waitForAll: < RecoilValues: | $ReadOnlyArray> | $ReadOnly<{[string]: RecoilValueReadOnly, ...}>, >( RecoilValues, ) => RecoilValueReadOnly< $ReadOnlyArray | $ReadOnly<{[string]: mixed, ...}>, > = selectorFamily({ key: '__waitForAll', get: ( dependencies: | $ReadOnly<{[string]: RecoilValueReadOnly}> | $ReadOnlyArray>, ) => ({get}) => { // Issue requests for all dependencies in parallel. // Exceptions can either be Promises of pending results or real errors const deps = unwrapDependencies(dependencies); const [results, exceptions] = concurrentRequests(get, deps); // If all results are available, return the results if (exceptions.every(exp => exp == null)) { return wrapResults(dependencies, results); } // If we have any errors, throw the first error const error = exceptions.find(isError); if (error != null) { throw error; } // Otherwise, return a promise that will resolve when all results are available return Promise.all(exceptions).then(exceptionResults => wrapResults( dependencies, combineAsyncResultsWithSyncResults(results, exceptionResults), ), ); }, dangerouslyAllowMutability: true, }); const waitForAllSettled: < RecoilValues: | $ReadOnlyArray> | $ReadOnly<{[string]: RecoilValueReadOnly, ...}>, >( RecoilValues, ) => RecoilValueReadOnly< $ReadOnlyArray | $ReadOnly<{[string]: mixed, ...}>, > = selectorFamily({ key: '__waitForAllSettled', get: ( dependencies: | $ReadOnly<{[string]: RecoilValueReadOnly}> | $ReadOnlyArray>, ) => ({get}) => { // Issue requests for all dependencies in parallel. // Exceptions can either be Promises of pending results or real errors const deps = unwrapDependencies(dependencies); const [results, exceptions] = concurrentRequests(get, deps); // If all results are available, return the results if (exceptions.every(exp => !isPromise(exp))) { return wrapLoadables(dependencies, results, exceptions); } // Wait for all results to settle return ( Promise.all( exceptions.map((exp, i) => isPromise(exp) ? exp .then(result => { results[i] = result; exceptions[i] = undefined; }) .catch(error => { results[i] = undefined; exceptions[i] = error; }) : null, ), ) // Then wrap them as loadables .then(() => wrapLoadables(dependencies, results, exceptions)) ); }, dangerouslyAllowMutability: true, }); const noWait: (RecoilValue) => RecoilValueReadOnly> = selectorFamily({ key: '__noWait', get: dependency => ({get}) => { try { return selector.value(loadableWithValue(get(dependency))); } catch (exception) { return selector.value( isPromise(exception) ? loadableWithPromise(exception) : loadableWithError(exception), ); } }, dangerouslyAllowMutability: true, }); module.exports = { waitForNone, waitForAny, waitForAll, waitForAllSettled, noWait, }; ================================================ FILE: packages/recoil/recoil_values/Recoil_WaitFor.js.flow ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type { RecoilValue, RecoilValueReadOnly, } from '../core/Recoil_RecoilValue'; type UnwrapArrayRecoilValues = $TupleMap< RecoilValues, (RecoilValue) => T, >; type UnwrapArrayRecoilValueLoadables = $TupleMap< RecoilValues, (RecoilValue) => Loadable, >; type UnwrapObjRecoilValues = $ObjMap< RecoilValues, (RecoilValue) => T, >; type UnwrapObjRecoilValueLoadables = $ObjMap< RecoilValues, (RecoilValue) => Loadable, >; /* eslint-disable no-redeclare */ declare function waitForNone>>( // flowlint-line unclear-type:off RecoilValues, ): RecoilValueReadOnly>; declare function waitForNone< RecoilValues: $ReadOnly<{[string]: RecoilValue, ...}>, // flowlint-line unclear-type:off >( RecoilValues, ): RecoilValueReadOnly>; declare function waitForAny>>( // flowlint-line unclear-type:off RecoilValues, ): RecoilValueReadOnly>; declare function waitForAny< RecoilValues: $ReadOnly<{[string]: RecoilValue, ...}>, // flowlint-line unclear-type:off >( RecoilValues, ): RecoilValueReadOnly>; // waitForAll() does not return placeholder Promises or Errors in the results // as it only returns when all values are available. declare function waitForAll>>( // flowlint-line unclear-type:off RecoilValues, ): RecoilValueReadOnly>; declare function waitForAll< RecoilValues: $ReadOnly<{[string]: RecoilValue, ...}>, // flowlint-line unclear-type:off >( RecoilValues, ): RecoilValueReadOnly>; declare function waitForAllSettled< RecoilValues: $ReadOnlyArray>, // flowlint-line unclear-type:off >( RecoilValues, ): RecoilValueReadOnly>; declare function waitForAllSettled< RecoilValues: $ReadOnly<{[string]: RecoilValue, ...}>, // flowlint-line unclear-type:off >( RecoilValues, ): RecoilValueReadOnly>; /* eslint-enable no-redeclare */ declare function noWait(RecoilValue): RecoilValueReadOnly>; module.exports = { waitForNone, waitForAny, waitForAll, waitForAllSettled, noWait, }; ================================================ FILE: packages/recoil/recoil_values/Recoil_atom.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Returns an atom, the basic unit of state in Recoil. An atom is a reference to * value that can be read, written, and subscribed to. It has a `key` that is * stable across time so that it can be persisted. * * There are two required options for creating an atom: * * key. This is a string that uniquely identifies the atom. It should be * stable across time so that persisted states remain valid. * * default * If `default` is provided, the atom is initialized to that value. * Or, it may be set to another RecoilValue to use as a fallback. * In that case, the value of the atom will be equal to that of the * fallback, and will remain so until the first time the atom is written * to, at which point it will stop tracking the fallback RecoilValue. * * The `persistence` option specifies that the atom should be saved to storage. * It is an object with two properties: `type` specifies where the atom should * be persisted; its only allowed value is "url"; `backButton` specifies whether * changes to the atom should result in pushes to the browser history stack; if * true, changing the atom and then hitting the Back button will cause the atom's * previous value to be restored. Applications are responsible for implementing * persistence by using the `useTransactionObservation` hook. * * Scoped atoms (DEPRECATED): * =================================================================================== * * The scopeRules_APPEND_ONLY_READ_THE_DOCS option causes the atom be be "scoped". * A scoped atom's value depends on other parts of the application state. * A separate value of the atom is stored for every value of the state that it * depends on. The dependencies may be changed without breaking existing URLs -- * it uses whatever rule was current when the URL was written. Values written * under the newer rules are overlaid atop the previously-written values just for * those states in which the write occurred, with reads falling back to the older * values in other states, and eventually to the default or fallback. * * The scopedRules_APPEND_ONLY_READ_THE_DOCS parameter is a list of rules; * it should start with a single entry. This list must only be appended to: * existing entries must never be deleted or modified. Each rule is an atom * or selector whose value is some arbitrary key. A different value of the * scoped atom will be stored for each key. To change the scope rule, simply add * a new function to the list. Each rule is either an array of atoms of primitives, * or an atom of an array of primitives. * * Ordinary atoms may be upgraded to scoped atoms. To un-scope an atom, add a new * scope rule consisting of a constant. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable, LoadingLoadableType} from '../adt/Recoil_Loadable'; import type {RecoilValueInfo} from '../core/Recoil_FunctionalCore'; import type {StoreID} from '../core/Recoil_Keys'; import type { PersistenceInfo, ReadWriteNodeOptions, Trigger, } from '../core/Recoil_Node'; import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue'; import type {RetainedBy} from '../core/Recoil_RetainedBy'; import type {AtomWrites, NodeKey, Store, TreeState} from '../core/Recoil_State'; // @fb-only: import type {ScopeRules} from 'Recoil_ScopedAtom'; // @fb-only: const {scopedAtom} = require('Recoil_ScopedAtom'); const { isLoadable, loadableWithError, loadableWithPromise, loadableWithValue, } = require('../adt/Recoil_Loadable'); const {WrappedValue} = require('../adt/Recoil_Wrapper'); const {peekNodeInfo} = require('../core/Recoil_FunctionalCore'); const { DEFAULT_VALUE, DefaultValue, getConfigDeletionHandler, registerNode, setConfigDeletionHandler, } = require('../core/Recoil_Node'); const {isRecoilValue} = require('../core/Recoil_RecoilValue'); const { getRecoilValueAsLoadable, markRecoilValueModified, setRecoilValue, setRecoilValueLoadable, } = require('../core/Recoil_RecoilValueInterface'); const {retainedByOptionWithDefault} = require('../core/Recoil_Retention'); const selector = require('./Recoil_selector'); const deepFreezeValue = require('recoil-shared/util/Recoil_deepFreezeValue'); const err = require('recoil-shared/util/Recoil_err'); const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation'); const isPromise = require('recoil-shared/util/Recoil_isPromise'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); export type PersistenceSettings = $ReadOnly<{ ...PersistenceInfo, validator: (mixed, DefaultValue) => Stored | DefaultValue, }>; // TODO Support Loadable type NewValue = | T | DefaultValue | Promise | WrappedValue; type NewValueOrUpdater = | T | DefaultValue | Promise | WrappedValue | (( T | DefaultValue, ) => T | DefaultValue | Promise | WrappedValue); // Effect is called the first time a node is used with a export type AtomEffect = ({ node: RecoilState, storeID: StoreID, parentStoreID_UNSTABLE?: StoreID, trigger: Trigger, // Call synchronously to initialize value or async to change it later setSelf: ( | T | DefaultValue | Promise | WrappedValue | (( T | DefaultValue, ) => T | DefaultValue | Promise | WrappedValue), ) => void, resetSelf: () => void, // Subscribe callbacks to events. // Atom effect observers are called before global transaction observers onSet: ( (newValue: T, oldValue: T | DefaultValue, isReset: boolean) => void, ) => void, // Accessors to read other atoms/selectors getPromise: (RecoilValue) => Promise, getLoadable: (RecoilValue) => Loadable, getInfo_UNSTABLE: (RecoilValue) => RecoilValueInfo, }) => void | (() => void); export type AtomOptionsWithoutDefault = $ReadOnly<{ key: NodeKey, effects?: $ReadOnlyArray>, effects_UNSTABLE?: $ReadOnlyArray>, persistence_UNSTABLE?: PersistenceSettings, // @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS?: ScopeRules, dangerouslyAllowMutability?: boolean, retainedBy_UNSTABLE?: RetainedBy, }>; type AtomOptionsWithDefault = $ReadOnly<{ ...AtomOptionsWithoutDefault, default: RecoilValue | Promise | Loadable | WrappedValue | T, }>; export type AtomOptions = | AtomOptionsWithDefault | AtomOptionsWithoutDefault; type BaseAtomOptions = $ReadOnly<{ ...AtomOptions, default: Promise | Loadable | WrappedValue | T, }>; const unwrap = (x: T | S | WrappedValue): T | S => x instanceof WrappedValue ? x.value : x; function baseAtom(options: BaseAtomOptions): RecoilState { const {key, persistence_UNSTABLE: persistence} = options; const retainedBy = retainedByOptionWithDefault(options.retainedBy_UNSTABLE); let liveStoresCount = 0; function unwrapPromise(promise: Promise): Loadable { return loadableWithPromise( promise .then(value => { defaultLoadable = loadableWithValue(value); return value; }) .catch(error => { defaultLoadable = loadableWithError(error); throw error; }), ); } let defaultLoadable: Loadable = isPromise(options.default) ? unwrapPromise(options.default) : isLoadable(options.default) ? options.default.state === 'loading' ? unwrapPromise((options.default: LoadingLoadableType).contents) : options.default : // $FlowFixMe[incompatible-call] loadableWithValue(unwrap(options.default)); // $FlowFixMe[unused-promise](site=www) maybeFreezeValueOrPromise(defaultLoadable.contents); let cachedAnswerForUnvalidatedValue: void | Loadable = undefined; // Cleanup handlers for this atom // Rely on stable reference equality of the store to use it as a key per const cleanupEffectsByStore: Map void>> = new Map(); function maybeFreezeValueOrPromise(valueOrPromise: mixed) { if (__DEV__) { if (options.dangerouslyAllowMutability !== true) { if (isPromise(valueOrPromise)) { return valueOrPromise.then(value => { deepFreezeValue(value); return value; }); } else { deepFreezeValue(valueOrPromise); return valueOrPromise; } } } return valueOrPromise; } function wrapPendingPromise( store: Store, promise: Promise, ): Promise { const wrappedPromise: Promise = promise .then(value => { const state = store.getState().nextTree ?? store.getState().currentTree; if (state.atomValues.get(key)?.contents === wrappedPromise) { markRecoilValueModified(store, node); setRecoilValue(store, node, value); } return value; }) .catch(error => { const state = store.getState().nextTree ?? store.getState().currentTree; if (state.atomValues.get(key)?.contents === wrappedPromise) { markRecoilValueModified(store, node); setRecoilValueLoadable(store, node, loadableWithError(error)); } throw error; }); return wrappedPromise; } function initAtom( store: Store, initState: TreeState, trigger: Trigger, ): () => void { liveStoresCount++; const cleanupAtom = () => { liveStoresCount--; cleanupEffectsByStore.get(store)?.forEach(cleanup => cleanup()); cleanupEffectsByStore.delete(store); }; store.getState().knownAtoms.add(key); // Setup async defaults to notify subscribers when they resolve if (defaultLoadable.state === 'loading') { const notifyDefaultSubscribers = () => { const state = store.getState().nextTree ?? store.getState().currentTree; if (!state.atomValues.has(key)) { markRecoilValueModified(store, node); } }; defaultLoadable.contents.finally(notifyDefaultSubscribers); } /////////////////// // Run Atom Effects /////////////////// const effects = options.effects ?? options.effects_UNSTABLE; if (effects != null) { // This state is scoped by Store, since this is in the initAtom() closure let initValue: NewValue = DEFAULT_VALUE; let isDuringInit = true; let isInitError: boolean = false; let pendingSetSelf: ?{ effect: AtomEffect, value: T | DefaultValue, } = null; function getLoadable(recoilValue: RecoilValue): Loadable { // Normally we can just get the current value of another atom. // But for our own value we need to check if there is a pending // initialized value or get the fallback default value. if (isDuringInit && recoilValue.key === key) { // Cast T to S const retValue: NewValue = (initValue: any); // flowlint-line unclear-type:off return retValue instanceof DefaultValue ? (peekAtom(store, initState): any) // flowlint-line unclear-type:off : isPromise(retValue) ? loadableWithPromise( retValue.then((v: S | DefaultValue): S | Promise => v instanceof DefaultValue ? // Cast T to S (defaultLoadable: any).toPromise() // flowlint-line unclear-type:off : v, ), ) : // $FlowFixMe[incompatible-call] loadableWithValue(retValue); } return getRecoilValueAsLoadable(store, recoilValue); } function getPromise(recoilValue: RecoilValue): Promise { return getLoadable(recoilValue).toPromise(); } function getInfo_UNSTABLE( recoilValue: RecoilValue, ): RecoilValueInfo { const info = peekNodeInfo( store, store.getState().nextTree ?? store.getState().currentTree, recoilValue.key, ); return isDuringInit && recoilValue.key === key && !(initValue instanceof DefaultValue) ? {...info, isSet: true, loadable: getLoadable(recoilValue)} : info; } const setSelf = (effect: AtomEffect) => (valueOrUpdater: NewValueOrUpdater) => { if (isDuringInit) { const currentLoadable = getLoadable(node); const currentValue: T | DefaultValue = currentLoadable.state === 'hasValue' ? currentLoadable.contents : DEFAULT_VALUE; initValue = typeof valueOrUpdater === 'function' ? // cast to any because we can't restrict T from being a function without losing support for opaque types (valueOrUpdater: any)(currentValue) // flowlint-line unclear-type:off : valueOrUpdater; if (isPromise(initValue)) { initValue = initValue.then(value => { // Avoid calling onSet() when setSelf() initializes with a Promise pendingSetSelf = {effect, value}; return value; }); } } else { if (isPromise(valueOrUpdater)) { throw err('Setting atoms to async values is not implemented.'); } if (typeof valueOrUpdater !== 'function') { pendingSetSelf = { effect, value: unwrap(valueOrUpdater), }; } setRecoilValue( store, node, typeof valueOrUpdater === 'function' ? (currentValue: $FlowFixMe) => { const updatedValue = // cast to any because we can't restrict T from being a function without losing support for opaque types (valueOrUpdater: any)(currentValue); // flowlint-line unclear-type:off if (isPromise(updatedValue)) { throw err( 'Setting atoms to async values is not yet implemented.', ); } const newValue = unwrap(updatedValue); // $FlowFixMe[incompatible-type] pendingSetSelf = {effect, value: newValue}; return newValue; } : unwrap(valueOrUpdater), ); } }; const resetSelf = (effect: AtomEffect) => () => setSelf(effect)(DEFAULT_VALUE); const onSet = (effect: AtomEffect) => (handler: (T, T | DefaultValue, boolean) => void) => { const {release} = store.subscribeToTransactions(currentStore => { // eslint-disable-next-line prefer-const let {currentTree, previousTree} = currentStore.getState(); if (!previousTree) { recoverableViolation( 'Transaction subscribers notified without a next tree being present -- this is a bug in Recoil', 'recoil', ); previousTree = currentTree; // attempt to trundle on } const newLoadable = currentTree.atomValues.get(key) ?? defaultLoadable; if (newLoadable.state === 'hasValue') { const newValue: T = newLoadable.contents; const oldLoadable = previousTree.atomValues.get(key) ?? defaultLoadable; const oldValue: T | DefaultValue = oldLoadable.state === 'hasValue' ? oldLoadable.contents : DEFAULT_VALUE; // TODO This isn't actually valid, use as a placeholder for now. // Ignore atom value changes that were set via setSelf() in the same effect. // We will still properly call the handler if there was a subsequent // set from something other than an atom effect which was batched // with the `setSelf()` call. However, we may incorrectly ignore // the handler if the subsequent batched call happens to set the // atom to the exact same value as the `setSelf()`. But, in that // case, it was kind of a noop, so the semantics are debatable.. if ( pendingSetSelf?.effect !== effect || pendingSetSelf?.value !== newValue ) { handler(newValue, oldValue, !currentTree.atomValues.has(key)); } else if (pendingSetSelf?.effect === effect) { pendingSetSelf = null; } } }, key); cleanupEffectsByStore.set(store, [ ...(cleanupEffectsByStore.get(store) ?? []), release, ]); }; for (const effect of effects) { try { const cleanup = effect({ node, storeID: store.storeID, parentStoreID_UNSTABLE: store.parentStoreID, trigger, setSelf: setSelf(effect), resetSelf: resetSelf(effect), onSet: onSet(effect), getPromise, getLoadable, getInfo_UNSTABLE, }); if (cleanup != null) { cleanupEffectsByStore.set(store, [ ...(cleanupEffectsByStore.get(store) ?? []), cleanup, ]); } } catch (error) { initValue = error; isInitError = true; } } isDuringInit = false; // Mutate initial state in place since we know there are no other subscribers // since we are the ones initializing on first use. if (!(initValue instanceof DefaultValue)) { const initLoadable = isInitError ? loadableWithError<$FlowFixMe>(initValue) : isPromise(initValue) ? loadableWithPromise(wrapPendingPromise(store, initValue)) : loadableWithValue(unwrap(initValue)); // $FlowFixMe[unused-promise](site=www) maybeFreezeValueOrPromise(initLoadable.contents); initState.atomValues.set(key, initLoadable); // If there is a pending transaction, then also mutate the next state tree. // This could happen if the atom was first initialized in an action that // also updated some other atom's state. store.getState().nextTree?.atomValues.set(key, initLoadable); } } return cleanupAtom; } function peekAtom(_store: Store, state: TreeState): Loadable { return ( state.atomValues.get(key) ?? cachedAnswerForUnvalidatedValue ?? defaultLoadable ); } function getAtom(_store: Store, state: TreeState): Loadable { if (state.atomValues.has(key)) { // Atom value is stored in state: return nullthrows(state.atomValues.get(key)); } else if (state.nonvalidatedAtoms.has(key)) { // Atom value is stored but needs validation before use. // We might have already validated it and have a cached validated value: if (cachedAnswerForUnvalidatedValue != null) { return cachedAnswerForUnvalidatedValue; } if (persistence == null) { expectationViolation( `Tried to restore a persisted value for atom ${key} but it has no persistence settings.`, ); return defaultLoadable; } const nonvalidatedValue = state.nonvalidatedAtoms.get(key); const validatorResult: T | DefaultValue = persistence.validator( nonvalidatedValue, DEFAULT_VALUE, ); const validatedValueLoadable = validatorResult instanceof DefaultValue ? defaultLoadable : loadableWithValue(validatorResult); cachedAnswerForUnvalidatedValue = validatedValueLoadable; return cachedAnswerForUnvalidatedValue; } else { return defaultLoadable; } } function invalidateAtom() { cachedAnswerForUnvalidatedValue = undefined; } function setAtom( _store: Store, state: TreeState, newValue: T | DefaultValue, ): AtomWrites { // Bail out if we're being set to the existing value, or if we're being // reset but have no stored value (validated or unvalidated) to reset from: if (state.atomValues.has(key)) { const existing = nullthrows(state.atomValues.get(key)); if (existing.state === 'hasValue' && newValue === existing.contents) { return new Map(); } } else if ( !state.nonvalidatedAtoms.has(key) && newValue instanceof DefaultValue ) { return new Map(); } // $FlowFixMe[unused-promise](site=www) maybeFreezeValueOrPromise(newValue); cachedAnswerForUnvalidatedValue = undefined; // can be released now if it was previously in use return new Map>().set( key, loadableWithValue(newValue), ); } function shouldDeleteConfigOnReleaseAtom() { return getConfigDeletionHandler(key) !== undefined && liveStoresCount <= 0; } const node = registerNode( ({ key, nodeType: 'atom', peek: peekAtom, get: getAtom, set: setAtom, init: initAtom, invalidate: invalidateAtom, shouldDeleteConfigOnRelease: shouldDeleteConfigOnReleaseAtom, dangerouslyAllowMutability: options.dangerouslyAllowMutability, persistence_UNSTABLE: options.persistence_UNSTABLE ? { type: options.persistence_UNSTABLE.type, backButton: options.persistence_UNSTABLE.backButton, } : undefined, shouldRestoreFromSnapshots: true, retainedBy, }: ReadWriteNodeOptions), ); return node; } // prettier-ignore function atom(options: AtomOptions): RecoilState { if (__DEV__) { if (typeof options.key !== 'string') { throw err( 'A key option with a unique string value must be provided when creating an atom.', ); } } const { // @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS, ...restOptions } = options; const optionsDefault: RecoilValue | Promise | Loadable | WrappedValue | T = 'default' in options ? // $FlowIssue[incompatible-type] No way to refine in Flow that property is not defined options.default : new Promise(() => {}); if (isRecoilValue(optionsDefault) // Continue to use atomWithFallback for promise defaults for scoped atoms // for now, since scoped atoms don't support async defaults // @fb-only: || (isPromise(optionsDefault) && scopeRules_APPEND_ONLY_READ_THE_DOCS) // @fb-only: || (isLoadable(optionsDefault) && scopeRules_APPEND_ONLY_READ_THE_DOCS) ) { return atomWithFallback({ ...restOptions, default: optionsDefault, // @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS, }); // @fb-only: } else if (scopeRules_APPEND_ONLY_READ_THE_DOCS // @fb-only: && !isPromise(optionsDefault) // @fb-only: && !isLoadable(optionsDefault) // @fb-only: ) { // @fb-only: return scopedAtom({ // @fb-only: ...restOptions, // @fb-only: default: unwrap(optionsDefault), // @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS, // @fb-only: }); } else { return baseAtom({...restOptions, default: optionsDefault}); } } type AtomWithFallbackOptions = $ReadOnly<{ ...AtomOptions, default: RecoilValue | Promise | Loadable, }>; function atomWithFallback( options: AtomWithFallbackOptions, ): RecoilState { const base = atom({ ...options, default: DEFAULT_VALUE, persistence_UNSTABLE: options.persistence_UNSTABLE === undefined ? undefined : { ...options.persistence_UNSTABLE, validator: (storedValue: mixed) => storedValue instanceof DefaultValue ? storedValue : nullthrows(options.persistence_UNSTABLE).validator( storedValue, DEFAULT_VALUE, ), }, // TODO Hack for now. effects: (options.effects: any), // flowlint-line unclear-type: off effects_UNSTABLE: (options.effects_UNSTABLE: any), // flowlint-line unclear-type: off }); const sel = selector({ key: `${options.key}__withFallback`, get: ({get}) => { const baseValue = get(base); return baseValue instanceof DefaultValue ? options.default : baseValue; }, set: ({set}, newValue) => set(base, newValue), // This selector does not need to cache as it is a wrapper selector // and the selector within the wrapper selector will have a cache // option by default cachePolicy_UNSTABLE: { eviction: 'most-recent', }, dangerouslyAllowMutability: options.dangerouslyAllowMutability, }); setConfigDeletionHandler(sel.key, getConfigDeletionHandler(options.key)); return sel; } // $FlowFixMe[missing-local-annot] atom.value = value => new WrappedValue(value); // $FlowFixMe[incompatible-exact] module.exports = (atom: { (AtomOptions): RecoilState, value: (S) => WrappedValue, }); ================================================ FILE: packages/recoil/recoil_values/Recoil_atomFamily.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type {WrappedValue} from '../adt/Recoil_Wrapper'; import type {CachePolicyWithoutEviction} from '../caches/Recoil_CachePolicy'; import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue'; import type {RetainedBy} from '../core/Recoil_RetainedBy'; import type {AtomEffect, AtomOptionsWithoutDefault} from './Recoil_atom'; // @fb-only: import type {ScopeRules} from 'Recoil_ScopedAtom'; const cacheFromPolicy = require('../caches/Recoil_cacheFromPolicy'); const {setConfigDeletionHandler} = require('../core/Recoil_Node'); const atom = require('./Recoil_atom'); const stableStringify = require('recoil-shared/util/Recoil_stableStringify'); type Primitive = void | null | boolean | number | string; interface HasToJSON { toJSON(): Parameter; } export type Parameter = | Primitive | HasToJSON | $ReadOnlyArray | $ReadOnly<{[string]: Parameter}> | $ReadOnlySet | $ReadOnlyMap; // flowlint unclear-type:off export type ParameterizedScopeRules

= $ReadOnlyArray< | RecoilValue<$ReadOnlyArray> | $ReadOnlyArray | (P => RecoilValue)>, >; // flowlint unclear-type:error export type AtomFamilyOptionsWithoutDefault = $ReadOnly<{ ...AtomOptionsWithoutDefault, effects?: | $ReadOnlyArray> | (P => $ReadOnlyArray>), // effects_UNSTABLE?: // | $ReadOnlyArray> // | (P => $ReadOnlyArray>), retainedBy_UNSTABLE?: RetainedBy | (P => RetainedBy), cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction, // @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS?: ParameterizedScopeRules

, }>; export type AtomFamilyOptions = | $ReadOnly<{ ...AtomFamilyOptionsWithoutDefault, default: | RecoilValue | Promise | Loadable | WrappedValue | T | (P => | T | RecoilValue | Promise | Loadable | WrappedValue), }> | AtomFamilyOptionsWithoutDefault; // Process scopeRules to handle any entries which are functions taking parameters // prettier-ignore // @fb-only: function mapScopeRules

( // @fb-only: scopeRules?: ParameterizedScopeRules

, // @fb-only: param: P, // @fb-only: ): ScopeRules | void { // @fb-only: return scopeRules?.map(rule => // @fb-only: Array.isArray(rule) // @fb-only: ? rule.map(entry => (typeof entry === 'function' ? entry(param) : entry)) // @fb-only: : rule, // @fb-only: ); // @fb-only: } /* A function which returns an atom based on the input parameter. Each unique parameter returns a unique atom. E.g., const f = atomFamily(...); f({a: 1}) => an atom f({a: 2}) => a different atom This allows components to persist local, private state using atoms. Each instance of the component may have a different key, which it uses as the parameter for a family of atoms; in this way, each component will have its own atom not shared by other instances. These state keys may be composed into children's state keys as well. */ function atomFamily( options: AtomFamilyOptions, ): P => RecoilState { const atomCache = cacheFromPolicy>({ equality: options.cachePolicyForParams_UNSTABLE?.equality ?? 'value', eviction: 'keep-all', }); // Simple atomFamily implementation to cache individual atoms based // on the parameter value equality. return (params: P) => { const cachedAtom = atomCache.get(params); if (cachedAtom != null) { return cachedAtom; } const {cachePolicyForParams_UNSTABLE, ...atomOptions} = options; const optionsDefault: | RecoilValue | Promise | Loadable | WrappedValue | T | (P => T | RecoilValue | Promise | Loadable | WrappedValue) = 'default' in options ? // $FlowIssue[incompatible-type] No way to refine in Flow that property is not defined options.default : new Promise(() => {}); const newAtom = atom({ ...atomOptions, key: `${options.key}__${stableStringify(params) ?? 'void'}`, default: typeof optionsDefault === 'function' ? // The default was parameterized // Flow doesn't know that T isn't a function, so we need to case to any // $FlowIssue[incompatible-use] optionsDefault(params) : // Default may be a static value, promise, or RecoilValue optionsDefault, retainedBy_UNSTABLE: typeof options.retainedBy_UNSTABLE === 'function' ? options.retainedBy_UNSTABLE(params) : options.retainedBy_UNSTABLE, effects: typeof options.effects === 'function' ? options.effects(params) : typeof options.effects_UNSTABLE === 'function' ? options.effects_UNSTABLE(params) : options.effects ?? options.effects_UNSTABLE, // prettier-ignore // @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS: mapScopeRules( // @fb-only: options.scopeRules_APPEND_ONLY_READ_THE_DOCS, // @fb-only: params, // @fb-only: ), }); atomCache.set(params, newAtom); setConfigDeletionHandler(newAtom.key, () => { atomCache.delete(params); }); return newAtom; }; } module.exports = atomFamily; ================================================ FILE: packages/recoil/recoil_values/Recoil_callbackTypes.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {DefaultValue} from '../core/Recoil_Node'; import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue'; export type ValueOrUpdater = | T | DefaultValue | ((prevValue: T) => T | DefaultValue); export type GetRecoilValue = (RecoilValue) => T; export type SetRecoilState = (RecoilState, ValueOrUpdater) => void; export type ResetRecoilState = (RecoilState) => void; module.exports = ({}: {...}); ================================================ FILE: packages/recoil/recoil_values/Recoil_constSelector.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValueReadOnly} from '../core/Recoil_RecoilValue'; import type {Parameter} from './Recoil_selectorFamily'; const selectorFamily = require('./Recoil_selectorFamily'); // flowlint-next-line unclear-type:off const constantSelector = selectorFamily({ key: '__constant', get: constant => () => constant, cachePolicyForParams_UNSTABLE: { equality: 'reference', }, }); // Function that returns a selector which always produces the // same constant value. It may be called multiple times with the // same value, based on reference equality, and will provide the // same selector. function constSelector(constant: T): RecoilValueReadOnly { return constantSelector(constant); } module.exports = constSelector; ================================================ FILE: packages/recoil/recoil_values/Recoil_errorSelector.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValueReadOnly} from '../core/Recoil_RecoilValue'; const selectorFamily = require('./Recoil_selectorFamily'); const err = require('recoil-shared/util/Recoil_err'); // flowlint-next-line unclear-type:off const throwingSelector = selectorFamily({ key: '__error', get: message => () => { throw err(message); }, // TODO Why? cachePolicyForParams_UNSTABLE: { equality: 'reference', }, }); // Function that returns a selector which always throws an error // with the provided message. function errorSelector(message: string): RecoilValueReadOnly { return throwingSelector(message); } module.exports = errorSelector; ================================================ FILE: packages/recoil/recoil_values/Recoil_readOnlySelector.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Wraps another recoil value and prevents writing to it. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { RecoilValue, RecoilValueReadOnly, } from '../core/Recoil_RecoilValue'; function readOnlySelector(atom: RecoilValue): RecoilValueReadOnly { // flowlint-next-line unclear-type: off return (atom: any); } module.exports = readOnlySelector; ================================================ FILE: packages/recoil/recoil_values/Recoil_selector.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Return an atom whose state cannot vary independently but is derived from that * of other atoms. Whenever its dependency atoms change, it will re-evaluate * a function and pass along the result to any components or further selectors: * * const exampleSelector = selector({ * key: 'example', * get: ({get}) => { * const a = get(atomA); * const b = get(atomB); * return a + b; * }, * }); * * In this example, the value of exampleSelector will be the sum of atomA and atomB. * This sum will be updated whenever either atomA or atomB changes. The value * returned by the function will be deeply frozen. * * The function is only reevaluated if the dependencies change and the selector * has a component subscribed to it (either directly or indirectly via other * selectors). By default, function results are cached, so if the same values * of the dependencies are seen again, the cached value will be returned instead * of the function being reevaluated. The caching behavior can be overridden * by providing the `cacheImplementation` option; this can be used to discard * old values or to provide different equality semantics. * * If the provided function returns a Promise, it will cause the value of the * atom to become unavailable until the promise resolves. This means that any * components subscribed to the selector will suspend. If the promise is rejected, * any subscribed components will throw the rejecting error during rendering. * * You can provide the `set` option to allow writing to the selector. This * should be used sparingly; maintain a conceptual separation between independent * state and derived values. The `set` function receives a function to set * upstream RecoilValues which can accept a value or an updater function. * The updater function provides parameters with the old value of the RecoilValue * as well as a get() function to read other RecoilValues. * * const multiplierSelector = selector({ * key: 'multiplier', * get: ({get}) => get(atomA) * 100, * set: ({set, reset, get}, newValue) => set(atomA, newValue / 100), * }); * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { Loadable, LoadingLoadableType, ValueLoadableType, } from '../adt/Recoil_Loadable'; import type {CachePolicy} from '../caches/Recoil_CachePolicy'; import type { NodeCacheRoute, TreeCacheImplementation, } from '../caches/Recoil_TreeCacheImplementationType'; import type {StateID} from '../core/Recoil_Keys'; import type {DefaultValue} from '../core/Recoil_Node'; import type { RecoilState, RecoilValue, RecoilValueReadOnly, } from '../core/Recoil_RecoilValue'; import type {RetainedBy} from '../core/Recoil_RetainedBy'; import type {AtomWrites, NodeKey, Store, TreeState} from '../core/Recoil_State'; import type {RecoilCallbackInterface} from '../hooks/Recoil_useRecoilCallback'; import type { GetRecoilValue, ResetRecoilState, SetRecoilState, ValueOrUpdater, } from './Recoil_callbackTypes'; const { isLoadable, loadableWithError, loadableWithPromise, loadableWithValue, } = require('../adt/Recoil_Loadable'); const {WrappedValue} = require('../adt/Recoil_Wrapper'); const treeCacheFromPolicy = require('../caches/Recoil_treeCacheFromPolicy'); const { getNodeLoadable, peekNodeLoadable, setNodeValue, } = require('../core/Recoil_FunctionalCore'); const {saveDepsToStore} = require('../core/Recoil_Graph'); const { DEFAULT_VALUE, getConfigDeletionHandler, getNode, registerNode, } = require('../core/Recoil_Node'); const {isRecoilValue} = require('../core/Recoil_RecoilValue'); const { markRecoilValueModified, } = require('../core/Recoil_RecoilValueInterface'); const {retainedByOptionWithDefault} = require('../core/Recoil_Retention'); const {recoilCallback} = require('../hooks/Recoil_useRecoilCallback'); const concatIterables = require('recoil-shared/util/Recoil_concatIterables'); const deepFreezeValue = require('recoil-shared/util/Recoil_deepFreezeValue'); const err = require('recoil-shared/util/Recoil_err'); const filterIterable = require('recoil-shared/util/Recoil_filterIterable'); const gkx = require('recoil-shared/util/Recoil_gkx'); const invariant = require('recoil-shared/util/Recoil_invariant'); const isPromise = require('recoil-shared/util/Recoil_isPromise'); const mapIterable = require('recoil-shared/util/Recoil_mapIterable'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const { startPerfBlock, } = require('recoil-shared/util/Recoil_PerformanceTimings'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); type SelectorCallbackInterface = $ReadOnly<{ // TODO Technically this could be RecoilValueReadOnly, but trying to parameterize // it based on the selector type ran into problems which would lead to // dangerous error suppressions. node: RecoilState, ...RecoilCallbackInterface, }>; export type GetCallback = , Return>( fn: (SelectorCallbackInterface) => (...Args) => Return, ) => (...Args) => Return; type BaseSelectorOptions = $ReadOnly<{ key: string, dangerouslyAllowMutability?: boolean, retainedBy_UNSTABLE?: RetainedBy, cachePolicy_UNSTABLE?: CachePolicy, }>; export type ReadOnlySelectorOptions = $ReadOnly<{ ...BaseSelectorOptions, get: ({ get: GetRecoilValue, getCallback: GetCallback, }) => RecoilValue | Promise | Loadable | WrappedValue | T, }>; export type ReadWriteSelectorOptions = $ReadOnly<{ ...ReadOnlySelectorOptions, set: ( {set: SetRecoilState, get: GetRecoilValue, reset: ResetRecoilState}, newValue: T | DefaultValue, ) => void, }>; export type SelectorOptions = | ReadOnlySelectorOptions | ReadWriteSelectorOptions; export type DepValues = Map>; class Canceled {} const CANCELED: Canceled = new Canceled(); /** * An ExecutionID is an arbitrary ID that lets us distinguish executions from * each other. This is necessary as we need a way of solving this problem: * "given 3 async executions, only update state for the 'latest' execution when * it finishes running regardless of when the other 2 finish". ExecutionIDs * provide a convenient way of identifying executions so that we can track and * manage them over time. */ type ExecutionID = number; /** * ExecutionInfo is useful for managing async work and resolving race * conditions. It keeps track of the following: * * 1. The dep values found so far for the latest running execution. This is * useful for answering the question "given a new state, have any of the * async execution's discovered dep values changed?" * 2. The latest loadable, which holds the loadable of the latest execution. * This is important because we need to return this loadable when the * selector's result is requested and there is a pending async execution. We * are essentially caching the latest loading loadable without using the * actual selector cache so that we can avoid creating cache keys that use * partial dependencies (we never want to cache based on partial * dependencies). * 3. The latest execution ID, which is needed to know whether or not an async * execution is stale. At any point in time there may be any number of stale * executions running, but there is only one 'latest' execution, which * represents the execution that will make its way to the UI and make updates * to global state when it finishes. * 4. The set of stateVersions which have already been tested as valid for this * evalution. This is an optimization to avoid having to transitively * check if any deps have changed for a state we have aleady checked. * If additional async dependencies are discovered later, they may have * different values in different stores/states, so this will have to be * cleared. */ type ExecutionInfo = { // This is mutable and updated as new deps are discovered depValuesDiscoveredSoFarDuringAsyncWork: DepValues, loadingLoadable: LoadingLoadableType, executionID: ExecutionID, stateVersions: Map, }; // An object to hold the current state for loading dependencies for a particular // execution of a selector. This is used for async selector handling to know // which dependency was pending or if a user-promise was thrown. An object is // used instead of just a variable with the loadingDepKey so that if the // selector is async we can still access the current state in a promise chain // by updating the object reference. type LoadingDepsState = { loadingDepKey: NodeKey | null, loadingDepPromise: Promise | null, }; // $FlowFixMe[missing-empty-array-annot] const dependencyStack = []; // for detecting circular dependencies. const waitingStores: Map> = new Map(); const getNewExecutionID: () => ExecutionID = (() => { let executionID = 0; return () => executionID++; })(); /* eslint-disable no-redeclare */ declare function selector( options: ReadOnlySelectorOptions, ): RecoilValueReadOnly; declare function selector( options: ReadWriteSelectorOptions, ): RecoilState; function selector( options: ReadOnlySelectorOptions | ReadWriteSelectorOptions, ): RecoilValue { let recoilValue: ?RecoilValue = null; const {key, get, cachePolicy_UNSTABLE: cachePolicy} = options; const set = options.set != null ? options.set : undefined; // flow if (__DEV__) { if (typeof key !== 'string') { throw err( 'A key option with a unique string value must be provided when creating a selector.', ); } if (typeof get !== 'function') { throw err( 'Selectors must specify a get callback option to get the selector value.', ); } } // This is every discovered dependency across all executions const discoveredDependencyNodeKeys = new Set(); const cache: TreeCacheImplementation> = treeCacheFromPolicy( cachePolicy ?? { equality: 'reference', eviction: 'keep-all', }, key, ); const retainedBy = retainedByOptionWithDefault(options.retainedBy_UNSTABLE); const executionInfoMap: Map> = new Map(); let liveStoresCount = 0; function selectorIsLive() { return !gkx('recoil_memory_managament_2020') || liveStoresCount > 0; } function selectorInit(store: Store): () => void { store.getState().knownSelectors.add(key); liveStoresCount++; return () => { liveStoresCount--; }; } function selectorShouldDeleteConfigOnRelease() { return getConfigDeletionHandler(key) !== undefined && !selectorIsLive(); } function resolveAsync( store: Store, state: TreeState, executionID: ExecutionID, loadable: Loadable, depValues: DepValues, ): void { setCache(state, loadable, depValues); notifyStoresOfResolvedAsync(store, executionID); } function notifyStoresOfResolvedAsync( store: Store, executionID: ExecutionID, ): void { if (isLatestExecution(store, executionID)) { clearExecutionInfo(store); } notifyWaitingStores(executionID, true); } /** * Notify stores to pull the selector again if a new async dep was discovered. * 1) Async selector adds a new dep but doesn't resolve yet. * Note that deps for an async selector are based on the state when the * evaluation started, in order to provide a consistent picture of state. * 2) But, new value of dep based on the current state might cause the selector * to resolve or resolve differently. * 3) Therefore, this notification will pull the selector based on the current * state for the components */ function notifyStoresOfNewAsyncDep( store: Store, executionID: ExecutionID, ): void { if (isLatestExecution(store, executionID)) { const executionInfo = nullthrows(getExecutionInfo(store)); executionInfo.stateVersions.clear(); notifyWaitingStores(executionID, false); } } function notifyWaitingStores( executionID: ExecutionID, clearWaitlist: boolean, ) { const stores = waitingStores.get(executionID); if (stores != null) { for (const waitingStore of stores) { markRecoilValueModified(waitingStore, nullthrows(recoilValue)); } if (clearWaitlist) { waitingStores.delete(executionID); } } } function markStoreWaitingForResolvedAsync( store: Store, executionID: ExecutionID, ): void { let stores = waitingStores.get(executionID); if (stores == null) { waitingStores.set(executionID, (stores = new Set())); } stores.add(store); } /** * This function attaches a then() and a catch() to a promise that was * returned from a selector's get() (either explicitly or implicitly by * running a function that uses the "async" keyword). If a selector's get() * returns a promise, we have two possibilities: * * 1. The promise will resolve, in which case it will have completely finished * executing without any remaining pending dependencies. No more retries * are needed and we can proceed with updating the cache and notifying * subscribers (if it is the latest execution, otherwise only the cache * will be updated and subscriptions will not be fired). This is the case * handled by the attached then() handler. * * 2. The promise will throw because it either has an error or it came across * an async dependency that has not yet resolved, in which case we will * call wrapDepdencyPromise(), whose responsibility is to handle dependency * promises. This case is handled by the attached catch() handler. * * Both branches will eventually resolve to the final result of the selector * (or an error if a real error occurred). * * The execution will run to completion even if it is stale, and its value * will be cached. But stale executions will not update global state or update * executionInfo as that is the responsibility of the 'latest' execution. * * Note this function should not be passed a promise that was thrown--AKA a * dependency promise. Dependency promises should be passed to * wrapPendingDependencyPromise()). */ function wrapResultPromise( store: Store, promise: Promise, state: TreeState, depValues: DepValues, executionID: ExecutionID, loadingDepsState: LoadingDepsState, ): Promise { return promise .then(value => { if (!selectorIsLive()) { // The selector was released since the request began; ignore the response. clearExecutionInfo(store); throw CANCELED; } const loadable = loadableWithValue(value); resolveAsync(store, state, executionID, loadable, depValues); return value; }) .catch(errorOrPromise => { if (!selectorIsLive()) { // The selector was released since the request began; ignore the response. clearExecutionInfo(store); throw CANCELED; } if (isPromise(errorOrPromise)) { return wrapPendingDependencyPromise( store, errorOrPromise, state, depValues, executionID, loadingDepsState, ); } const loadable = loadableWithError(errorOrPromise); resolveAsync(store, state, executionID, loadable, depValues); throw errorOrPromise; }); } /** * This function attaches a then() and a catch() to a promise that was * thrown from a selector's get(). If a selector's get() throws a promise, * we have two possibilities: * * 1. The promise will resolve, meaning one of our selector's dependencies is * now available and we should "retry" our get() by running it again. This * is the case handled by the attached then() handler. * * 2. The promise will throw because something went wrong with the dependency * promise (in other words a real error occurred). This case is handled by * the attached catch() handler. If the dependency promise throws, it is * _always_ a real error and not another dependency promise (any dependency * promises would have been handled upstream). * * The then() branch will eventually resolve to the final result of the * selector (or an error if a real error occurs), and the catch() will always * resolve to an error because the dependency promise is a promise that was * wrapped upstream, meaning it will only resolve to its real value or to a * real error. * * The execution will run to completion even if it is stale, and its value * will be cached. But stale executions will not update global state or update * executionInfo as that is the responsibility of the 'latest' execution. * * Note this function should not be passed a promise that was returned from * get(). The intention is that this function is only passed promises that * were thrown due to a pending dependency. Promises returned by get() should * be passed to wrapResultPromise() instead. */ function wrapPendingDependencyPromise( store: Store, promise: Promise, state: TreeState, existingDeps: DepValues, executionID: ExecutionID, loadingDepsState: LoadingDepsState, ): Promise { return promise .then(resolvedDep => { if (!selectorIsLive()) { // The selector was released since the request began; ignore the response. clearExecutionInfo(store); throw CANCELED; } // Check if we are handling a pending Recoil dependency or if the user // threw their own Promise to "suspend" a selector evaluation. We need // to check that the loadingDepPromise actually matches the promise that // we caught in case the selector happened to catch the promise we threw // for a pending Recoil dependency from `getRecoilValue()` and threw // their own promise instead. if ( loadingDepsState.loadingDepKey != null && loadingDepsState.loadingDepPromise === promise ) { /** * Note for async atoms, this means we are changing the atom's value * in the store for the given version. This should be alright because * the version of state is now stale and a new version will have * already been triggered by the atom being resolved (see this logic * in Recoil_atom.js) */ state.atomValues.set( loadingDepsState.loadingDepKey, loadableWithValue(resolvedDep), ); } else { /** * If resolvedDepKey is not defined, the promise was a user-thrown * promise. User-thrown promises are an advanced feature and they * should be avoided in almost all cases. Using `loadable.map()` inside * of selectors for loading loadables and then throwing that mapped * loadable's promise is an example of a user-thrown promise. * * When we hit a user-thrown promise, we have to bail out of an optimization * where we bypass calculating selector cache keys for selectors that * have been previously seen for a given state (these selectors are saved in * state.atomValues) to avoid stale state as we have no way of knowing * what state changes happened (if any) in result to the promise resolving. * * Ideally we would only bail out selectors that are in the chain of * dependencies for this selector, but there's currently no way to get * a full list of a selector's downstream nodes because the state that * is executing may be a discarded tree (so store.getGraph(state.version) * will be empty), and the full dep tree may not be in the selector * caches in the case where the selector's cache was cleared. To solve * for this we would have to keep track of all running selector * executions and their downstream deps. Because this only covers edge * cases, that complexity might not be justifyable. */ store.getState().knownSelectors.forEach(nodeKey => { state.atomValues.delete(nodeKey); }); } /** * Optimization: Now that the dependency has resolved, let's try hitting * the cache in case the dep resolved to a value we have previously seen. * * TODO: * Note this optimization is not perfect because it only prevents re-executions * _after_ the point where an async dependency is found. Any code leading * up to the async dependency may have run unnecessarily. The ideal case * would be to wait for the async dependency to resolve first, check the * cache, and prevent _any_ execution of the selector if the resulting * value of the dependency leads to a path that is found in the cache. * The ideal case is more difficult to implement as it would require that * we capture and wait for the the async dependency right after checking * the cache. The current approach takes advantage of the fact that running * the selector already has a code path that lets us exit early when * an async dep resolves. */ const cachedLoadable = getLoadableFromCacheAndUpdateDeps(store, state); if (cachedLoadable && cachedLoadable.state !== 'loading') { /** * This has to notify stores of a resolved async, even if there is no * current pending execution for the following case: * 1) A component renders with this pending loadable. * 2) The upstream dependency resolves. * 3) While processing some other selector it reads this one, such as * while traversing its dependencies. At this point it gets the * new resolved value synchronously and clears the current * execution ID. The component wasn't getting the value itself, * though, so it still has the pending loadable. * 4) When this code executes the current execution id was cleared * and it wouldn't notify the component of the new value. * * I think this is only an issue with "early" rendering since the * components got their value using the in-progress execution. * We don't have a unit test for this case yet. I'm not sure it is * necessary with recoil_transition_support mode. */ if ( isLatestExecution(store, executionID) || getExecutionInfo(store) == null ) { notifyStoresOfResolvedAsync(store, executionID); } if (cachedLoadable.state === 'hasValue') { return cachedLoadable.contents; } else { throw cachedLoadable.contents; } } /** * If this execution is stale, let's check to see if there is some in * progress execution with a matching state. If we find a match, then * we can take the value from that in-progress execution. Note this may * sound like an edge case, but may be very common in cases where a * loading dependency resolves from loading to having a value (thus * possibly triggering a re-render), and React re-renders before the * chained .then() functions run, thus starting a new execution as the * dep has changed value. Without this check we will run the selector * twice (once in the new execution and once again in this .then(), so * this check is necessary to keep unnecessary re-executions to a * minimum). * * Also note this code does not check across all executions that may be * running. It only optimizes for the _latest_ execution per store as * we currently do not maintain a list of all currently running executions. * This means in some cases we may run selectors more than strictly * necessary when there are multiple executions running for the same * selector. This may be a valid tradeoff as checking for dep changes * across all in-progress executions may take longer than just * re-running the selector. This will be app-dependent, and maybe in the * future we can make the behavior configurable. An ideal fix may be * to extend the tree cache to support caching loading states. */ if (!isLatestExecution(store, executionID)) { const executionInfo = getInProgressExecutionInfo(store, state); if (executionInfo != null) { /** * Returning promise here without wrapping as the wrapper logic was * already done upstream when this promise was generated. */ return executionInfo.loadingLoadable.contents; } } // Retry the selector evaluation now that the dependency has resolved const [loadable, depValues] = evaluateSelectorGetter( store, state, executionID, ); if (loadable.state !== 'loading') { resolveAsync(store, state, executionID, loadable, depValues); } if (loadable.state === 'hasError') { throw loadable.contents; } return loadable.contents; }) .catch(error => { // The selector was released since the request began; ignore the response. if (error instanceof Canceled) { throw CANCELED; } if (!selectorIsLive()) { clearExecutionInfo(store); throw CANCELED; } const loadable = loadableWithError(error); resolveAsync(store, state, executionID, loadable, existingDeps); throw error; }); } function updateDeps( store: Store, state: TreeState, deps: $ReadOnlySet, executionID: ?ExecutionID, ): void { if ( isLatestExecution(store, executionID) || state.version === store.getState()?.currentTree?.version || state.version === store.getState()?.nextTree?.version ) { saveDepsToStore( key, deps, store, store.getState()?.nextTree?.version ?? store.getState().currentTree.version, ); } for (const nodeKey of deps) { discoveredDependencyNodeKeys.add(nodeKey); } } function evaluateSelectorGetter( store: Store, state: TreeState, executionID: ExecutionID, ): [Loadable, DepValues] { const endPerfBlock = startPerfBlock(key); // TODO T63965866: use execution ID here let duringSynchronousExecution = true; let duringAsynchronousExecution = true; const finishEvaluation = () => { endPerfBlock(); duringAsynchronousExecution = false; }; let result; let resultIsError = false; let loadable: Loadable; const loadingDepsState: LoadingDepsState = { loadingDepKey: null, loadingDepPromise: null, }; /** * Starting a fresh set of deps that we'll be using to update state. We're * starting a new set versus adding it in existing state deps because * the version of state that we update deps for may be a more recent version * than the version the selector was called with. This is because the latest * execution will update the deps of the current/latest version of state * (This is safe to do because the fact that the selector is the latest * execution means the deps we discover below are our best guess at the * deps for the current/latest state in the store) */ const depValues = new Map>(); function getRecoilValue({key: depKey}: RecoilValue): S { const depLoadable = getNodeLoadable(store, state, depKey); depValues.set(depKey, depLoadable); // We need to update asynchronous dependencies as we go so the selector // knows if it has to restart evaluation if one of them is updated before // the asynchronous selector completely resolves. if (!duringSynchronousExecution) { updateDeps(store, state, new Set(depValues.keys()), executionID); notifyStoresOfNewAsyncDep(store, executionID); } switch (depLoadable.state) { case 'hasValue': return depLoadable.contents; case 'hasError': throw depLoadable.contents; case 'loading': loadingDepsState.loadingDepKey = depKey; loadingDepsState.loadingDepPromise = depLoadable.contents; throw depLoadable.contents; } throw err('Invalid Loadable state'); } const getCallback = , Return>( fn: (SelectorCallbackInterface) => (...Args) => Return, ): ((...Args) => Return) => { return (...args) => { if (duringAsynchronousExecution) { throw err( 'Callbacks from getCallback() should only be called asynchronously after the selector is evalutated. It can be used for selectors to return objects with callbacks that can work with Recoil state without a subscription.', ); } invariant(recoilValue != null, 'Recoil Value can never be null'); return recoilCallback}>( store, fn, args, {node: (recoilValue: any)}, // flowlint-line unclear-type:off ); }; }; try { result = get({get: getRecoilValue, getCallback}); result = isRecoilValue(result) ? getRecoilValue(result) : result; if (isLoadable(result)) { if (result.state === 'hasError') { resultIsError = true; } result = (result: ValueLoadableType).contents; } if (isPromise(result)) { result = wrapResultPromise( store, result, state, depValues, executionID, loadingDepsState, ).finally(finishEvaluation); } else { finishEvaluation(); } result = result instanceof WrappedValue ? result.value : result; } catch (errorOrDepPromise) { result = errorOrDepPromise; if (isPromise(result)) { result = wrapPendingDependencyPromise( store, result, state, depValues, executionID, loadingDepsState, ).finally(finishEvaluation); } else { resultIsError = true; finishEvaluation(); } } if (resultIsError) { loadable = loadableWithError(result); } else if (isPromise(result)) { loadable = loadableWithPromise(result); } else { loadable = loadableWithValue(result); } duringSynchronousExecution = false; updateExecutionInfoDepValues(store, executionID, depValues); updateDeps(store, state, new Set(depValues.keys()), executionID); return [loadable, depValues]; } function getLoadableFromCacheAndUpdateDeps( store: Store, state: TreeState, ): ?Loadable { // First, look up in the state cache // If it's here, then the deps in the store should already be valid. let cachedLoadable: ?Loadable = state.atomValues.get(key); if (cachedLoadable != null) { return cachedLoadable; } // Second, look up in the selector cache and update the deps in the store const depsAfterCacheLookup = new Set(); try { cachedLoadable = cache.get( nodeKey => { invariant( typeof nodeKey === 'string', 'Cache nodeKey is type string', ); return getNodeLoadable(store, state, nodeKey).contents; }, { onNodeVisit: node => { if (node.type === 'branch' && node.nodeKey !== key) { depsAfterCacheLookup.add(node.nodeKey); } }, }, ); } catch (error) { throw err( `Problem with cache lookup for selector "${key}": ${error.message}`, ); } if (cachedLoadable) { // Cache the results in the state to allow for cheaper lookup than // iterating the tree cache of dependencies. state.atomValues.set(key, cachedLoadable); /** * Ensure store contains correct dependencies if we hit the cache so that * the store deps and cache are in sync for a given state. This is important * because store deps are normally updated when new executions are created, * but cache hits don't trigger new executions but they still _may_ signify * a change in deps in the store if the store deps for this state are empty * or stale. */ updateDeps( store, state, depsAfterCacheLookup, getExecutionInfo(store)?.executionID, ); } return cachedLoadable; } /** * Given a tree state, this function returns a Loadable of the current state. * * The selector's get() function will only be re-evaluated if _both_ of the * following statements are true: * * 1. The current dep values from the given state produced a cache key that * was not found in the cache. * 2. There is no currently running async execution OR there is an * async execution that is running, but after comparing the dep values in * the given state with the dep values that the execution has discovered so * far we find that at least one dep value has changed, in which case we * start a new execution (the previously running execution will continue to * run to completion, but only the new execution will be deemed the * 'latest' execution, meaning it will be the only execution that will * update global state when it is finished. Any non-latest executions will * run to completion and update the selector cache but not global state). */ function getSelectorLoadableAndUpdateDeps( store: Store, state: TreeState, ): Loadable { // First, see if our current state is cached const cachedVal = getLoadableFromCacheAndUpdateDeps(store, state); if (cachedVal != null) { clearExecutionInfo(store); return cachedVal; } // Second, check if there is already an ongoing execution based on the current state const inProgressExecutionInfo = getInProgressExecutionInfo(store, state); if (inProgressExecutionInfo != null) { if (inProgressExecutionInfo.loadingLoadable?.state === 'loading') { markStoreWaitingForResolvedAsync( store, inProgressExecutionInfo.executionID, ); } // FIXME: check after the fact to see if we made the right choice by waiting return inProgressExecutionInfo.loadingLoadable; } // Third, start a new evaluation of the selector const newExecutionID = getNewExecutionID(); const [loadable, newDepValues] = evaluateSelectorGetter( store, state, newExecutionID, ); /** * Conditionally updates the cache with a given loadable. * * We only cache loadables that are not loading because our cache keys are * based on dep values, which are in an unfinished state for loadables that * have a 'loading' state (new deps may be discovered while the selector * runs its async code). We never want to cache partial dependencies b/c it * could lead to errors, such as prematurely returning the result based on a * partial list of deps-- we need the full list of deps to ensure that we * are returning the correct result from cache. */ if (loadable.state === 'loading') { setExecutionInfo(store, newExecutionID, loadable, newDepValues, state); markStoreWaitingForResolvedAsync(store, newExecutionID); } else { clearExecutionInfo(store); setCache(state, loadable, newDepValues); } return loadable; } /** * Searches execution info across all stores to see if there is an in-progress * execution whose dependency values match the values of the requesting store. */ function getInProgressExecutionInfo( store: Store, state: TreeState, ): ?ExecutionInfo { // Sort the pending executions so that our current store is checked first. const pendingExecutions = concatIterables([ executionInfoMap.has(store) ? [nullthrows(executionInfoMap.get(store))] : [], mapIterable( filterIterable(executionInfoMap, ([s]) => s !== store), ([, execInfo]) => execInfo, ), ]); function anyDepChanged(execDepValues: DepValues): boolean { for (const [depKey, execLoadable] of execDepValues) { if (!getNodeLoadable(store, state, depKey).is(execLoadable)) { return true; } } return false; } for (const execInfo of pendingExecutions) { if ( // If this execution was already checked to be valid with this version // of state, then let's use it! execInfo.stateVersions.get(state.version) || // If the deps for the execution match our current state, then it's valid !anyDepChanged(execInfo.depValuesDiscoveredSoFarDuringAsyncWork) ) { execInfo.stateVersions.set(state.version, true); return execInfo; } else { execInfo.stateVersions.set(state.version, false); } } return undefined; } function getExecutionInfo(store: Store): ?ExecutionInfo { return executionInfoMap.get(store); } /** * This function will update the selector's execution info when the selector * has either finished running an execution or has started a new execution. If * the given loadable is in a 'loading' state, the intention is that a new * execution has started. Otherwise, the intention is that an execution has * just finished. */ function setExecutionInfo( store: Store, newExecutionID: ExecutionID, loadable: LoadingLoadableType, depValues: DepValues, state: TreeState, ) { executionInfoMap.set(store, { depValuesDiscoveredSoFarDuringAsyncWork: depValues, executionID: newExecutionID, loadingLoadable: loadable, stateVersions: new Map([[state.version, true]]), }); } function updateExecutionInfoDepValues( store: Store, executionID: ExecutionID, depValues: DepValues, ) { // We only need to bother updating the deps for the latest execution because // that's all getInProgressExecutionInfo() will be looking for. if (isLatestExecution(store, executionID)) { const executionInfo = getExecutionInfo(store); if (executionInfo != null) { executionInfo.depValuesDiscoveredSoFarDuringAsyncWork = depValues; } } } function clearExecutionInfo(store: Store) { executionInfoMap.delete(store); } function isLatestExecution(store: Store, executionID: ?ExecutionID): boolean { return executionID === getExecutionInfo(store)?.executionID; } /** * FIXME: dep keys should take into account the state of the loadable to * prevent the edge case where a loadable with an error and a loadable with * an error as a value are treated as the same thing incorrectly. For example * these two should be treated differently: * * selector({key: '', get: () => new Error('hi')}); * selector({key: '', get () => {throw new Error('hi')}}); * * With current implementation they are treated the same */ function depValuesToDepRoute(depValues: DepValues): NodeCacheRoute { return Array.from(depValues.entries()).map(([depKey, valLoadable]) => [ depKey, valLoadable.contents, ]); } function setCache( state: TreeState, loadable: Loadable, depValues: DepValues, ) { if (__DEV__) { if ( loadable.state !== 'loading' && Boolean(options.dangerouslyAllowMutability) === false ) { deepFreezeValue(loadable.contents); } } state.atomValues.set(key, loadable); try { cache.set(depValuesToDepRoute(depValues), loadable); } catch (error) { throw err( `Problem with setting cache for selector "${key}": ${error.message}`, ); } } function detectCircularDependencies(fn: () => Loadable) { if (dependencyStack.includes(key)) { const message = `Recoil selector has circular dependencies: ${dependencyStack .slice(dependencyStack.indexOf(key)) .join(' \u2192 ')}`; return loadableWithError(err(message)); } // $FlowFixMe[incompatible-call] dependencyStack.push(key); try { return fn(); } finally { dependencyStack.pop(); } } function selectorPeek(store: Store, state: TreeState): ?Loadable { const cachedLoadable = state.atomValues.get(key); if (cachedLoadable != null) { return cachedLoadable; } return cache.get(nodeKey => { invariant(typeof nodeKey === 'string', 'Cache nodeKey is type string'); return peekNodeLoadable(store, state, nodeKey)?.contents; }); } function selectorGet(store: Store, state: TreeState): Loadable { if (store.skipCircularDependencyDetection_DANGEROUS === true) { return getSelectorLoadableAndUpdateDeps(store, state); } return detectCircularDependencies(() => getSelectorLoadableAndUpdateDeps(store, state), ); } function invalidateSelector(state: TreeState) { state.atomValues.delete(key); } function clearSelectorCache(store: Store, treeState: TreeState) { invariant(recoilValue != null, 'Recoil Value can never be null'); for (const nodeKey of discoveredDependencyNodeKeys) { const node = getNode(nodeKey); node.clearCache?.(store, treeState); } discoveredDependencyNodeKeys.clear(); invalidateSelector(treeState); cache.clear(); markRecoilValueModified(store, recoilValue); } if (set != null) { /** * ES5 strict mode prohibits defining non-top-level function declarations, * so don't use function declaration syntax here */ const selectorSet = ( store: Store, state: TreeState, newValue: T | DefaultValue, ): AtomWrites => { let syncSelectorSetFinished = false; const writes: AtomWrites = new Map(); function getRecoilValue({key: depKey}: RecoilValue): S { if (syncSelectorSetFinished) { throw err('Recoil: Async selector sets are not currently supported.'); } const loadable = getNodeLoadable(store, state, depKey); if (loadable.state === 'hasValue') { return loadable.contents; } else if (loadable.state === 'loading') { const msg = `Getting value of asynchronous atom or selector "${depKey}" in a pending state while setting selector "${key}" is not yet supported.`; recoverableViolation(msg, 'recoil'); throw err(msg); } else { throw loadable.contents; } } function setRecoilState( recoilState: RecoilState, valueOrUpdater: ValueOrUpdater, // $FlowFixMe[missing-local-annot] ) { if (syncSelectorSetFinished) { const msg = 'Recoil: Async selector sets are not currently supported.'; recoverableViolation(msg, 'recoil'); throw err(msg); } const setValue = typeof valueOrUpdater === 'function' ? // cast to any because we can't restrict type S from being a function itself without losing support for opaque types // flowlint-next-line unclear-type:off (valueOrUpdater: any)(getRecoilValue(recoilState)) : valueOrUpdater; const upstreamWrites = setNodeValue( store, state, recoilState.key, setValue, ); upstreamWrites.forEach((v, k) => writes.set(k, v)); } function resetRecoilState(recoilState: RecoilState) { setRecoilState(recoilState, DEFAULT_VALUE); } const ret = set( {set: setRecoilState, get: getRecoilValue, reset: resetRecoilState}, newValue, ); // set should be a void method, but if the user makes it `async`, then it // will return a Promise, which we don't currently support. if (ret !== undefined) { throw isPromise(ret) ? err('Recoil: Async selector sets are not currently supported.') : err('Recoil: selector set should be a void function.'); } syncSelectorSetFinished = true; return writes; }; return (recoilValue = registerNode({ key, nodeType: 'selector', peek: selectorPeek, get: selectorGet, set: selectorSet, init: selectorInit, invalidate: invalidateSelector, clearCache: clearSelectorCache, shouldDeleteConfigOnRelease: selectorShouldDeleteConfigOnRelease, dangerouslyAllowMutability: options.dangerouslyAllowMutability, shouldRestoreFromSnapshots: false, retainedBy, })); } else { return (recoilValue = registerNode({ key, nodeType: 'selector', peek: selectorPeek, get: selectorGet, init: selectorInit, invalidate: invalidateSelector, clearCache: clearSelectorCache, shouldDeleteConfigOnRelease: selectorShouldDeleteConfigOnRelease, dangerouslyAllowMutability: options.dangerouslyAllowMutability, shouldRestoreFromSnapshots: false, retainedBy, })); } } /* eslint-enable no-redeclare */ // $FlowIssue[incompatible-use] // $FlowFixMe[missing-local-annot] selector.value = value => new WrappedValue(value); // $FlowFixMe[incompatible-cast] module.exports = (selector: { (ReadOnlySelectorOptions): RecoilValueReadOnly, (ReadWriteSelectorOptions): RecoilState, value: (S) => WrappedValue, }); ================================================ FILE: packages/recoil/recoil_values/Recoil_selectorFamily.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../adt/Recoil_Loadable'; import type {WrappedValue} from '../adt/Recoil_Wrapper'; import type { CachePolicy, CachePolicyWithoutEviction, } from '../caches/Recoil_CachePolicy'; import type {DefaultValue} from '../core/Recoil_Node'; import type { RecoilState, RecoilValue, RecoilValueReadOnly, } from '../core/Recoil_RecoilValue'; import type {RetainedBy} from '../core/Recoil_RetainedBy'; import type {GetCallback} from '../recoil_values/Recoil_selector'; import type { GetRecoilValue, ResetRecoilState, SetRecoilState, } from './Recoil_callbackTypes'; const cacheFromPolicy = require('../caches/Recoil_cacheFromPolicy'); const {setConfigDeletionHandler} = require('../core/Recoil_Node'); const selector = require('./Recoil_selector'); const err = require('recoil-shared/util/Recoil_err'); const stableStringify = require('recoil-shared/util/Recoil_stableStringify'); // Keep in mind the parameter needs to be serializable as a cahche key // using Recoil_stableStringify type Primitive = void | null | boolean | number | string; interface HasToJSON { toJSON(): Parameter; } export type Parameter = | Primitive | HasToJSON | $ReadOnlySet | $ReadOnlyMap | $ReadOnlyArray | $ReadOnly<{...}>; // | $ReadOnly<{[string]: Parameter}>; // TODO Better enforce object is serializable type BaseSelectorFamilyOptions = $ReadOnly<{ key: string, cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction, cachePolicy_UNSTABLE?: CachePolicy, dangerouslyAllowMutability?: boolean, retainedBy_UNSTABLE?: RetainedBy | (P => RetainedBy), }>; export type ReadOnlySelectorFamilyOptions = $ReadOnly<{ ...BaseSelectorFamilyOptions

, get: P => ({ get: GetRecoilValue, getCallback: GetCallback, }) => Promise | Loadable | WrappedValue | RecoilValue | T, }>; export type ReadWriteSelectorFamilyOptions = $ReadOnly<{ ...ReadOnlySelectorFamilyOptions, set: P => ( {set: SetRecoilState, get: GetRecoilValue, reset: ResetRecoilState}, newValue: T | DefaultValue, ) => void, }>; export type SelectorFamilyOptions = | ReadOnlySelectorFamilyOptions | ReadWriteSelectorFamilyOptions; // Add a unique index to each selector in case the cache implementation allows // duplicate keys based on equivalent stringified parameters let nextIndex = 0; /* eslint-disable no-redeclare */ declare function selectorFamily( options: ReadOnlySelectorFamilyOptions, ): Params => RecoilValueReadOnly; declare function selectorFamily( options: ReadWriteSelectorFamilyOptions, ): Params => RecoilState; // Return a function that returns members of a family of selectors of the same type // E.g., // // const s = selectorFamily(...); // s({a: 1}) => a selector // s({a: 2}) => a different selector // // By default, the selectors are distinguished by distinct values of the // parameter based on value equality, not reference equality. This allows using // object literals or other equivalent objects at callsites to not create // duplicate cache entries. This behavior may be overridden with the // cacheImplementationForParams option. function selectorFamily( options: | ReadOnlySelectorFamilyOptions | ReadWriteSelectorFamilyOptions, ): Params => RecoilValue { const selectorCache = cacheFromPolicy< Params, RecoilState | RecoilValueReadOnly, >({ equality: options.cachePolicyForParams_UNSTABLE?.equality ?? 'value', eviction: 'keep-all', }); return (params: Params) => { // Throw an error with selector key so that it is clear which // selector is causing an error let cachedSelector; try { cachedSelector = selectorCache.get(params); } catch (error) { throw err( `Problem with cache lookup for selector ${options.key}: ${error.message}`, ); } if (cachedSelector != null) { return cachedSelector; } const myKey = `${options.key}__selectorFamily/${ stableStringify(params, { // It is possible to use functions in parameters if the user uses // a cache with reference equality thanks to the incrementing index. allowFunctions: true, }) ?? 'void' }/${nextIndex++}`; // Append index in case values serialize to the same key string const myGet = (callbacks: { get: GetRecoilValue, getCallback: GetCallback, }) => options.get(params)(callbacks); const myCachePolicy = options.cachePolicy_UNSTABLE; const retainedBy = typeof options.retainedBy_UNSTABLE === 'function' ? options.retainedBy_UNSTABLE(params) : options.retainedBy_UNSTABLE; let newSelector; if (options.set != null) { const set = options.set; const mySet = ( callbacks: { get: GetRecoilValue, reset: ResetRecoilState, set: SetRecoilState, }, newValue: T | DefaultValue, ) => set(params)(callbacks, newValue); newSelector = selector({ key: myKey, get: myGet, set: mySet, cachePolicy_UNSTABLE: myCachePolicy, dangerouslyAllowMutability: options.dangerouslyAllowMutability, retainedBy_UNSTABLE: retainedBy, }); } else { newSelector = selector({ key: myKey, get: myGet, cachePolicy_UNSTABLE: myCachePolicy, dangerouslyAllowMutability: options.dangerouslyAllowMutability, retainedBy_UNSTABLE: retainedBy, }); } selectorCache.set(params, newSelector); setConfigDeletionHandler(newSelector.key, () => { selectorCache.delete(params); }); return newSelector; }; } /* eslint-enable no-redeclare */ module.exports = selectorFamily; ================================================ FILE: packages/recoil/recoil_values/__flowtests__/Recoil_WaitFor-flowtest.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilState} from '../../core/Recoil_RecoilValue'; const {useRecoilValue} = require('../../hooks/Recoil_Hooks'); const atom = require('../Recoil_atom'); const readOnlySelector = require('../Recoil_readOnlySelector'); const { noWait, waitForAll, waitForAllSettled, waitForNone, } = require('../Recoil_WaitFor'); const numberAtom: RecoilState = atom({key: 'number', default: 0}); const stringAtom: RecoilState = atom({key: 'string', default: ''}); let num: number; let str: string; ////////////// // waitForAll ////////////// // Test tuple unwrapping of types // eslint-disable-next-line fb-www/react-hooks const arrayResults = useRecoilValue( // $FlowIssue[invalid-tuple-map] waitForAll([readOnlySelector(numberAtom), readOnlySelector(stringAtom)]), ); num = arrayResults[0]; str = arrayResults[1]; // $FlowExpectedError num = arrayResults[1]; // $FlowExpectedError str = arrayResults[0]; // Test object unwrapping of types // eslint-disable-next-line fb-www/react-hooks const objResults = useRecoilValue( // $FlowIssue[invalid-tuple-map] // $FlowIssue[incompatible-call] waitForAll({num: numberAtom, str: stringAtom}), ); num = objResults.num; str = objResults.str; // $FlowExpectedError num = objResults.str; // $FlowExpectedError str = objResults.num; ////////////// // waitForNone ////////////// // Test tuple unwrapping of types // eslint-disable-next-line fb-www/react-hooks const arrayResultsNone = useRecoilValue( // $FlowIssue[invalid-tuple-map] waitForNone([readOnlySelector(numberAtom), readOnlySelector(stringAtom)]), ); num = arrayResultsNone[0].valueOrThrow(); str = arrayResultsNone[1].valueOrThrow(); // $FlowExpectedError num = arrayResultsNone[1].valueOrThrow(); // $FlowExpectedError str = arrayResultsNone[0].valueOrThrow(); // Test object unwrapping of types // eslint-disable-next-line fb-www/react-hooks const objResultsNone = useRecoilValue( // $FlowIssue[incompatible-call] waitForNone({num: numberAtom, str: stringAtom}), ); num = objResultsNone.num.valueOrThrow(); str = objResultsNone.str.valueOrThrow(); // $FlowExpectedError num = objResultsNone.str.valueOrThrow(); // $FlowExpectedError str = objResultsNone.num.valueOrThrow(); ////////////// // waitForAllSettled ////////////// // Test tuple unwrapping of types // eslint-disable-next-line fb-www/react-hooks const arrayResultsAllSettled = useRecoilValue( waitForAllSettled([ // $FlowIssue[invalid-tuple-map] readOnlySelector(numberAtom), // $FlowIssue[invalid-tuple-map] readOnlySelector(stringAtom), ]), ); num = arrayResultsAllSettled[0].valueOrThrow(); str = arrayResultsAllSettled[1].valueOrThrow(); // $FlowExpectedError num = arrayResultsAllSettled[1].valueOrThrow(); // $FlowExpectedError str = arrayResultsAllSettled[0].valueOrThrow(); // Test object unwrapping of types // eslint-disable-next-line fb-www/react-hooks const objResultsAllSettled = useRecoilValue( // $FlowIssue[invalid-tuple-map] // $FlowIssue[incompatible-call] waitForAllSettled({num: numberAtom, str: stringAtom}), ); num = objResultsAllSettled.num.valueOrThrow(); str = objResultsAllSettled.str.valueOrThrow(); // $FlowExpectedError num = objResultsAllSettled.str.valueOrThrow(); // $FlowExpectedError str = objResultsAllSettled.num.valueOrThrow(); ////////////// // noWait ////////////// num = useRecoilValue(noWait(numberAtom)).valueOrThrow(); // eslint-disable-line fb-www/react-hooks // $FlowExpectedError str = useRecoilValue(noWait(numberAtom)).valueOrThrow(); // eslint-disable-line fb-www/react-hooks ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_WaitFor-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValue} from '../../core/Recoil_RecoilValue'; const { flushPromisesAndTimers, getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let getRecoilValueAsLoadable, noWait, waitForAll, waitForAllSettled, waitForAny, waitForNone, store, selector, invariant; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); invariant = require('recoil-shared/util/Recoil_invariant'); ({ getRecoilValueAsLoadable, } = require('../../core/Recoil_RecoilValueInterface')); selector = require('../Recoil_selector'); ({ noWait, waitForAll, waitForAllSettled, waitForAny, waitForNone, } = require('../Recoil_WaitFor')); store = makeStore(); }); /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function getLoadable(atom) { return getRecoilValueAsLoadable(store, atom).contents; } function getState( recoilValue: RecoilValue, ): 'loading' | 'hasValue' | 'hasError' { return getRecoilValueAsLoadable(store, recoilValue).state; } function getValue(recoilValue: RecoilValue): T { const loadable = getRecoilValueAsLoadable(store, recoilValue); if (loadable.state !== 'hasValue') { throw new Error(`expected atom "${recoilValue.key}" to have a value`); } return loadable.contents; } function getPromise(recoilValue: RecoilValue): Promise { const loadable = getRecoilValueAsLoadable(store, recoilValue); if (loadable.state !== 'loading') { throw new Error(`expected atom "${recoilValue.key}" to be a promise`); } return loadable.toPromise(); } let id = 0; function asyncSelector( dep?: RecoilValue, ): [RecoilValue, (T) => void, (Error) => void, () => boolean] { let resolve: T => void = () => invariant(false, 'bug in test code'); // make flow happy with initialization let reject: mixed => void = () => invariant(false, 'bug in test code'); let evaluated = false; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); const sel = selector({ key: `AsyncSelector${id++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => { evaluated = true; if (dep != null) { get(dep); } return promise; }, }); // $FlowFixMe[incompatible-return] return [sel, resolve, reject, () => evaluated]; } /* eslint-disable jest/valid-expect */ testRecoil('noWait - resolve', async () => { const [dep, resolve] = asyncSelector(); const pTest = expect(getValue(noWait(dep)).toPromise()).resolves.toBe(42); expect(getValue(noWait(dep)).contents).toBeInstanceOf(Promise); resolve(42); await flushPromisesAndTimers(); expect(getValue(noWait(dep)).contents).toBe(42); await pTest; }); testRecoil('noWait - reject', async () => { const [dep, _resolve, reject] = asyncSelector<$FlowFixMe, _>(); class MyError extends Error {} const pTest = expect( getValue(noWait(dep)).toPromise(), ).rejects.toBeInstanceOf(MyError); expect(getValue(noWait(dep)).contents).toBeInstanceOf(Promise); reject(new MyError()); await flushPromisesAndTimers(); expect(getValue(noWait(dep)).contents).toBeInstanceOf(MyError); await pTest; }); // TRUTH TABLE // Dependencies waitForNone waitForAny waitForAll waitForAllSettled // [loading, loading] [Promise, Promise] Promise Promise Promise // [value, loading] [value, Promise] [value, Promise] Promise Promise // [value, value] [value, value] [value, value] [value, value] [value, value] testRecoil('waitFor - resolve to values', async () => { const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>(); const [depB, resolveB] = asyncSelector<$FlowFixMe | number, _>(); const deps = [depA, depB]; // Test for initial values // watiForNone returns loadables with promises that resolve to their values expect(getValue(waitForNone(deps)).every(r => r.state === 'loading')).toBe( true, ); const depTest0 = expect( getValue(waitForNone(deps))[0].promiseMaybe(), ).resolves.toBe(0); const depTest1 = expect( getValue(waitForNone(deps))[1].promiseMaybe(), ).resolves.toBe(1); // waitForAny returns a promise that resolves to the state with the next // resolved value. So, that includes the first value and a promise for the second. expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise); const anyTest0 = expect( getPromise(waitForAny(deps)).then(value => { expect(value[0].valueMaybe()).toEqual(0); return value[0].valueMaybe(); }), ).resolves.toEqual(0); const anyTest1 = expect( getPromise(waitForAny(deps)).then(value => { expect(value[1].promiseMaybe()).toBeInstanceOf(Promise); return value[1].promiseMaybe(); }), ).resolves.toBe(1); // waitForAll returns a promise that resolves to the actual values expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise); const allTest0 = expect(getPromise(waitForAll(deps))).resolves.toEqual([ 0, 1, ]); // Resolve the first dep resolveA(0); await flushPromisesAndTimers(); expect(getValue(waitForNone(deps))[0].contents).toBe(0); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise); expect(getValue(waitForAny(deps))[0].contents).toBe(0); expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise); const allTest1 = expect(getPromise(waitForAll(deps))).resolves.toEqual([ 0, 1, ]); // Resolve the second dep resolveB(1); await flushPromisesAndTimers(); expect(getValue(waitForNone(deps))[0].contents).toBe(0); expect(getValue(waitForNone(deps))[1].contents).toBe(1); expect(getValue(waitForAny(deps))[0].contents).toBe(0); expect(getValue(waitForAny(deps))[1].contents).toBe(1); expect(getValue(waitForAll(deps))[0]).toBe(0); expect(getValue(waitForAll(deps))[1]).toBe(1); await depTest0; await depTest1; await anyTest0; await anyTest1; await allTest0; await allTest1; }); // TRUTH TABLE // Dependencies waitForNone waitForAny waitForAll waitForAllSettled // [loading, loading] [Promise, Promise] Promise Promise Promise // [error, loading] [Error, Promise] [Error, Promise] Error Promise // [error, error] [Error, Error] [Error, Error] Error [Error, Error] testRecoil('waitFor - rejected', async () => { const [depA, _resolveA, rejectA] = asyncSelector<$FlowFixMe, _>(); const [depB, _resolveB, rejectB] = asyncSelector<$FlowFixMe, _>(); const deps = [depA, depB]; class Error1 extends Error {} class Error2 extends Error {} // All deps Loading Tests expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAny(deps))).toEqual('loading'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise); const anyTest0 = expect( getPromise(waitForAny(deps)).then(res => { expect(res[0].contents).toBeInstanceOf(Error1); expect(res[1].contents).toBeInstanceOf(Promise); return 'success'; }), ).resolves.toEqual('success'); expect(getState(waitForAll(deps))).toEqual('loading'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise); const allTest0 = expect( getPromise(waitForAll(deps)).catch(err => { expect(err).toBeInstanceOf(Error1); return 'failure'; }), ).resolves.toEqual('failure'); expect(getState(waitForAllSettled(deps))).toEqual('loading'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise); const allSettledTest0 = expect( getPromise(waitForAllSettled(deps)).then(res => { expect(res[0].contents).toBeInstanceOf(Error1); expect(res[1].contents).toBeInstanceOf(Error2); return 'success'; }), ).resolves.toEqual('success'); // depA Rejected tests rejectA(new Error1()); await flushPromisesAndTimers(); expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAny(deps))).toEqual('hasValue'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array); expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAll(deps))).toEqual('hasError'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1); expect(getState(waitForAllSettled(deps))).toEqual('loading'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise); const allSettledTest1 = expect( getPromise(waitForAllSettled(deps)).then(res => { expect(res[0].contents).toBeInstanceOf(Error1); expect(res[1].contents).toBeInstanceOf(Error2); return 'success'; }), ).resolves.toEqual('success'); // depB Rejected tests rejectB(new Error2()); await flushPromisesAndTimers(); expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Error2); expect(getState(waitForAny(deps))).toEqual('hasValue'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array); expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Error2); expect(getState(waitForAll(deps))).toEqual('hasError'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1); expect(getState(waitForAllSettled(deps))).toEqual('hasValue'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array); expect(getValue(waitForAllSettled(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForAllSettled(deps))[1].contents).toBeInstanceOf(Error2); await anyTest0; await allTest0; await allSettledTest0; await allSettledTest1; }); // TRUTH TABLE // Dependencies waitForNone waitForAny waitForAll waitForAllSettled // [loading, loading] [Promise, Promise] Promise Promise Promise // [value, loading] [value, Promise] [value, Promise] Promise Promise // [value, error] [value, Error] [value, Error] Error [value, Error] testRecoil('waitFor - resolve then reject', async () => { const [depA, resolveA, _rejectA] = asyncSelector<$FlowFixMe | number, _>(); const [depB, _resolveB, rejectB] = asyncSelector<$FlowFixMe | number, _>(); const deps = [depA, depB]; class Error2 extends Error {} // All deps Loading Tests expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAny(deps))).toEqual('loading'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise); const anyTest0 = expect( getPromise(waitForAny(deps)).then(res => { expect(res[0].contents).toEqual(1); expect(res[1].contents).toBeInstanceOf(Promise); return 'success'; }), ).resolves.toEqual('success'); expect(getState(waitForAll(deps))).toEqual('loading'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise); const allTest0 = expect( getPromise(waitForAll(deps)).catch(err => { expect(err).toBeInstanceOf(Error2); return 'failure'; }), ).resolves.toEqual('failure'); expect(getState(waitForAllSettled(deps))).toEqual('loading'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise); const allSettledTest0 = expect( getPromise(waitForAllSettled(deps)).then(res => { expect(res[0].contents).toEqual(1); expect(res[1].contents).toBeInstanceOf(Error2); return 'success'; }), ).resolves.toEqual('success'); // depA Resolves tests resolveA(1); await flushPromisesAndTimers(); expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toEqual(1); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAny(deps))).toEqual('hasValue'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array); expect(getValue(waitForAny(deps))[0].contents).toEqual(1); expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAll(deps))).toEqual('loading'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise); const allTest1 = expect(getPromise(waitForAll(deps))).rejects.toBeInstanceOf( Error2, ); expect(getState(waitForAllSettled(deps))).toEqual('loading'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise); const allSettledTest1 = expect( getPromise(waitForAllSettled(deps)).then(res => { expect(res[0].contents).toEqual(1); expect(res[1].contents).toBeInstanceOf(Error2); return 'success'; }), ).resolves.toEqual('success'); // depB Rejected tests rejectB(new Error2()); await flushPromisesAndTimers(); expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toEqual(1); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Error2); expect(getState(waitForAny(deps))).toEqual('hasValue'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array); expect(getValue(waitForAny(deps))[0].contents).toEqual(1); expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Error2); expect(getState(waitForAll(deps))).toEqual('hasError'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error2); expect(getState(waitForAllSettled(deps))).toEqual('hasValue'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array); expect(getValue(waitForAllSettled(deps))[0].contents).toEqual(1); expect(getValue(waitForAllSettled(deps))[1].contents).toBeInstanceOf(Error2); await anyTest0; await allTest0; await allTest1; await allSettledTest0; await allSettledTest1; }); // TRUTH TABLE // Dependencies waitForNone waitForAny waitForAll waitForAllSettled // [loading, loading] [Promise, Promise] Promise Promise Promise // [error, loading] [Error, Promise] [Error, Promsie] Error Promise // [error, value] [Error, value] [Error, value] Error [Error, value] testRecoil('waitFor - reject then resolve', async () => { const [depA, _resolveA, rejectA] = asyncSelector<$FlowFixMe | number, _>(); const [depB, resolveB, _rejectB] = asyncSelector<$FlowFixMe | number, _>(); const deps = [depA, depB]; class Error1 extends Error {} // All deps Loading Tests expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAny(deps))).toEqual('loading'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise); const anyTest0 = expect( getPromise(waitForAny(deps)).then(res => { expect(res[0].contents).toBeInstanceOf(Error1); expect(res[1].contents).toBeInstanceOf(Promise); return 'success'; }), ).resolves.toEqual('success'); expect(getState(waitForAll(deps))).toEqual('loading'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise); const allTest0 = expect( getPromise(waitForAll(deps)).catch(err => { expect(err).toBeInstanceOf(Error1); return 'failure'; }), ).resolves.toEqual('failure'); expect(getState(waitForAllSettled(deps))).toEqual('loading'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise); const allSettledTest0 = expect( getPromise(waitForAllSettled(deps)).then(res => { expect(res[0].contents).toBeInstanceOf(Error1); expect(res[1].contents).toEqual(1); return 'success'; }), ).resolves.toEqual('success'); // depA Rejects tests rejectA(new Error1()); await flushPromisesAndTimers(); expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAny(deps))).toEqual('hasValue'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array); expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise); expect(getState(waitForAll(deps))).toEqual('hasError'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1); expect(getState(waitForAllSettled(deps))).toEqual('loading'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise); const allSettledTest1 = expect( getPromise(waitForAllSettled(deps)).then(res => { expect(res[0].contents).toBeInstanceOf(Error1); expect(res[1].contents).toEqual(1); return 'success'; }), ).resolves.toEqual('success'); // depB Resolves tests resolveB(1); await flushPromisesAndTimers(); expect(getState(waitForNone(deps))).toEqual('hasValue'); expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array); expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForNone(deps))[1].contents).toEqual(1); expect(getState(waitForAny(deps))).toEqual('hasValue'); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array); expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForAny(deps))[1].contents).toEqual(1); expect(getState(waitForAll(deps))).toEqual('hasError'); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1); expect(getState(waitForAllSettled(deps))).toEqual('hasValue'); expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array); expect(getValue(waitForAllSettled(deps))[0].contents).toBeInstanceOf(Error1); expect(getValue(waitForAllSettled(deps))[1].contents).toEqual(1); await anyTest0; await allTest0; await allSettledTest0; await allSettledTest1; }); // Similar as the first test that resolves both dependencies, but with named dependencies. testRecoil('waitFor - named dependency version', async () => { const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>(); const [depB, resolveB] = asyncSelector<$FlowFixMe | number, _>(); const deps = {a: depA, b: depB}; expect(getValue(waitForNone(deps)).a.promiseMaybe()).toBeInstanceOf(Promise); expect(getValue(waitForNone(deps)).b.promiseMaybe()).toBeInstanceOf(Promise); const depTest0 = expect( getValue(waitForNone(deps)).a.promiseMaybe(), ).resolves.toBe(0); const depTest1 = expect( getValue(waitForNone(deps)).b.promiseMaybe(), ).resolves.toBe(1); expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise); const anyTest0 = expect( getPromise(waitForAny(deps)).then(value => { expect(value.a.valueMaybe()).toEqual(0); return value.a.valueMaybe(); }), ).resolves.toEqual(0); const anyTest1 = expect( getPromise(waitForAny(deps)).then(value => { expect(value.b.promiseMaybe()).toBeInstanceOf(Promise); return value.b.promiseMaybe(); }), ).resolves.toBe(1); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise); const allTest0 = expect(getPromise(waitForAll(deps))).resolves.toEqual({ a: 0, b: 1, }); resolveA(0); await flushPromisesAndTimers(); expect(getValue(waitForNone(deps)).a.contents).toBe(0); expect(getValue(waitForNone(deps)).b.contents).toBeInstanceOf(Promise); expect(getValue(waitForAny(deps)).a.contents).toBe(0); expect(getValue(waitForAny(deps)).b.contents).toBeInstanceOf(Promise); expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise); const allTest1 = expect(getPromise(waitForAll(deps))).resolves.toEqual({ a: 0, b: 1, }); resolveB(1); await flushPromisesAndTimers(); expect(getValue(waitForNone(deps)).a.contents).toBe(0); expect(getValue(waitForNone(deps)).b.contents).toBe(1); expect(getValue(waitForAny(deps)).a.contents).toBe(0); expect(getValue(waitForAny(deps)).b.contents).toBe(1); expect(getValue(waitForAll(deps)).a).toBe(0); expect(getValue(waitForAll(deps)).b).toBe(1); await depTest0; await depTest1; await anyTest0; await anyTest1; await allTest0; await allTest1; }); testRecoil('waitForAll - Evaluated concurrently', async () => { const [depA, resolveA, _rejectA, evaluatedA] = asyncSelector< $FlowFixMe | number, _, >(); const [depB, _resolveB, _rejectB, evaluatedB] = asyncSelector< $FlowFixMe, _, >(); const deps = [depA, depB]; expect(evaluatedA()).toBe(false); expect(evaluatedB()).toBe(false); // $FlowFixMe[unused-promise] getPromise(waitForAll(deps)); await flushPromisesAndTimers(); // Confirm dependencies were evaluated in parallel expect(evaluatedA()).toBe(true); expect(evaluatedB()).toBe(true); resolveA(0); // $FlowFixMe[unused-promise] getPromise(waitForAll(deps)); await flushPromisesAndTimers(); expect(evaluatedA()).toBe(true); expect(evaluatedB()).toBe(true); }); testRecoil('waitForAll - mixed sync and async deps', async () => { const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>(); const depB = selector({ key: 'mydepkeyB', get: () => 1, }); const deps = [depA, depB]; const allTest = expect(getPromise(waitForAll(deps))).resolves.toEqual([0, 1]); resolveA(0); await flushPromisesAndTimers(); expect(getValue(waitForAll(deps))).toEqual([0, 1]); await allTest; }); /* eslint-enable jest/valid-expect */ ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_atom-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../../adt/Recoil_Loadable'; import type {RecoilValue} from '../../core/Recoil_RecoilValue'; import type {DefaultValue as $IMPORTED_TYPE$_DefaultValue} from 'Recoil_Node'; import type {RecoilState} from 'Recoil_RecoilValue'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useState, Profiler, act, DEFAULT_VALUE, DefaultValue, RecoilRoot, RecoilEnv, isRecoilValue, RecoilLoadable, isLoadable, getRecoilValueAsLoadable, setRecoilValue, useRecoilState, useRecoilCallback, useRecoilValue, useRecoilStoreID, selector, useRecoilTransactionObserver, useResetRecoilState, ReadsAtom, stringAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, atom, immutable, store; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); React = require('react'); ({useState, Profiler} = require('react')); ({act} = require('ReactTestUtils')); ({DEFAULT_VALUE, DefaultValue} = require('../../core/Recoil_Node')); ({RecoilRoot, useRecoilStoreID} = require('../../core/Recoil_RecoilRoot')); RecoilEnv = require('recoil-shared/util/Recoil_RecoilEnv'); ({isRecoilValue} = require('../../core/Recoil_RecoilValue')); ({RecoilLoadable, isLoadable} = require('../../adt/Recoil_Loadable')); ({ getRecoilValueAsLoadable, setRecoilValue, } = require('../../core/Recoil_RecoilValueInterface')); ({ useRecoilState, useResetRecoilState, useRecoilValue, } = require('../../hooks/Recoil_Hooks')); ({ useRecoilTransactionObserver, } = require('../../hooks/Recoil_SnapshotHooks')); ({useRecoilCallback} = require('../../hooks/Recoil_useRecoilCallback')); ({ ReadsAtom, stringAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); atom = require('../Recoil_atom'); selector = require('../Recoil_selector'); immutable = require('immutable'); store = makeStore(); }); function getValue(recoilValue: RecoilValue): T { return getRecoilValueAsLoadable(store, recoilValue).valueOrThrow(); } function getError(recoilValue: RecoilValue): mixed { return getRecoilValueAsLoadable(store, recoilValue).errorOrThrow(); } function getRecoilStateLoadable(recoilValue: RecoilValue): Loadable { return getRecoilValueAsLoadable(store, recoilValue); } function getRecoilStatePromise(recoilValue: RecoilValue): Promise { return getRecoilStateLoadable(recoilValue).promiseOrThrow(); } function set(recoilValue: RecoilState<$FlowFixMe>, value: mixed) { setRecoilValue(store, recoilValue, value); } function reset( recoilValue: RecoilState< $TEMPORARY$string<'DEFAULT'> | $TEMPORARY$string<'INIT'>, >, ) { setRecoilValue(store, recoilValue, DEFAULT_VALUE); } testRecoil('Key is required when creating atoms', () => { const devStatus = window.__DEV__; window.__DEV__ = true; // $FlowExpectedError[incompatible-call] expect(() => atom({default: undefined})).toThrow(); window.__DEV__ = devStatus; }); testRecoil('atom can read and write value', () => { const myAtom = atom({ key: 'atom with default', default: 'DEFAULT', }); expect(getValue(myAtom)).toBe('DEFAULT'); act(() => set(myAtom, 'VALUE')); expect(getValue(myAtom)).toBe('VALUE'); }); describe('creating two atoms with the same key', () => { let consoleErrorSpy, consoleWarnSpy; beforeEach(() => { consoleErrorSpy = jest.spyOn(console, 'error'); consoleWarnSpy = jest.spyOn(console, 'warn'); // squelch output from the actual consoles consoleErrorSpy.mockImplementation(() => undefined); consoleWarnSpy.mockImplementation(() => undefined); }); afterEach(() => { jest.restoreAllMocks(); // spys are mocks, now unmock them }); const createAtomsWithDuplicateKeys = () => { // Create two atoms with the same key const _myAtom = atom({ key: 'an atom', default: 'DEFAULT', }); const _myAtom2 = atom({ key: 'an atom', // with the same key! default: 'DEFAULT 2', }); }; describe('log behavior with __DEV__ setting', () => { const originalDEV = window.__DEV__; beforeEach(() => { window.__DEV__ = true; }); afterEach(() => { window.__DEV__ = originalDEV; }); testRecoil('logs to error and warning in development mode', () => { __DEV__ = true; createAtomsWithDuplicateKeys(); const loggedError = consoleErrorSpy.mock.calls[0]?.[0]; const loggedWarning = consoleWarnSpy.mock.calls[0]?.[0]; // either is ok, implementation difference between fb and oss expect(loggedError ?? loggedWarning).toBeDefined(); }); testRecoil('logs to error only in production mode', () => { __DEV__ = false; createAtomsWithDuplicateKeys(); const loggedError = consoleErrorSpy.mock.calls[0]?.[0]; const loggedWarning = consoleWarnSpy.mock.calls[0]?.[0]; // either is ok, implementation difference between fb and oss expect(loggedError ?? loggedWarning).toBeDefined(); }); }); testRecoil( 'disabling the duplicate checking flag stops console output ', () => { RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = false; createAtomsWithDuplicateKeys(); const loggedError = consoleErrorSpy.mock.calls[0]?.[0]; const loggedWarning = consoleWarnSpy.mock.calls[0]?.[0]; expect(loggedError).toBeUndefined(); expect(loggedWarning).toBeUndefined(); }, ); describe('support for process.env.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED if present (workaround for NextJS)', () => { const originalProcessEnv = process.env; beforeEach(() => { process.env = {...originalProcessEnv}; process.env.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = 'false'; }); afterEach(() => { process.env = originalProcessEnv; }); testRecoil('duplicate checking is disabled when true', () => { createAtomsWithDuplicateKeys(); expect(RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED).toBe(false); const loggedError = consoleErrorSpy.mock.calls[0]?.[0]; const loggedWarning = consoleWarnSpy.mock.calls[0]?.[0]; expect(loggedError).toBeUndefined(); expect(loggedWarning).toBeUndefined(); }); }); }); describe('Valid values', () => { testRecoil('atom can store null and undefined', () => { const myAtom = atom({ key: 'atom with default for null and undefined', default: 'DEFAULT', }); expect(getValue(myAtom)).toBe('DEFAULT'); act(() => set(myAtom, 'VALUE')); expect(getValue(myAtom)).toBe('VALUE'); act(() => set(myAtom, null)); expect(getValue(myAtom)).toBe(null); act(() => set(myAtom, undefined)); expect(getValue(myAtom)).toBe(undefined); act(() => set(myAtom, 'VALUE')); expect(getValue(myAtom)).toBe('VALUE'); }); testRecoil('atom can store a circular reference object', () => { class Circular { self: Circular; constructor() { this.self = this; } } const circular = new Circular(); const myAtom = atom({ key: 'atom', default: undefined, }); expect(getValue(myAtom)).toBe(undefined); act(() => set(myAtom, circular)); expect(getValue(myAtom)).toBe(circular); }); }); describe('Defaults', () => { testRecoil('default is optional', () => { const myAtom = atom<$FlowFixMe>({key: 'atom without default'}); expect(getRecoilStateLoadable(myAtom).state).toBe('loading'); act(() => set(myAtom, 'VALUE')); expect(getValue(myAtom)).toBe('VALUE'); }); testRecoil('default promise', async () => { const myAtom = atom({ key: 'atom async default', default: Promise.resolve('RESOLVE'), }); const container = renderElements(); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('"RESOLVE"'); }); testRecoil('default promise overwritten before resolution', () => { let resolveAtom; const myAtom = atom({ key: 'atom async default overwritten', default: new Promise(resolve => { resolveAtom = resolve; }), }); const [ReadsWritesAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements(); expect(container.textContent).toEqual('loading'); act(() => setAtom('SET')); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('"SET"'); act(() => resolveAtom('RESOLVE')); expect(container.textContent).toEqual('"SET"'); act(() => resetAtom()); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('"RESOLVE"'); }); // NOTE: This test intentionally throws an error testRecoil('default promise rejection', async () => { const myAtom = atom({ key: 'atom async default', default: Promise.reject(new Error('REJECT')), }); const container = renderElements(); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('error'); }); testRecoil('atom default ValueLoadable', () => { const myAtom = atom({ key: 'atom default ValueLoadable', default: RecoilLoadable.of('VALUE'), }); expect(getValue(myAtom)).toBe('VALUE'); }); testRecoil('atom default ErrorLoadable', () => { const myAtom = atom({ key: 'atom default ErrorLoadable', default: RecoilLoadable.error(new Error('ERROR')), }); expect(getError(myAtom)).toBeInstanceOf(Error); // $FlowExpectedError[incompatible-use] expect(getError(myAtom).message).toBe('ERROR'); }); testRecoil('atom default LoadingLoadable', async () => { const myAtom = atom({ key: 'atom default LoadingLoadable', default: RecoilLoadable.of(Promise.resolve('VALUE')), }); await expect(getRecoilStatePromise(myAtom)).resolves.toBe('VALUE'); }); testRecoil('atom default derived Loadable', () => { const myAtom = atom({ key: 'atom default Loadable derived', default: RecoilLoadable.of('A').map(x => x + 'B'), }); expect(getValue(myAtom)).toBe('AB'); }); testRecoil('atom default AtomValue', () => { const myAtom = atom({ key: 'atom default AtomValue', default: atom.value('VALUE'), }); expect(getValue(myAtom)).toBe('VALUE'); }); testRecoil('atom default AtomValue Loadable', async () => { const myAtom = atom>({ key: 'atom default AtomValue Loadable', default: atom.value(RecoilLoadable.of('VALUE')), }); expect(isLoadable(getValue(myAtom))).toBe(true); expect(getValue(myAtom).valueOrThrow()).toBe('VALUE'); }); testRecoil('atom default AtomValue ErrorLoadable', () => { const myAtom = atom({ key: 'atom default AtomValue Loadable Error', default: atom.value(RecoilLoadable.error('ERROR')), }); expect(isLoadable(getValue(myAtom))).toBe(true); expect(getValue(myAtom).errorOrThrow()).toBe('ERROR'); }); testRecoil('atom default AtomValue Atom', () => { const otherAtom = stringAtom(); const myAtom = atom({ key: 'atom default AtomValue Loadable Error', default: atom.value(otherAtom), }); expect(isRecoilValue(getValue(myAtom))).toBe(true); }); }); testRecoil("Updating with same value doesn't rerender", () => { const myAtom = atom({key: 'atom same value rerender', default: 'DEFAULT'}); let setAtom; let resetAtom; let renders = 0; function AtomComponent() { const [value, setValue] = useRecoilState(myAtom); const resetValue = useResetRecoilState(myAtom); setAtom = setValue; resetAtom = resetValue; return value; } expect(renders).toEqual(0); const c = renderElements( { renders++; }}> , ); // Initial render happens one time in www and 2 times in oss. // resetting the counter to 1 after the initial render to make them // the same in both repos. 2 renders probably need to be looked into. renders = 1; expect(c.textContent).toEqual('DEFAULT'); act(() => setAtom('SET')); expect(renders).toEqual(2); expect(c.textContent).toEqual('SET'); act(() => setAtom('SET')); expect(renders).toEqual(2); expect(c.textContent).toEqual('SET'); act(() => setAtom('CHANGE')); expect(renders).toEqual(3); expect(c.textContent).toEqual('CHANGE'); act(resetAtom); expect(renders).toEqual(4); expect(c.textContent).toEqual('DEFAULT'); act(resetAtom); expect(renders).toEqual(4); expect(c.textContent).toEqual('DEFAULT'); }); describe('Effects', () => { testRecoil('effect error', () => { const ERROR = new Error('ERROR'); const myAtom = atom({ key: 'atom effect error', default: 'DEFAULT', effects: [ () => { throw ERROR; }, ], }); const mySelector = selector({ key: 'atom effect error selector', get: ({get}) => { try { return get(myAtom); } catch (e) { return e.message; } }, }); const container = renderElements(); expect(container.textContent).toEqual('"ERROR"'); }); testRecoil('initialization', () => { let inited = false; const myAtom: RecoilState = atom({ key: 'atom effect init', default: 'DEFAULT', effects: [ ({node, trigger, setSelf}) => { inited = true; expect(trigger).toEqual('get'); expect(node).toBe(myAtom); setSelf('INIT'); }, ], }); expect(getValue(myAtom)).toEqual('INIT'); expect(inited).toEqual(true); }); testRecoil('async default', () => { let inited = false; const myAtom = atom({ key: 'atom effect async default', default: Promise.resolve('RESOLVE'), effects: [ ({setSelf, onSet}) => { inited = true; setSelf('INIT'); onSet(newValue => { expect(newValue).toBe('RESOLVE'); }); }, ], }); expect(inited).toEqual(false); const [ReadsWritesAtom, _, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); expect(inited).toEqual(true); expect(c.textContent).toEqual('"INIT"'); act(resetAtom); expect(c.textContent).toEqual('loading'); act(() => jest.runAllTimers()); expect(c.textContent).toEqual('"RESOLVE"'); }); testRecoil('set to Promise', async () => { let setLater; const myAtom = atom({ key: 'atom effect set promise', default: 'DEFAULT', effects: [ ({setSelf}) => { // $FlowFixMe[incompatible-call] setSelf(atom.value(Promise.resolve('INIT_PROMISE'))); setLater = setSelf; }, ], }); expect(getRecoilStateLoadable(myAtom).state).toBe('hasValue'); await expect(getRecoilStateLoadable(myAtom).contents).resolves.toBe( 'INIT_PROMISE', ); // $FlowFixMe[incompatible-call] act(() => setLater(atom.value(Promise.resolve('LATER_PROMISE')))); expect(getRecoilStateLoadable(myAtom).state).toBe('hasValue'); await expect(getRecoilStateLoadable(myAtom).contents).resolves.toBe( 'LATER_PROMISE', ); // $FlowFixMe[incompatible-call] act(() => setLater(() => atom.value(Promise.resolve('UPDATER_PROMISE')))); expect(getRecoilStateLoadable(myAtom).state).toBe('hasValue'); await expect(getRecoilStateLoadable(myAtom).contents).resolves.toBe( 'UPDATER_PROMISE', ); }); testRecoil( 'atom initialized with promise update downstream selectors when resolved', async () => { let resolveA = null; let resolveB = null; const atomA = atom({ key: 'downstream selectors when resolved/atomA', default: 'a-default', effects: [ ({setSelf}) => { setSelf( new Promise(resolve => { resolveA = resolve; }), ); }, ], }); const atomB = atom({ key: 'downstream selectors when resolved/atomB', default: 'b-default', effects: [ ({setSelf}) => { setSelf( new Promise(resolve => { resolveB = resolve; }), ); }, ], }); const selectorA = selector({ key: 'downstream selectors when resolved/selectorA', // $FlowFixMe[missing-local-annot] get: ({get}) => get(atomA), }); const directSelector = selector({ key: 'downstream selectors when resolved/directSelector', // $FlowFixMe[missing-local-annot] get: ({get}) => { const valueA = get(selectorA); const valueB = get(atomB); return `${valueA}/${valueB}`; }, }); const c = renderElements(); expect(c.textContent).toEqual('loading'); act(() => resolveA?.('a-async')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('loading'); act(() => resolveB?.('b-async')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"a-async/b-async"'); }, ); testRecoil('order of effects', () => { const myAtom = atom({ key: 'atom effect order', default: 'DEFAULT', effects: [ ({setSelf}) => { setSelf(x => { expect(x).toEqual('DEFAULT'); return 'EFFECT 1a'; }); setSelf(x => { expect(x).toEqual('EFFECT 1a'); return 'EFFECT 1b'; }); }, ({setSelf}) => { setSelf(x => { expect(x).toEqual('EFFECT 1b'); return 'EFFECT 2'; }); }, () => {}, ], }); expect(getValue(myAtom)).toEqual('EFFECT 2'); }); testRecoil('reset during init', () => { const myAtom = atom({ key: 'atom effect reset', default: 'DEFAULT', effects: [({setSelf}) => setSelf('INIT'), ({resetSelf}) => resetSelf()], }); expect(getValue(myAtom)).toEqual('DEFAULT'); }); testRecoil('init to undefined', () => { const myAtom = atom({ key: 'atom effect init undefined', default: 'DEFAULT', effects: [({setSelf}) => setSelf('INIT'), ({setSelf}) => setSelf()], }); expect(getValue(myAtom)).toEqual(undefined); }); testRecoil('init on set', () => { let inited = 0; const myAtom = atom({ key: 'atom effect - init on set', default: 'DEFAULT', effects: [ ({setSelf, trigger}) => { inited++; setSelf('INIT'); expect(trigger).toEqual('set'); }, ], }); set(myAtom, 'SET'); expect(getValue(myAtom)).toEqual('SET'); expect(inited).toEqual(1); reset(myAtom); expect(getValue(myAtom)).toEqual('DEFAULT'); expect(inited).toEqual(1); }); testRecoil('async set', () => { let setAtom, resetAtom; let effectRan = false; const myAtom = atom({ key: 'atom effect init set', default: 'DEFAULT', effects: [ ({setSelf, resetSelf}) => { setAtom = setSelf; resetAtom = resetSelf; setSelf(x => { expect(x).toEqual(effectRan ? 'INIT' : 'DEFAULT'); effectRan = true; return 'INIT'; }); }, ], }); const c = renderElements(); expect(c.textContent).toEqual('"INIT"'); // Test async set act(() => setAtom(value => { expect(value).toEqual('INIT'); return 'SET'; }), ); expect(c.textContent).toEqual('"SET"'); // Test async change act(() => setAtom(value => { expect(value).toEqual('SET'); return 'CHANGE'; }), ); expect(c.textContent).toEqual('"CHANGE"'); // Test reset act(resetAtom); expect(c.textContent).toEqual('"DEFAULT"'); // Test setting to undefined act(() => // $FlowFixMe[incompatible-call] setAtom(value => { expect(value).toEqual('DEFAULT'); return undefined; }), ); expect(c.textContent).toEqual(''); }); testRecoil('set promise', async () => { let resolveAtom; let validated; const onSetForSameEffect = jest.fn(() => {}); const myAtom = atom({ key: 'atom effect init set promise', default: 'DEFAULT', effects: [ ({setSelf, onSet}) => { setSelf( new Promise(resolve => { resolveAtom = resolve; }), ); // $FlowFixMe[invalid-tuple-arity] onSet(onSetForSameEffect); }, ({onSet}) => { onSet(value => { expect(value).toEqual('RESOLVE'); validated = true; }); }, ], }); const c = renderElements(); expect(c.textContent).toEqual('loading'); act(() => resolveAtom?.('RESOLVE')); await flushPromisesAndTimers(); act(() => undefined); expect(c.textContent).toEqual('"RESOLVE"'); expect(validated).toEqual(true); // onSet() should not be called for this hook's setSelf() expect(onSetForSameEffect).toHaveBeenCalledTimes(0); }); testRecoil('set promise via updater', async () => { let resolveAtom; let validated; const onSetForSameEffect = jest.fn(() => {}); const myAtom = atom({ key: 'atom effect init set promise', default: 'DEFAULT', effects: [ ({setSelf, onSet}) => { setSelf(() => { return new Promise(resolve => { resolveAtom = resolve; }); }); // $FlowFixMe[invalid-tuple-arity] onSet(onSetForSameEffect); }, ({onSet}) => { onSet(value => { expect(value).toEqual('RESOLVE'); validated = true; }); }, ], }); const c = renderElements(); expect(c.textContent).toEqual('loading'); act(() => resolveAtom?.('RESOLVE')); await flushPromisesAndTimers(); act(() => undefined); expect(c.textContent).toEqual('"RESOLVE"'); expect(validated).toEqual(true); // onSet() should not be called for this hook's setSelf() expect(onSetForSameEffect).toHaveBeenCalledTimes(0); }); testRecoil('set default promise', async () => { let setValue = 'RESOLVE_DEFAULT'; const onSetHandler = jest.fn(newValue => { expect(newValue).toBe(setValue); }); let resolveDefault; const myAtom = atom({ key: 'atom effect default promise', default: new Promise(resolve => { resolveDefault = resolve; }), effects: [ ({onSet}) => { // $FlowFixMe[invalid-tuple-arity] onSet(onSetHandler); }, ], }); const [ReadsWritesAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); expect(c.textContent).toEqual('loading'); act(() => resolveDefault?.('RESOLVE_DEFAULT')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"RESOLVE_DEFAULT"'); expect(onSetHandler).toHaveBeenCalledTimes(1); setValue = 'SET'; act(() => setAtom('SET')); expect(c.textContent).toEqual('"SET"'); expect(onSetHandler).toHaveBeenCalledTimes(2); setValue = 'RESOLVE_DEFAULT'; act(resetAtom); expect(c.textContent).toEqual('"RESOLVE_DEFAULT"'); expect(onSetHandler).toHaveBeenCalledTimes(3); }); testRecoil( 'when setSelf is called in onSet, then onSet is not triggered again', () => { let set1 = false; const valueToSet1 = 'value#1'; const transformedBySetSelf = 'transformed after value#1'; const myAtom = atom({ key: 'atom setSelf with set-updater', default: 'DEFAULT', effects: [ ({setSelf, onSet}) => { onSet(newValue => { expect(set1).toBe(false); if (newValue === valueToSet1) { setSelf(transformedBySetSelf); set1 = true; } }); }, ], }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); expect(c.textContent).toEqual('"DEFAULT"'); act(() => setAtom(valueToSet1)); expect(c.textContent).toEqual(`"${transformedBySetSelf}"`); }, ); testRecoil('Always call setSelf() in onSet() handler', () => { const myAtom = atom({ key: 'atom setSelf in onSet', default: 'DEFAULT', effects: [ ({setSelf, onSet}) => { onSet(newValue => { setSelf('TRANSFORM ' + newValue); }); }, ], }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); expect(c.textContent).toEqual('"DEFAULT"'); act(() => setAtom('SET')); expect(c.textContent).toEqual('"TRANSFORM SET"'); act(() => setAtom('SET2')); expect(c.textContent).toEqual('"TRANSFORM SET2"'); }); testRecoil('Patch value using setSelf() in onSet() handler', () => { let patch = 'PATCH'; const myAtom = atom({ key: 'atom patch setSelf in onSet', default: {value: 'DEFAULT', patch}, effects: [ ({setSelf, onSet}) => { onSet(newValue => { if ( !(newValue instanceof DefaultValue) && newValue.patch != patch ) { setSelf({value: 'TRANSFORM_ALT ' + newValue.value, patch}); } }); }, ({setSelf, onSet}) => { onSet(newValue => { if ( !(newValue instanceof DefaultValue) && newValue.patch != patch ) { setSelf({value: 'TRANSFORM ' + newValue.value, patch}); } }); }, ], }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); expect(c.textContent).toEqual('{"patch":"PATCH","value":"DEFAULT"}'); // $FlowFixMe[missing-local-annot] // $FlowFixMe[incompatible-exact] act(() => setAtom(x => ({...x, value: 'SET'}))); expect(c.textContent).toEqual('{"patch":"PATCH","value":"SET"}'); // $FlowFixMe[missing-local-annot] // $FlowFixMe[incompatible-exact] act(() => setAtom(x => ({...x, value: 'SET2'}))); expect(c.textContent).toEqual('{"patch":"PATCH","value":"SET2"}'); patch = 'PATCHB'; // $FlowFixMe[missing-local-annot] // $FlowFixMe[incompatible-exact] act(() => setAtom(x => ({...x, value: 'SET3'}))); expect(c.textContent).toEqual( '{"patch":"PATCHB","value":"TRANSFORM SET3"}', ); // $FlowFixMe[missing-local-annot] // $FlowFixMe[incompatible-exact] act(() => setAtom(x => ({...x, value: 'SET4'}))); expect(c.textContent).toEqual('{"patch":"PATCHB","value":"SET4"}'); }); // NOTE: This test throws an expected error testRecoil('reject promise', async () => { let rejectAtom; let validated = false; const myAtom = atom({ key: 'atom effect init reject promise', default: 'DEFAULT', effects: [ ({setSelf, onSet}) => { setSelf( new Promise((_resolve, reject) => { rejectAtom = reject; }), ); onSet(() => { validated = true; }); }, ], }); const c = renderElements(); expect(c.textContent).toEqual('loading'); act(() => rejectAtom?.(new Error('REJECT'))); await flushPromisesAndTimers(); act(() => undefined); expect(c.textContent).toEqual('error'); expect(validated).toEqual(false); }); testRecoil('overwrite promise', async () => { let resolveAtom; let validated; const myAtom = atom({ key: 'atom effect init overwrite promise', default: 'DEFAULT', effects: [ ({setSelf, onSet}) => { setSelf( new Promise(resolve => { resolveAtom = resolve; }), ); onSet(value => { expect(value).toEqual('OVERWRITE'); validated = true; }); }, ], }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); expect(c.textContent).toEqual('loading'); act(() => setAtom('OVERWRITE')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"OVERWRITE"'); // Resolving after atom is set to another value will be ignored. act(() => resolveAtom?.('RESOLVE')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"OVERWRITE"'); expect(validated).toEqual(true); }); testRecoil('abort promise init', async () => { let resolveAtom; let validated; const myAtom = atom({ key: 'atom effect abort promise init', default: 'DEFAULT', effects: [ ({setSelf, onSet}) => { setSelf( new Promise(resolve => { resolveAtom = resolve; }), ); onSet(value => { expect(value).toBe('DEFAULT'); validated = true; }); }, ], }); const c = renderElements(); expect(c.textContent).toEqual('loading'); act(() => resolveAtom?.(new DefaultValue())); await flushPromisesAndTimers(); act(() => undefined); expect(c.textContent).toEqual('"DEFAULT"'); expect(validated).toEqual(true); }); testRecoil('once per root', ({strictMode, concurrentMode}) => { let inited = 0; const myAtom = atom({ key: 'atom effect once per root', default: 'DEFAULT', effects: [ ({setSelf}) => { inited++; setSelf('INIT'); }, ], }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); // effect is called once per const c1 = renderElements(); const c2 = renderElements(); expect(c1.textContent).toEqual('"INIT"'); expect(c2.textContent).toEqual('"INIT"'); act(() => setAtom('SET')); expect(c1.textContent).toEqual('"SET"'); expect(c2.textContent).toEqual('"INIT"'); expect(inited).toEqual(strictMode && concurrentMode ? 4 : 2); }); testRecoil('onSet', () => { const oldSets = {a: 0, b: 0}; const newSets = {a: 0, b: 0}; const observer = (key: $TEMPORARY$string<'a'> | $TEMPORARY$string<'b'>) => ( newValue: number, oldValue: number | $IMPORTED_TYPE$_DefaultValue, isReset: boolean, ) => { expect(oldValue).toEqual(oldSets[key]); expect(newValue).toEqual(newSets[key]); expect(isReset).toEqual(newValue === 0); oldSets[key] = newValue; }; const atomA = atom({ key: 'atom effect onSet A', default: 0, effects: [({onSet}) => onSet(observer('a'))], }); const atomB = atom({ key: 'atom effect onSet B', default: 0, effects: [({onSet}) => onSet(observer('b'))], }); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); const c = renderElements( <> , ); expect(oldSets).toEqual({a: 0, b: 0}); expect(c.textContent).toEqual('00'); newSets.a = 1; act(() => setA(1)); expect(c.textContent).toEqual('10'); newSets.a = 2; act(() => setA(2)); expect(c.textContent).toEqual('20'); newSets.b = 1; act(() => setB(1)); expect(c.textContent).toEqual('21'); newSets.a = 0; act(() => resetA()); expect(c.textContent).toEqual('01'); }); testRecoil('onSet ordering', () => { let set1 = false; let set2 = false; let globalObserver = false; const myAtom = atom({ key: 'atom effect onSet ordering', default: 'DEFAULT', effects: [ ({onSet}) => { onSet(() => { expect(set2).toBe(false); set1 = true; }); onSet(() => { expect(set1).toBe(true); set2 = true; }); }, ], }); function TransactionObserver({ callback, }: $TEMPORARY$object<{callback: () => void}>) { useRecoilTransactionObserver(callback); return null; } const [AtomA, setA] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements( <> { expect(set1).toBe(true); expect(set2).toBe(true); globalObserver = true; }} /> , ); expect(set1).toEqual(false); expect(set2).toEqual(false); // $FlowFixMe[incompatible-call] act(() => setA(1)); expect(set1).toEqual(true); expect(set2).toEqual(true); expect(globalObserver).toEqual(true); expect(c.textContent).toEqual('1'); }); testRecoil('onSet History', () => { const history: Array<() => void> = []; // Array of undo functions /* $FlowFixMe[missing-local-annot] The type annotation(s) required by * Flow's LTI update could not be added via codemod */ function historyEffect({setSelf, onSet}) { onSet((_, oldValue) => { history.push(() => { setSelf(oldValue); }); }); } const atomA = atom({ key: 'atom effect onSte history A', default: 'DEFAULT_A', effects: [historyEffect], }); const atomB = atom({ key: 'atom effect onSte history B', default: 'DEFAULT_B', effects: [historyEffect], }); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); const c = renderElements( <> , ); expect(c.textContent).toEqual('"DEFAULT_A""DEFAULT_B"'); act(() => setA('SET_A')); expect(c.textContent).toEqual('"SET_A""DEFAULT_B"'); act(() => setB('SET_B')); expect(c.textContent).toEqual('"SET_A""SET_B"'); act(() => setB('CHANGE_B')); expect(c.textContent).toEqual('"SET_A""CHANGE_B"'); act(resetA); expect(c.textContent).toEqual('"DEFAULT_A""CHANGE_B"'); expect(history.length).toEqual(4); act(() => history.pop()()); expect(c.textContent).toEqual('"SET_A""CHANGE_B"'); act(() => history.pop()()); expect(c.textContent).toEqual('"SET_A""SET_B"'); act(() => history.pop()()); expect(c.textContent).toEqual('"SET_A""DEFAULT_B"'); act(() => history.pop()()); expect(c.textContent).toEqual('"DEFAULT_A""DEFAULT_B"'); }); testRecoil('Cleanup Handlers - when root unmounted', () => { const refCountsA = [0, 0]; const refCountsB = [0, 0]; const atomA = atom({ key: 'atom effect cleanup - A', default: 'A', effects: [ () => { refCountsA[0]++; return () => { refCountsA[0]--; }; }, () => { refCountsA[1]++; return () => { refCountsA[1]--; }; }, ], }); const atomB = atom({ key: 'atom effect cleanup - B', default: 'B', effects: [ () => { refCountsB[0]++; return () => { refCountsB[0]--; }; }, () => { refCountsB[1]++; return () => { refCountsB[1]--; }; }, ], }); let setNumRoots; function App() { const [numRoots, _setNumRoots] = useState(0); setNumRoots = _setNumRoots; return (

{Array(numRoots) .fill(null) .map((_, idx) => ( ))}
); } const c = renderElements(); expect(c.textContent).toBe(''); expect(refCountsA).toEqual([0, 0]); expect(refCountsB).toEqual([0, 0]); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCountsA).toEqual([1, 1]); expect(refCountsB).toEqual([1, 1]); act(() => setNumRoots(2)); expect(c.textContent).toBe('"A""B""A""B"'); expect(refCountsA).toEqual([2, 2]); expect(refCountsB).toEqual([2, 2]); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCountsA).toEqual([1, 1]); expect(refCountsB).toEqual([1, 1]); act(() => setNumRoots(0)); expect(c.textContent).toBe(''); expect(refCountsA).toEqual([0, 0]); expect(refCountsB).toEqual([0, 0]); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCountsA).toEqual([1, 1]); expect(refCountsB).toEqual([1, 1]); act(() => setNumRoots(0)); expect(c.textContent).toBe(''); expect(refCountsA).toEqual([0, 0]); expect(refCountsB).toEqual([0, 0]); }); testRecoil('onSet unsubscribes', () => { let onSetRan = 0; const myAtom = atom({ key: 'atom effects onSet unsubscribe', default: 'DEFAULT', effects: [ ({onSet}) => { onSet(() => { onSetRan++; }); }, ], }); let setMount: boolean => void = _ => { throw new Error('Test Error'); }; const [ReadWriteAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); function Component() { const [mount, setState] = useState(false); setMount = setState; return mount ? ( ) : ( 'UNMOUNTED' ); } const c = renderElements(); expect(c.textContent).toBe('UNMOUNTED'); expect(onSetRan).toBe(0); act(() => setMount(true)); expect(c.textContent).toBe('"DEFAULT"'); expect(onSetRan).toBe(0); act(() => setAtom('SET')); expect(c.textContent).toBe('"SET"'); expect(onSetRan).toBe(1); act(() => setMount(false)); expect(c.textContent).toBe('UNMOUNTED'); expect(onSetRan).toBe(1); // onSet() handler not called after store is unmounted and effects cleanedup act(() => setAtom('SET INVALID')); expect(c.textContent).toBe('UNMOUNTED'); expect(onSetRan).toBe(1); act(() => setMount(true)); expect(c.textContent).toBe('"DEFAULT"'); expect(onSetRan).toBe(1); act(() => setAtom('SET2')); expect(c.textContent).toBe('"SET2"'); expect(onSetRan).toBe(2); }); // Test that effects can initialize state when an atom is first used after an // action that also updated another atom's state. // This corner case was reported by multiple customers. testRecoil('initialize concurrent with state update', () => { const myAtom = atom({ key: 'atom effect - concurrent update', default: 'DEFAULT', effects: [({setSelf}) => setSelf('INIT')], }); const otherAtom = atom({ key: 'atom effect - concurrent update / other atom', default: 'OTHER_DEFAULT', }); const [OtherAtom, setOtherAtom] = componentThatReadsAndWritesAtom(otherAtom); function NewPage() { return ; } let renderPage; function App() { const [showPage, setShowPage] = useState(false); renderPage = () => setShowPage(true); return ( <> {showPage && } ); } const c = renderElements(); // is not yet rendered expect(c.textContent).toEqual('"OTHER_DEFAULT"'); // Render which initializes myAtom via effect while also // updating an unrelated atom. act(() => { renderPage(); setOtherAtom('OTHER'); }); expect(c.textContent).toEqual('"OTHER""INIT"'); }); testRecoil( 'atom effect runs twice when atom is read from a snapshot and the atom is read for first time in that snapshot', ({strictMode, concurrentMode}) => { let numTimesEffectInit = 0; let latestSetSelf = (a: number) => a; const atomWithEffect = atom({ key: 'atomWithEffect', default: 0, effects: [ ({setSelf}) => { // $FlowFixMe[incompatible-type] latestSetSelf = setSelf; setSelf(1); // to accurately reproduce minimal reproducible example based on GitHub #1107 issue numTimesEffectInit++; }, ], }); const Component = () => { const readSelFromSnapshot = useRecoilCallback(({snapshot}) => () => { snapshot.getLoadable(atomWithEffect); }); readSelFromSnapshot(); // first initialization; return useRecoilValue(atomWithEffect); // second initialization; }; const c = renderElements(); expect(c.textContent).toBe('1'); expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2); act(() => latestSetSelf(100)); expect(c.textContent).toBe('100'); expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2); act(() => latestSetSelf(200)); expect(c.textContent).toBe('200'); expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2); }, ); /** * See github issue #1107 item #1 */ testRecoil( 'atom effect runs twice when selector that depends on that atom is read from a snapshot and the atom is read for first time in that snapshot', ({strictMode, concurrentMode}) => { let numTimesEffectInit = 0; let latestSetSelf = (a: number) => a; const atomWithEffect = atom({ key: 'atomWithEffect', default: 0, effects: [ ({setSelf}) => { // $FlowFixMe[incompatible-type] latestSetSelf = setSelf; setSelf(1); // to accurately reproduce minimal reproducible example based on GitHub #1107 issue numTimesEffectInit++; }, ], }); const selThatDependsOnAtom = selector({ key: 'selThatDependsOnAtom', // $FlowFixMe[missing-local-annot] get: ({get}) => get(atomWithEffect), }); const Component = () => { const readSelFromSnapshot = useRecoilCallback(({snapshot}) => () => { snapshot.getLoadable(selThatDependsOnAtom); }); readSelFromSnapshot(); // first initialization; return useRecoilValue(selThatDependsOnAtom); // second initialization; }; const c = renderElements(); expect(c.textContent).toBe('1'); expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2); act(() => latestSetSelf(100)); expect(c.textContent).toBe('100'); expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2); act(() => latestSetSelf(200)); expect(c.textContent).toBe('200'); expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2); }, ); describe('Other Atoms', () => { testRecoil('init from other atom', () => { const myAtom = atom({ key: 'atom effect - init from other atom', default: 'DEFAULT', effects: [ ({node, setSelf, getLoadable, getInfo_UNSTABLE}) => { const otherValue = getLoadable(otherAtom).contents; expect(otherValue).toEqual('OTHER'); expect(getInfo_UNSTABLE(node).isSet).toBe(false); expect(getInfo_UNSTABLE(otherAtom).isSet).toBe(false); expect(getInfo_UNSTABLE(otherAtom).loadable?.contents).toBe( 'OTHER', ); // $FlowFixMe[incompatible-call] setSelf(otherValue); }, ], }); const otherAtom = atom({ key: 'atom effect - other atom', default: 'OTHER', }); expect(getValue(myAtom)).toEqual('OTHER'); }); testRecoil('init from other atom async', async () => { const myAtom = atom({ key: 'atom effect - init from other atom async', default: 'DEFAULT', effects: [ ({setSelf, getPromise}) => { const otherValue = getPromise(otherAtom); setSelf(otherValue); }, ], }); const otherAtom = atom({ key: 'atom effect - other atom async', default: Promise.resolve('OTHER'), }); await expect( getRecoilStateLoadable(myAtom).promiseOrThrow(), ).resolves.toEqual('OTHER'); }); testRecoil('async get other atoms', async () => { let initTest1 = null; let initTest2: null | void = null; let initTest3: null | void = null; let initTest4: null | void = null; let initTest5: null | void = null; let initTest6: null | void = null; let setTest = null; // StrictMode will render twice let firstRender = true; const myAtom: RecoilState = atom({ key: 'atom effect - async get', default: 'DEFAULT', effects: [ // Test we can get default values ({node, getLoadable, getPromise, getInfo_UNSTABLE}) => { expect(getLoadable(node).contents).toEqual( firstRender ? 'DEFAULT' : 'INIT', ); expect(getInfo_UNSTABLE(node).isSet).toBe(!firstRender); expect(getInfo_UNSTABLE(node).loadable?.contents).toBe( firstRender ? 'DEFAULT' : 'INIT', ); // eslint-disable-next-line jest/valid-expect initTest1 = expect(getPromise(asyncAtom)).resolves.toEqual('ASYNC'); }, ({setSelf}) => { setSelf('INIT'); }, // Test we can get value from previous initialization ({node, getLoadable, getInfo_UNSTABLE}) => { expect(getLoadable(node).contents).toEqual('INIT'); expect(getInfo_UNSTABLE(node).isSet).toBe(true); expect(getInfo_UNSTABLE(node).loadable?.contents).toBe('INIT'); }, // Test we can asynchronously get "current" values of both self and other atoms // This will be executed when myAtom is set, but checks both atoms. ({onSet, getLoadable, getPromise, getInfo_UNSTABLE}) => { onSet(x => { expect(x).toEqual('SET_ATOM'); expect(getLoadable(myAtom).contents).toEqual(x); expect(getInfo_UNSTABLE(myAtom).isSet).toBe(true); expect(getInfo_UNSTABLE(myAtom).loadable?.contents).toBe( 'SET_ATOM', ); // eslint-disable-next-line jest/valid-expect setTest = expect(getPromise(asyncAtom)).resolves.toEqual( 'SET_OTHER', ); }); }, () => { firstRender = false; }, ], }); const asyncAtom: RecoilState< Promise<$IMPORTED_TYPE$_DefaultValue> | Promise | string, > = atom({ key: 'atom effect - other atom async get', default: Promise.resolve('ASYNC_DEFAULT'), effects: [ ({setSelf}) => void setSelf(Promise.resolve('ASYNC')), ({getPromise, getInfo_UNSTABLE}) => { expect(getInfo_UNSTABLE(asyncAtom).isSet).toBe(true); // eslint-disable-next-line jest/valid-expect initTest2 = expect( getInfo_UNSTABLE(asyncAtom).loadable?.toPromise(), ).resolves.toBe('ASYNC'); // eslint-disable-next-line jest/valid-expect initTest3 = expect(getPromise(asyncAtom)).resolves.toEqual('ASYNC'); }, // Test that we can read default for an aborted initialization ({setSelf}) => void setSelf(Promise.resolve(new DefaultValue())), ({getPromise, getInfo_UNSTABLE}) => { expect(getInfo_UNSTABLE(asyncAtom).isSet).toBe(true); // TODO sketchy... // eslint-disable-next-line jest/valid-expect initTest4 = expect( getInfo_UNSTABLE(asyncAtom).loadable?.toPromise(), ).resolves.toBe('ASYNC_DEFAULT'); // eslint-disable-next-line jest/valid-expect initTest5 = expect(getPromise(asyncAtom)).resolves.toEqual( 'ASYNC_DEFAULT', ); }, // Test initializing to async value and other atom can read it ({setSelf}) => void setSelf(Promise.resolve('ASYNC')), // Test we can also read it ourselves ({getInfo_UNSTABLE}) => { expect(getInfo_UNSTABLE(asyncAtom).isSet).toBe(true); // eslint-disable-next-line jest/valid-expect initTest6 = expect( getInfo_UNSTABLE(asyncAtom).loadable?.toPromise(), ).resolves.toBe('ASYNC'); }, ], }); const [MyAtom, setMyAtom] = componentThatReadsAndWritesAtom(myAtom); const [AsyncAtom, setAsyncAtom] = componentThatReadsAndWritesAtom(asyncAtom); const c = renderElements( <> , ); await flushPromisesAndTimers(); expect(c.textContent).toBe('"INIT""ASYNC"'); expect(initTest1).not.toBe(null); await initTest1; expect(initTest2).not.toBe(null); await initTest2; expect(initTest3).not.toBe(null); await initTest3; expect(initTest4).not.toBe(null); await initTest4; expect(initTest5).not.toBe(null); await initTest5; expect(initTest6).not.toBe(null); await initTest6; act(() => setAsyncAtom('SET_OTHER')); act(() => setMyAtom('SET_ATOM')); expect(setTest).not.toBe(null); await setTest; }); }); testRecoil('storeID matches ', async () => { let effectStoreID; const myAtom = atom({ key: 'atom effect - storeID', default: 'DEFAULT', effects: [ ({storeID, setSelf}) => { effectStoreID = storeID; setSelf('INIT'); }, ], }); let rootStoreID; function StoreID() { rootStoreID = useRecoilStoreID(); return null; } const c = renderElements(
, ); expect(c.textContent).toEqual('"INIT"'); expect(effectStoreID).not.toEqual(undefined); expect(effectStoreID).toEqual(rootStoreID); }); testRecoil('parentStoreID matches ', async () => { const myAtom = atom({ key: 'atom effect - parentStoreID', effects: [ // $FlowFixMe[missing-local-annot] ({parentStoreID_UNSTABLE, setSelf}) => { setSelf(parentStoreID_UNSTABLE); }, ], }); let prefetch; function PrefetchComponent() { const storeID = useRecoilStoreID(); prefetch = useRecoilCallback(({snapshot}) => () => { const parentStoreID = snapshot.getLoadable(myAtom).getValue(); expect(storeID).toBe(parentStoreID); }); } renderElements(); act(prefetch); }); }); testRecoil('object is frozen when stored in atom', async () => { const devStatus = window.__DEV__; window.__DEV__ = true; const anAtom = atom<{x: mixed, ...}>({key: 'atom frozen', default: {x: 0}}); function valueAfterSettingInAtom(value: T): T { act(() => set(anAtom, value)); return value; } /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function isFrozen(value, getter = x => x) { const object = valueAfterSettingInAtom({x: value}); return Object.isFrozen(getter(object.x)); } expect(isFrozen({y: 0})).toBe(true); // React elements are not deep-frozen (they are already shallow-frozen on creation): const element = { ...(
), _owner: {ifThisWereAReactFiberItShouldNotBeFrozen: true}, }; expect(isFrozen(element, x => (x: any)._owner)).toBe(false); // flowlint-line unclear-type:off // Immutable stuff is not frozen: expect(isFrozen(immutable.List())).toBe(false); expect(isFrozen(immutable.Map())).toBe(false); expect(isFrozen(immutable.OrderedMap())).toBe(false); expect(isFrozen(immutable.Set())).toBe(false); expect(isFrozen(immutable.OrderedSet())).toBe(false); expect(isFrozen(immutable.Seq())).toBe(false); expect(isFrozen(immutable.Stack())).toBe(false); expect(isFrozen(immutable.Range())).toBe(false); expect(isFrozen(immutable.Repeat())).toBe(false); expect(isFrozen(new (immutable.Record({}))())).toBe(false); // Default values are frozen const defaultFrozenAtom = atom({ key: 'atom frozen default', default: {state: 'frozen', nested: {state: 'frozen'}}, }); expect(Object.isFrozen(getValue(defaultFrozenAtom))).toBe(true); expect(Object.isFrozen(getValue(defaultFrozenAtom).nested)).toBe(true); // Async Default values are frozen const defaultFrozenAsyncAtom = atom({ key: 'atom frozen default async', default: Promise.resolve({state: 'frozen', nested: {state: 'frozen'}}), }); await expect( getRecoilStatePromise(defaultFrozenAsyncAtom).then(x => Object.isFrozen(x)), ).resolves.toBe(true); expect(Object.isFrozen(getValue(defaultFrozenAsyncAtom).nested)).toBe(true); // Initialized values are frozen const initializedValueInAtom = atom({ key: 'atom frozen initialized', default: {nested: 'DEFAULT'}, effects: [ // $FlowFixMe[incompatible-call] ({setSelf}) => setSelf({state: 'frozen', nested: {state: 'frozen'}}), ], }); expect(Object.isFrozen(getValue(initializedValueInAtom))).toBe(true); expect(Object.isFrozen(getValue(initializedValueInAtom).nested)).toBe(true); // Async Initialized values are frozen const initializedAsyncValueInAtom = atom<{state: string, nested: {...}, ...}>( { key: 'atom frozen initialized async', default: {state: 'DEFAULT', nested: {state: 'DEFAULT'}}, effects: [ ({setSelf}) => setSelf( Promise.resolve({state: 'frozen', nested: {state: 'frozen'}}), ), ], }, ); await expect( getRecoilStatePromise(initializedAsyncValueInAtom).then(x => Object.isFrozen(x), ), ).resolves.toBe(true); expect(Object.isFrozen(getValue(initializedAsyncValueInAtom).nested)).toBe( true, ); expect(getValue(initializedAsyncValueInAtom).nested).toEqual({ state: 'frozen', }); // dangerouslyAllowMutability const thawedAtom = atom({ key: 'atom frozen thawed', default: {state: 'thawed', nested: {state: 'thawed'}}, dangerouslyAllowMutability: true, }); expect(Object.isFrozen(getValue(thawedAtom))).toBe(false); expect(Object.isFrozen(getValue(thawedAtom).nested)).toBe(false); window.__DEV__ = devStatus; }); ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_atomFamily-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Store} from '../../core/Recoil_State'; import type {Parameter} from 'Recoil_atomFamily'; import type {NodeKey, StoreID as StoreIDType} from 'Recoil_Keys'; import type {RecoilState} from 'Recoil_RecoilValue'; import type {Node} from 'react'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let store: Store, React, Profiler, useState, act, RecoilRoot, getRecoilValueAsLoadable, setRecoilValue, useRecoilState, useRecoilValue, useSetRecoilState, useSetUnvalidatedAtomValues, useRecoilStoreID, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, reactMode, stableStringify, atom, atomFamily, selectorFamily, RecoilLoadable, pAtom; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); React = require('react'); ({Profiler, useState} = require('react')); ({act} = require('ReactTestUtils')); ({RecoilRoot, useRecoilStoreID} = require('../../core/Recoil_RecoilRoot')); ({ getRecoilValueAsLoadable, setRecoilValue, } = require('../../core/Recoil_RecoilValueInterface')); ({ useRecoilState, useRecoilValue, useSetRecoilState, useSetUnvalidatedAtomValues, } = require('../../hooks/Recoil_Hooks')); ({ ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({reactMode} = require('../../core/Recoil_ReactMode')); stableStringify = require('recoil-shared/util/Recoil_stableStringify'); atom = require('../Recoil_atom'); atomFamily = require('../Recoil_atomFamily'); selectorFamily = require('../Recoil_selectorFamily'); ({RecoilLoadable} = require('../../adt/Recoil_Loadable')); store = makeStore(); pAtom = atomFamily<_, {k: string} | {x: string} | {y: string}>({ key: 'pAtom', default: 'fallback', }); }); let fbOnlyTest = test.skip; // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-type] // @fb-only: fbOnlyTest = testRecoil; let id = 0; /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function get(recoilValue) { return getRecoilValueAsLoadable(store, recoilValue).contents; } /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function getLoadable(recoilValue) { return getRecoilValueAsLoadable(store, recoilValue); } function set( /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ recoilValue, value: | void | number | $TEMPORARY$string<'VALUE'> | $TEMPORARY$string<'bar'> | $TEMPORARY$string<'eggs'> | $TEMPORARY$string<'spam'> | $TEMPORARY$string<'xValue'> | $TEMPORARY$string<'xValue1'> | $TEMPORARY$string<'xValue2'> | $TEMPORARY$string<'xValue3'> | $TEMPORARY$string<'xValue4'> | $TEMPORARY$string<'yValue'> | $TEMPORARY$string<'yValue1'> | $TEMPORARY$string<'yValue2'> | $TEMPORARY$string<'yValue3'> | $TEMPORARY$string<'yValue4'>, ) { setRecoilValue(store, recoilValue, value); } testRecoil('Read fallback by default', () => { expect(get(pAtom({k: 'x'}))).toBe('fallback'); }); testRecoil('Uses value for parameter', () => { set(pAtom({k: 'x'}), 'xValue'); set(pAtom({k: 'y'}), 'yValue'); expect(get(pAtom({k: 'x'}))).toBe('xValue'); expect(get(pAtom({k: 'y'}))).toBe('yValue'); expect(get(pAtom({k: 'z'}))).toBe('fallback'); }); testRecoil('Works with non-overlapping sets', () => { set(pAtom({x: 'x'}), 'xValue'); set(pAtom({y: 'y'}), 'yValue'); expect(get(pAtom({x: 'x'}))).toBe('xValue'); expect(get(pAtom({y: 'y'}))).toBe('yValue'); }); describe('Default', () => { testRecoil('default is optional', () => { const myAtom = atom<$FlowFixMe>({key: 'atom without default'}); expect(getLoadable(myAtom).state).toBe('loading'); act(() => set(myAtom, 'VALUE')); expect(get(myAtom)).toBe('VALUE'); }); testRecoil('Works with atom default', () => { const fallbackAtom = atom({key: 'fallback', default: 0}); const hasFallback = atomFamily<_, {k: string}>({ key: 'hasFallback', default: fallbackAtom, }); expect(get(hasFallback({k: 'x'}))).toBe(0); set(fallbackAtom, 1); expect(get(hasFallback({k: 'x'}))).toBe(1); set(hasFallback({k: 'x'}), 2); expect(get(hasFallback({k: 'x'}))).toBe(2); expect(get(hasFallback({k: 'y'}))).toBe(1); }); testRecoil('Works with parameterized default', () => { const paramDefaultAtom = atomFamily<_, {num: number}>({ key: 'parameterized default', // $FlowFixMe[missing-local-annot] default: ({num}) => num, }); expect(get(paramDefaultAtom({num: 1}))).toBe(1); expect(get(paramDefaultAtom({num: 2}))).toBe(2); set(paramDefaultAtom({num: 1}), 3); expect(get(paramDefaultAtom({num: 1}))).toBe(3); expect(get(paramDefaultAtom({num: 2}))).toBe(2); }); testRecoil('Parameterized async default', async () => { const paramDefaultAtom = atomFamily<_, {num: number}>({ key: 'parameterized async default', // $FlowFixMe[missing-local-annot] default: ({num}) => num === 1 ? Promise.reject(num) : Promise.resolve(num), }); await expect(get(paramDefaultAtom({num: 1}))).rejects.toBe(1); await expect(get(paramDefaultAtom({num: 2}))).resolves.toBe(2); set(paramDefaultAtom({num: 1}), 3); expect(get(paramDefaultAtom({num: 1}))).toBe(3); expect(get(paramDefaultAtom({num: 2}))).toBe(2); }); testRecoil('Parameterized loadable default', async () => { const paramDefaultAtom = atomFamily<_, {num: number}>({ key: 'parameterized loadable default', // $FlowFixMe[missing-local-annot] default: ({num}) => // $FlowFixMe[underconstrained-implicit-instantiation] num === 1 ? RecoilLoadable.error(num) : RecoilLoadable.of(num), }); expect(getLoadable(paramDefaultAtom({num: 1})).state).toBe('hasError'); expect(getLoadable(paramDefaultAtom({num: 1})).contents).toBe(1); expect(getLoadable(paramDefaultAtom({num: 2})).state).toBe('hasValue'); expect(getLoadable(paramDefaultAtom({num: 2})).contents).toBe(2); set(paramDefaultAtom({num: 1}), 3); expect(getLoadable(paramDefaultAtom({num: 1})).state).toBe('hasValue'); expect(getLoadable(paramDefaultAtom({num: 1})).contents).toBe(3); expect(getLoadable(paramDefaultAtom({num: 2})).state).toBe('hasValue'); expect(getLoadable(paramDefaultAtom({num: 2})).contents).toBe(2); }); }); testRecoil('Works with date as parameter', () => { const dateAtomFamily = atomFamily<_, Date>({ key: 'dateFamily', // $FlowFixMe[missing-local-annot] default: _date => 0, }); expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(0); expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0); set(dateAtomFamily(new Date(2021, 2, 25)), 1); expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(1); expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0); }); testRecoil('Works with parameterized fallback', () => { const fallbackAtom = atomFamily<_, $FlowFixMe | {num: number}>({ key: 'parameterized fallback default', // $FlowFixMe[missing-local-annot] default: ({num}) => num * 10, }); const paramFallbackAtom = atomFamily<_, {num: number}>({ key: 'parameterized fallback', default: fallbackAtom, }); expect(get(paramFallbackAtom({num: 1}))).toBe(10); expect(get(paramFallbackAtom({num: 2}))).toBe(20); set(paramFallbackAtom({num: 1}), 3); expect(get(paramFallbackAtom({num: 1}))).toBe(3); expect(get(paramFallbackAtom({num: 2}))).toBe(20); set(fallbackAtom({num: 2}), 200); expect(get(paramFallbackAtom({num: 2}))).toBe(200); set(fallbackAtom({num: 1}), 100); expect(get(paramFallbackAtom({num: 1}))).toBe(3); expect(get(paramFallbackAtom({num: 2}))).toBe(200); }); testRecoil('atomFamily async fallback', async () => { const paramFallback = atomFamily<_, {}>({ key: 'paramaterizedAtom async Fallback', default: Promise.resolve(42), }); const container = renderElements(); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('42'); }); testRecoil('Parameterized fallback with atom and async', async () => { const paramFallback = atomFamily<_, {param: string}>({ key: 'parameterized async Fallback', // $FlowFixMe[missing-local-annot] default: ({param}) => ({ value: 'value', atom: atom({key: `param async fallback atom ${id++}`, default: 'atom'}), async: Promise.resolve('async'), })[param], }); const valueCont = renderElements( , ); expect(valueCont.textContent).toEqual('"value"'); const atomCont = renderElements( , ); expect(atomCont.textContent).toEqual('"atom"'); const asyncCont = renderElements( , ); expect(asyncCont.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(asyncCont.textContent).toEqual('"async"'); }); fbOnlyTest('atomFamily with scope', () => { const scopeForParamAtom = atom({ key: 'scope atom for atomFamily', default: 'foo', }); const paramAtomWithScope = atomFamily({ key: 'parameterized atom with scope', default: 'default', scopeRules_APPEND_ONLY_READ_THE_DOCS: [[scopeForParamAtom]], }); expect(get(paramAtomWithScope({k: 'x'}))).toBe('default'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('default'); set(paramAtomWithScope({k: 'x'}), 'xValue1'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue1'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('default'); set(paramAtomWithScope({k: 'y'}), 'yValue1'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue1'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('yValue1'); set(scopeForParamAtom, 'bar'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('default'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('default'); set(paramAtomWithScope({k: 'x'}), 'xValue2'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue2'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('default'); set(paramAtomWithScope({k: 'y'}), 'yValue2'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue2'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('yValue2'); }); fbOnlyTest('atomFamily with parameterized scope', () => { const paramScopeForParamAtom = atomFamily({ key: 'scope atom for atomFamily with parameterized scope', default: ({namespace}) => namespace, }); const paramAtomWithParamScope = atomFamily({ key: 'parameterized atom with parameterized scope', default: 'default', scopeRules_APPEND_ONLY_READ_THE_DOCS: [ [({n}) => paramScopeForParamAtom({namespace: n})], ], }); expect(get(paramScopeForParamAtom({namespace: 'foo'}))).toBe('foo'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('default'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'foo', k: 'x'}), 'xValue1'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue1'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'foo', k: 'y'}), 'yValue1'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue1'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('yValue1'); set(paramScopeForParamAtom({namespace: 'foo'}), 'eggs'); expect(get(paramScopeForParamAtom({namespace: 'foo'}))).toBe('eggs'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('default'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'foo', k: 'x'}), 'xValue2'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue2'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'foo', k: 'y'}), 'yValue2'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue2'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('yValue2'); expect(get(paramScopeForParamAtom({namespace: 'bar'}))).toBe('bar'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('default'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'bar', k: 'x'}), 'xValue3'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue3'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'bar', k: 'y'}), 'yValue3'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue3'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('yValue3'); set(paramScopeForParamAtom({namespace: 'bar'}), 'spam'); expect(get(paramScopeForParamAtom({namespace: 'bar'}))).toBe('spam'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('default'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'bar', k: 'x'}), 'xValue4'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue4'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'bar', k: 'y'}), 'yValue4'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue4'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('yValue4'); }); testRecoil('Returns the fallback for parameterized atoms', () => { let theAtom = null; let setUnvalidatedAtomValues; let setAtomParam; let setAtomValue; function SetsUnvalidatedAtomValues() { setUnvalidatedAtomValues = useSetUnvalidatedAtomValues(); return null; } let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(false); setVisible = mySetVisible; return visible ? children : null; } function MyReadsAtom({ getAtom, }: $TEMPORARY$object<{ getAtom: () => null | (Parameter => RecoilState), }>) { const [param, setParam] = useState({num: 1}); setAtomParam = setParam; // flowlint-next-line unclear-type:off const myAtom: any = getAtom(); const [value, setValue] = useRecoilState(myAtom(param)); setAtomValue = setValue; return value; } const container = renderElements( <> theAtom} /> , ); act(() => { setUnvalidatedAtomValues( new Map().set('notDefinedYetAtomFamilyWithFallback', 123), ); }); const fallback = atom({ key: 'fallback for atomFamily', default: 222, }); theAtom = atomFamily<_, Parameter>({ key: 'notDefinedYetAtomFamilyWithFallback', default: fallback, persistence_UNSTABLE: { type: 'url', validator: (_, returnFallback) => returnFallback, }, }); act(() => { setVisible(true); }); expect(container.textContent).toBe('222'); act(() => { setAtomValue(111); }); expect(container.textContent).toBe('111'); act(() => { setAtomParam({num: 2}); }); expect(container.textContent).toBe('222'); }); testRecoil( 'Returns the fallback for parameterized atoms with a selector as the fallback', () => { let theAtom = null; let setUnvalidatedAtomValues; let setAtomParam; let setAtomValue; function SetsUnvalidatedAtomValues() { setUnvalidatedAtomValues = useSetUnvalidatedAtomValues(); return null; } let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(false); setVisible = mySetVisible; return visible ? children : null; } /* $FlowFixMe[missing-local-annot] The type annotation(s) required by * Flow's LTI update could not be added via codemod */ function MyReadsAtom({getAtom}) { const [param, setParam] = useState({num: 10}); setAtomParam = setParam; // flowlint-next-line unclear-type:off const myAtom: any = getAtom(); const [value, setValue] = useRecoilState(myAtom(param)); setAtomValue = setValue; return value; } const container = renderElements( <> theAtom} /> , ); act(() => { setUnvalidatedAtomValues( new Map().set( 'notDefinedYetAtomFamilyFallbackSel', 123, ), ); }); theAtom = atomFamily<_, $FlowFixMe>({ key: 'notDefinedYetAtomFamilyFallbackSel', default: selectorFamily({ key: 'notDefinedYetAtomFamilyFallbackSelFallback', get: ( // $FlowFixMe[missing-local-annot] {num}, ) => () => num === 1 ? 456 : 789, }), persistence_UNSTABLE: { type: 'url', validator: (_, notValid) => notValid, }, }); act(() => { setVisible(true); }); expect(container.textContent).toBe('789'); act(() => { setAtomValue(111); }); expect(container.textContent).toBe('111'); act(() => { setAtomParam({num: 1}); }); expect(container.textContent).toBe('456'); }, ); testRecoil('Independent atom subscriptions', ({gks}) => { const BASE_CALLS = reactMode().mode === 'LEGACY' && !gks.includes('recoil_suppress_rerender_in_callback') ? 1 : 0; const myAtom = atomFamily<_, string>({ key: 'atomFamily/independent subscriptions', default: 'DEFAULT', }); const TrackingComponent = ( param: $TEMPORARY$string<'A'> | $TEMPORARY$string<'B'>, ) => { let numUpdates = 0; let setValue; const Component = () => { setValue = useSetRecoilState(myAtom(param)); return ( { numUpdates++; }}> {stableStringify(useRecoilValue(myAtom(param)))} ); }; // $FlowFixMe[incompatible-call] return [Component, (value: number) => setValue(value), () => numUpdates]; }; const [ComponentA, setValueA, getNumUpdatesA] = TrackingComponent('A'); const [ComponentB, setValueB, getNumUpdatesB] = TrackingComponent('B'); const container = renderElements( <> , ); // Initial: expect(container.textContent).toBe('"DEFAULT""DEFAULT"'); expect(getNumUpdatesA()).toBe(BASE_CALLS + 1); expect(getNumUpdatesB()).toBe(BASE_CALLS + 1); // After setting at parameter A, component A should update: act(() => setValueA(1)); expect(container.textContent).toBe('1"DEFAULT"'); expect(getNumUpdatesA()).toBe(BASE_CALLS + 2); expect(getNumUpdatesB()).toBe(BASE_CALLS + 1); // After setting at parameter B, component B should update: act(() => setValueB(2)); expect(container.textContent).toBe('12'); expect(getNumUpdatesA()).toBe(BASE_CALLS + 2); expect(getNumUpdatesB()).toBe(BASE_CALLS + 2); }); describe('Effects', () => { testRecoil('Initialization', () => { let inited = 0; const myFamily = atomFamily({ key: 'atomFamily effect init', default: 'DEFAULT', effects: [ ({setSelf}) => { inited++; setSelf('INIT'); }, ], }); expect(inited).toEqual(0); expect(get(myFamily(1))).toEqual('INIT'); expect(inited).toEqual(1); set(myFamily(2)); expect(inited).toEqual(2); const [ReadsWritesAtom, _, reset] = componentThatReadsAndWritesAtom( myFamily(1), ); const c = renderElements(); expect(c.textContent).toEqual('"INIT"'); act(reset); expect(c.textContent).toEqual('"DEFAULT"'); }); testRecoil('Parameterized Initialization', () => { const myFamily = atomFamily({ key: 'atomFamily effect parameterized init', default: 'DEFAULT', // $FlowFixMe[missing-local-annot] effects: param => [({setSelf}) => setSelf(param)], }); expect(get(myFamily(1))).toEqual(1); expect(get(myFamily(2))).toEqual(2); }); testRecoil('Cleanup Handlers - when root unmounted', () => { const refCounts: {[string]: number} = {A: 0, B: 0}; const atoms = atomFamily({ key: 'atomFamily effect cleanup', // $FlowFixMe[missing-local-annot] default: p => p, // $FlowFixMe[missing-local-annot] effects: p => [ () => { refCounts[p]++; return () => { refCounts[p]--; }; }, ], }); let setNumRoots; function App() { const [numRoots, _setNumRoots] = useState(0); setNumRoots = _setNumRoots; return (
{Array(numRoots) .fill(null) .map((_, idx) => ( ))}
); } const c = renderElements(); expect(c.textContent).toBe(''); expect(refCounts).toEqual({A: 0, B: 0}); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCounts).toEqual({A: 1, B: 1}); act(() => setNumRoots(2)); expect(c.textContent).toBe('"A""B""A""B"'); expect(refCounts).toEqual({A: 2, B: 2}); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCounts).toEqual({A: 1, B: 1}); act(() => setNumRoots(0)); expect(c.textContent).toBe(''); expect(refCounts).toEqual({A: 0, B: 0}); }); testRecoil('storeID matches ', async () => { const atoms = atomFamily({ key: 'atomFamily effect - storeID', default: 'DEFAULT', // $FlowFixMe[missing-local-annot] effects: rootKey => [ ({storeID, setSelf}) => { expect(storeID).toEqual(storeIDs[rootKey]); setSelf(rootKey); }, ], }); const storeIDs: {[string]: StoreIDType} = {}; function StoreID({ rootKey, }: | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A1'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A2'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'B'>}>) { const storeID = useRecoilStoreID(); storeIDs[rootKey] = storeID; return null; } function MyApp() { return (
); } const c = renderElements(); expect(c.textContent).toEqual('"A""A1""A2""B"'); expect('A' in storeIDs).toEqual(true); expect('A1' in storeIDs).toEqual(true); expect('A2' in storeIDs).toEqual(true); expect('B' in storeIDs).toEqual(true); expect(storeIDs.A).not.toEqual(storeIDs.B); expect(storeIDs.A).not.toEqual(storeIDs.A1); expect(storeIDs.A).toEqual(storeIDs.A2); expect(storeIDs.B).not.toEqual(storeIDs.A1); expect(storeIDs.B).not.toEqual(storeIDs.A2); }); }); ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_atomWithFallback-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValue} from '../../core/Recoil_RecoilValue'; import type {Store} from '../../core/Recoil_State'; import type {NodeKey} from 'Recoil_Keys'; import type {RecoilState} from 'Recoil_RecoilValue'; import type {Node} from 'react'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useState, act, getRecoilValueAsLoadable, setRecoilValue, subscribeToRecoilValue, useRecoilState, useSetUnvalidatedAtomValues, componentThatReadsAndWritesAtom, renderElements, atom, constSelector, store: Store; let fallbackAtom: RecoilValue, hasFallbackAtom: RecoilValue; let id = 0; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); React = require('react'); ({useState} = require('react')); ({act} = require('ReactTestUtils')); ({ getRecoilValueAsLoadable, setRecoilValue, subscribeToRecoilValue, } = require('../../core/Recoil_RecoilValueInterface')); ({ useRecoilState, useSetUnvalidatedAtomValues, } = require('../../hooks/Recoil_Hooks')); ({ componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); atom = require('../Recoil_atom'); constSelector = require('../Recoil_constSelector'); store = makeStore(); fallbackAtom = atom({key: `fallback${id}`, default: 1}); hasFallbackAtom = atom({ key: `hasFallback${id++}`, default: fallbackAtom, }); subscribeToRecoilValue(store, hasFallbackAtom, () => undefined); }); function get( recoilValue: RecoilState | RecoilState | RecoilValue, ) { return getRecoilValueAsLoadable(store, recoilValue).contents; } function set( recoilValue: RecoilState | RecoilValue, value: ?(number | $TEMPORARY$string<'VALUE'>), ) { setRecoilValue(store, recoilValue, value); } testRecoil('atomWithFallback', () => { expect(get(hasFallbackAtom)).toBe(1); set(fallbackAtom, 2); expect(get(hasFallbackAtom)).toBe(2); set(hasFallbackAtom, 3); expect(get(hasFallbackAtom)).toBe(3); }); describe('ReturnDefaultOrFallback', () => { testRecoil('Returns the default', () => { let theAtom = null; let setUnvalidatedAtomValues; function SetsUnvalidatedAtomValues() { setUnvalidatedAtomValues = useSetUnvalidatedAtomValues(); return null; } let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(false); setVisible = mySetVisible; return visible ? children : null; } function MyReadsAtom({ getAtom, }: $TEMPORARY$object<{getAtom: () => null | RecoilState}>) { // flowlint-next-line unclear-type:off const [value] = useRecoilState((getAtom(): any)); return value; } const container = renderElements( <> theAtom} /> , ); act(() => { setUnvalidatedAtomValues( new Map().set('notDefinedYetAtomValidator', 123), ); }); theAtom = atom({ key: 'notDefinedYetAtomValidator', default: 456, persistence_UNSTABLE: { type: 'url', validator: (_, returnFallback) => returnFallback, }, }); act(() => { setVisible(true); }); expect(container.textContent).toBe('456'); }); testRecoil('Returns the fallback', () => { let theAtom = null; let setUnvalidatedAtomValues; function SetsUnvalidatedAtomValues() { setUnvalidatedAtomValues = useSetUnvalidatedAtomValues(); return null; } let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(false); setVisible = mySetVisible; return visible ? children : null; } function MyReadsAtom({ getAtom, }: $TEMPORARY$object<{getAtom: () => null | RecoilState}>) { // flowlint-next-line unclear-type:off const [value] = useRecoilState((getAtom(): any)); return value; } const container = renderElements( <> theAtom} /> , ); act(() => { setUnvalidatedAtomValues( new Map().set('notDefinedYetAtomWithFallback', 123), ); }); const fallback = atom({ key: 'notDefinedYetAtomFallback', default: 222, }); theAtom = atom({ key: 'notDefinedYetAtomWithFallback', default: fallback, persistence_UNSTABLE: { type: 'url', validator: (_, returnFallback) => returnFallback, }, }); act(() => { setVisible(true); }); expect(container.textContent).toBe('222'); }); }); testRecoil('Atom with atom fallback can store null and undefined', () => { const myFallbackAtom = atom({ key: 'fallback for null undefined', default: 'FALLBACK', }); const myAtom = atom({ key: 'fallback atom with undefined', default: myFallbackAtom, }); expect(get(myAtom)).toBe('FALLBACK'); act(() => set(myAtom, 'VALUE')); expect(get(myAtom)).toBe('VALUE'); act(() => set(myAtom, null)); expect(get(myAtom)).toBe(null); act(() => set(myAtom, undefined)); expect(get(myAtom)).toBe(undefined); act(() => set(myAtom, 'VALUE')); expect(get(myAtom)).toBe('VALUE'); }); testRecoil('Atom with selector fallback can store null and undefined', () => { const fallbackSelector = constSelector('FALLBACK'); const myAtom = atom({ key: 'fallback selector with undefined', default: fallbackSelector, }); expect(get(myAtom)).toBe('FALLBACK'); act(() => set(myAtom, 'VALUE')); expect(get(myAtom)).toBe('VALUE'); act(() => set(myAtom, null)); expect(get(myAtom)).toBe(null); act(() => set(myAtom, undefined)); expect(get(myAtom)).toBe(undefined); act(() => set(myAtom, 'VALUE')); expect(get(myAtom)).toBe('VALUE'); }); testRecoil('Effects', () => { let inited = false; const myFallbackAtom = atom({ key: 'atom with fallback effects init fallback', default: 'FALLBACK', }); const myAtom = atom({ key: 'atom with fallback effects init', default: myFallbackAtom, effects: [ ({setSelf}) => { inited = true; setSelf('INIT'); }, ], }); expect(get(myAtom)).toEqual('INIT'); expect(inited).toEqual(true); const [ReadsWritesAtom, _, reset] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); expect(c.textContent).toEqual('"INIT"'); act(reset); expect(c.textContent).toEqual('"FALLBACK"'); }); ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_constSelector-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValue} from '../../core/Recoil_RecoilValue'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let getRecoilValueAsLoadable, store, constSelector; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); ({ getRecoilValueAsLoadable, } = require('../../core/Recoil_RecoilValueInterface')); constSelector = require('../Recoil_constSelector'); store = makeStore(); }); function get(recoilValue: RecoilValue): T { return getRecoilValueAsLoadable(store, recoilValue).valueOrThrow(); } testRecoil('constSelector - string', () => { const mySelector = constSelector('HELLO'); expect(get(mySelector)).toEqual('HELLO'); expect(get(mySelector)).toBe('HELLO'); }); testRecoil('constSelector - number', () => { const mySelector = constSelector(42); expect(get(mySelector)).toEqual(42); expect(get(mySelector)).toBe(42); }); testRecoil('constSelector - null', () => { const mySelector = constSelector(null); expect(get(mySelector)).toEqual(null); expect(get(mySelector)).toBe(null); }); testRecoil('constSelector - boolean', () => { const mySelector = constSelector(true); expect(get(mySelector)).toEqual(true); expect(get(mySelector)).toBe(true); }); testRecoil('constSelector - array', () => { const emptyArraySelector = constSelector(([]: Array<$FlowFixMe>)); expect(get(emptyArraySelector)).toEqual([]); const numberArray = [1, 2, 3]; const numberArraySelector = constSelector(numberArray); expect(get(numberArraySelector)).toEqual([1, 2, 3]); expect(get(numberArraySelector)).toBe(numberArray); }); testRecoil('constSelector - object', () => { const emptyObjSelector = constSelector({}); expect(get(emptyObjSelector)).toEqual({}); const obj = {foo: 'bar'}; const objSelector = constSelector(obj); expect(get(objSelector)).toEqual({foo: 'bar'}); expect(get(objSelector)).toBe(obj); // Calling a second time with same object provides the same selector const objSelector2 = constSelector(obj); expect(objSelector2).toBe(objSelector); expect(get(objSelector2)).toEqual({foo: 'bar'}); expect(get(objSelector2)).toBe(obj); // Calling a third time with similar but different object provides // a new selector for the new reference. const newObj = {foo: 'bar'}; const objSelector3 = constSelector(newObj); expect(get(objSelector3)).toEqual({foo: 'bar'}); expect(get(objSelector3)).toBe(newObj); }); testRecoil('constSelector - function', () => { const foo = () => 'FOO'; const bar = () => 'BAR'; const fooSelector = constSelector(foo); const barSelector = constSelector(bar); expect(get(fooSelector)()).toEqual('FOO'); expect(get(barSelector)()).toEqual('BAR'); expect(constSelector(foo)).toEqual(fooSelector); }); ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_errorSelector-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValueReadOnly} from 'Recoil_RecoilValue'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let store, getRecoilValueAsLoadable, errorSelector; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); ({ getRecoilValueAsLoadable, } = require('../../core/Recoil_RecoilValueInterface')); errorSelector = require('../Recoil_errorSelector'); store = makeStore(); }); function getError(recoilValue: RecoilValueReadOnly): Error { const error = getRecoilValueAsLoadable(store, recoilValue).errorOrThrow(); if (!(error instanceof Error)) { throw new Error('Expected error to be an instance of Error'); } return error; } testRecoil('errorSelector - string', () => { const mySelector = errorSelector('My Error'); expect(getError(mySelector).message).toEqual( expect.stringContaining('My Error'), ); }); ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_selector-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Loadable} from '../../adt/Recoil_Loadable'; import type {RecoilValue} from '../../core/Recoil_RecoilValue'; import type {RecoilState} from 'Recoil'; import type {RecoilValueReadOnly} from 'Recoil_RecoilValue'; import type {WrappedValue} from 'Recoil_Wrapper'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let store, atom, noWait, act, isRecoilValue, constSelector, errorSelector, getRecoilValueAsLoadable, setRecoilValue, selector, asyncSelector, resolvingAsyncSelector, stringAtom, loadingAsyncSelector, flushPromisesAndTimers, DefaultValue, RecoilLoadable, isLoadable, freshSnapshot; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); ({act} = require('ReactTestUtils')); ({isRecoilValue} = require('../../core/Recoil_RecoilValue')); atom = require('../Recoil_atom'); constSelector = require('../Recoil_constSelector'); errorSelector = require('../Recoil_errorSelector'); ({ getRecoilValueAsLoadable, setRecoilValue, } = require('../../core/Recoil_RecoilValueInterface')); selector = require('../Recoil_selector'); ({freshSnapshot} = require('../../core/Recoil_Snapshot')); ({ asyncSelector, resolvingAsyncSelector, stringAtom, loadingAsyncSelector, flushPromisesAndTimers, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({noWait} = require('../Recoil_WaitFor')); ({RecoilLoadable, isLoadable} = require('../../adt/Recoil_Loadable')); ({DefaultValue} = require('../../core/Recoil_Node')); store = makeStore(); }); function getLoadable(recoilValue: RecoilValue): Loadable { return getRecoilValueAsLoadable(store, recoilValue); } function getValue(recoilValue: RecoilValue): T { return (getLoadable(recoilValue).contents: any); // flowlint-line unclear-type:off } function getPromise(recoilValue: RecoilValue): Promise { return getLoadable(recoilValue).promiseOrThrow(); } /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function getError(recoilValue): Error { const error = getLoadable(recoilValue).errorOrThrow(); if (!(error instanceof Error)) { throw new Error('Expected error to be instance of Error'); } return error; } function setValue(recoilState: RecoilState, value: T) { setRecoilValue(store, recoilState, value); // $FlowExpectedError[cannot-write] // $FlowFixMe[unsafe-arithmetic] store.getState().currentTree.version++; } function resetValue(recoilState: RecoilState) { setRecoilValue(store, recoilState, new DefaultValue()); // $FlowExpectedError[cannot-write] // $FlowFixMe[unsafe-arithmetic] store.getState().currentTree.version++; } testRecoil('Required options are provided when creating selectors', () => { const devStatus = window.__DEV__; window.__DEV__ = true; // $FlowExpectedError[incompatible-call] expect(() => selector({get: () => {}})).toThrow(); // $FlowExpectedError[incompatible-call] expect(() => selector({get: false})).toThrow(); // $FlowExpectedError[incompatible-call] expect(() => selector({key: 'MISSING GET'})).toThrow(); window.__DEV__ = devStatus; }); testRecoil('selector get', () => { const staticSel = constSelector('HELLO'); const selectorRO = selector({ key: 'selector/get', // $FlowFixMe[missing-local-annot] get: ({get}) => get(staticSel), }); expect(getValue(selectorRO)).toEqual('HELLO'); }); testRecoil('selector set', () => { const myAtom = atom({ key: 'selector/set/atom', default: 'DEFAULT', }); const selectorRW = selector({ key: 'selector/set', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom), // $FlowFixMe[missing-local-annot] set: ({set}, newValue) => set( myAtom, newValue instanceof DefaultValue ? newValue : 'SET: ' + newValue, ), }); expect(getValue(selectorRW)).toEqual('DEFAULT'); setValue(myAtom, 'SET ATOM'); expect(getValue(selectorRW)).toEqual('SET ATOM'); setValue(selectorRW, 'SET SELECTOR'); expect(getValue(selectorRW)).toEqual('SET: SET SELECTOR'); resetValue(selectorRW); expect(getValue(selectorRW)).toEqual('DEFAULT'); }); testRecoil('selector reset', () => { const myAtom = atom({ key: 'selector/reset/atom', default: 'DEFAULT', }); const selectorRW = selector({ key: 'selector/reset', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom), set: ({reset}) => reset(myAtom), }); expect(getValue(selectorRW)).toEqual('DEFAULT'); setValue(myAtom, 'SET ATOM'); expect(getValue(selectorRW)).toEqual('SET ATOM'); setValue(selectorRW, 'SET SELECTOR'); expect(getValue(selectorRW)).toEqual('DEFAULT'); }); describe('get return types', () => { testRecoil('evaluate to RecoilValue', () => { const atomA = atom({key: 'selector/const atom A', default: 'A'}); const atomB = atom({key: 'selector/const atom B', default: 'B'}); const inputAtom = atom({key: 'selector/input', default: 'a'}); const mySelector = selector({ key: 'selector/output recoil value', get: ({get}) => (get(inputAtom) === 'a' ? atomA : atomB), }); expect(getValue(mySelector)).toEqual('A'); setValue(inputAtom, 'b'); expect(getValue(mySelector)).toEqual('B'); }); testRecoil('evaluate to ValueLoadable', () => { const mySelector = selector({ key: 'selector/output loadable value', get: () => RecoilLoadable.of('VALUE'), }); expect(getValue(mySelector)).toEqual('VALUE'); }); testRecoil('evaluate to ErrorLoadable', () => { const mySelector = selector({ key: 'selector/output loadable error', get: () => RecoilLoadable.error(new Error('ERROR')), }); expect(getError(mySelector)).toBeInstanceOf(Error); expect(getError(mySelector).message).toBe('ERROR'); }); testRecoil('evaluate to LoadingLoadable', async () => { const mySelector = selector({ key: 'selector/output loadable loading', get: () => RecoilLoadable.of(Promise.resolve('ASYNC')), }); await expect(getPromise(mySelector)).resolves.toEqual('ASYNC'); }); testRecoil('evaluate to derived Loadable', async () => { const myAtom = stringAtom(); const mySelector = selector({ key: 'selector/output loadable derived', get: ({get}) => get(noWait(myAtom)).map(x => Promise.resolve(`DERIVE-${x}`)), }); await expect(getPromise(mySelector)).resolves.toEqual('DERIVE-DEFAULT'); }); testRecoil('evaluate to SelectorValue value', () => { const mySelector = selector({ key: 'selector/output SelectorValue value', get: () => selector.value('VALUE'), }); expect(getValue(mySelector)).toEqual('VALUE'); }); testRecoil('evaluate to SelectorValue Promise', async () => { const mySelector = selector>({ key: 'selector/output SelectorValue promise', get: () => selector.value(Promise.resolve('ASYNC')), }); expect(getValue(mySelector)).toBeInstanceOf(Promise); await expect(getValue(mySelector)).resolves.toBe('ASYNC'); }); testRecoil('evaluate to SelectorValue Loadable', () => { const mySelector = selector>({ key: 'selector/output SelectorValue loadable', get: () => selector.value(RecoilLoadable.of('VALUE')), }); expect(isLoadable(getValue(mySelector))).toBe(true); expect(getValue(mySelector).getValue()).toBe('VALUE'); }); testRecoil('evaluate to SelectorValue ErrorLoadable', () => { const mySelector = selector({ key: 'selector/output SelectorValue loadable error', // $FlowFixMe[underconstrained-implicit-instantiation] get: () => selector.value(RecoilLoadable.error('ERROR')), }); expect(isLoadable(getValue(mySelector))).toBe(true); expect(getValue(mySelector).errorOrThrow()).toBe('ERROR'); }); testRecoil('evaluate to SelectorValue Atom', () => { const myAtom = stringAtom(); const mySelector = selector({ key: 'selector/output SelectorValue loadable error', get: () => selector.value(myAtom), }); expect(isRecoilValue(getValue(mySelector))).toBe(true); }); }); describe('Catching Deps', () => { testRecoil('selector - catching exceptions', () => { const throwingSel = errorSelector<$FlowFixMe>('MY ERROR'); expect(getValue(throwingSel)).toBeInstanceOf(Error); const catchingSelector = selector({ key: 'selector/catching selector', // $FlowFixMe[missing-local-annot] get: ({get}) => { try { return get(throwingSel); } catch (e) { expect(e instanceof Error).toBe(true); expect(e.message).toContain('MY ERROR'); return 'CAUGHT'; } }, }); expect(getValue(catchingSelector)).toEqual('CAUGHT'); }); testRecoil('selector - catching exception (non Error)', () => { const throwingSel = selector< | RecoilValue | Promise | Loadable | WrappedValue | string, >({ key: '__error/non Error thrown', get: () => { // eslint-disable-next-line no-throw-literal throw 'MY ERROR'; }, }); const catchingSelector = selector({ key: 'selector/catching selector', // $FlowFixMe[missing-local-annot] get: ({get}) => { try { return get(throwingSel); } catch (e) { expect(e).toBe('MY ERROR'); return 'CAUGHT'; } }, }); expect(getValue(catchingSelector)).toEqual('CAUGHT'); }); testRecoil('selector - catching loads', () => { const loadingSel = resolvingAsyncSelector('READY'); expect(getValue(loadingSel) instanceof Promise).toBe(true); const blockedSelector = selector({ key: 'selector/blocked selector', // $FlowFixMe[missing-local-annot] get: ({get}) => get(loadingSel), }); expect(getValue(blockedSelector) instanceof Promise).toBe(true); const bypassSelector = selector({ key: 'selector/bypassing selector', // $FlowFixMe[missing-local-annot] get: ({get}) => { try { return get(loadingSel); } catch (promise) { expect(promise instanceof Promise).toBe(true); return 'BYPASS'; } }, }); expect(getValue(bypassSelector)).toBe('BYPASS'); act(() => jest.runAllTimers()); expect(getValue(bypassSelector)).toEqual('READY'); }); }); describe('Dependencies', () => { // Test that Recoil will throw an error with a useful debug message instead of // infinite recurssion when there is a circular dependency testRecoil('Detect circular dependencies', () => { const selectorA: RecoilValueReadOnly<$FlowFixMe> = selector({ key: 'circular/A', get: ({get}) => get(selectorC), }); const selectorB: RecoilValueReadOnly<$FlowFixMe> = selector({ key: 'circular/B', get: ({get}) => get(selectorA), }); const selectorC: RecoilValueReadOnly<$FlowFixMe> = selector({ key: 'circular/C', get: ({get}) => get(selectorB), }); const devStatus = window.__DEV__; window.__DEV__ = true; expect(getValue(selectorC)).toBeInstanceOf(Error); expect(getError(selectorC).message).toEqual( expect.stringContaining('circular/A'), ); window.__DEV__ = devStatus; }); testRecoil( 'distinct loading dependencies are treated as distinct', async () => { const upstreamAtom = atom({ key: 'distinct loading dependencies/upstreamAtom', default: 0, }); const upstreamAsyncSelector = selector({ key: 'distinct loading dependencies/upstreamAsyncSelector', // $FlowFixMe[missing-local-annot] get: ({get}) => Promise.resolve(get(upstreamAtom)), }); const directSelector = selector({ key: 'distinct loading dependencies/directSelector', // $FlowFixMe[missing-local-annot] get: ({get}) => get(upstreamAsyncSelector), }); expect(getValue(directSelector) instanceof Promise).toBe(true); act(() => jest.runAllTimers()); expect(getValue(directSelector)).toEqual(0); setValue(upstreamAtom, 1); expect(getValue(directSelector) instanceof Promise).toBe(true); act(() => jest.runAllTimers()); expect(getValue(directSelector)).toEqual(1); }, ); testRecoil( 'selector - kite pattern runs only necessary selectors', async () => { const aNode = atom({ key: 'aNode', default: true, }); let bNodeRunCount = 0; const bNode = selector({ key: 'bNode', // $FlowFixMe[missing-local-annot] get: ({get}) => { bNodeRunCount++; const a = get(aNode); return String(a); }, }); let cNodeRunCount = 0; const cNode = selector({ key: 'cNode', // $FlowFixMe[missing-local-annot] get: ({get}) => { cNodeRunCount++; const a = get(aNode); return String(Number(a)); }, }); let dNodeRunCount = 0; const dNode = selector({ key: 'dNode', // $FlowFixMe[missing-local-annot] get: ({get}) => { dNodeRunCount++; const value = get(aNode) ? get(bNode) : get(cNode); return value.toUpperCase(); }, }); let dNodeValue = getValue(dNode); expect(dNodeValue).toEqual('TRUE'); expect(bNodeRunCount).toEqual(1); expect(cNodeRunCount).toEqual(0); expect(dNodeRunCount).toEqual(1); setValue(aNode, false); dNodeValue = getValue(dNode); expect(dNodeValue).toEqual('0'); expect(bNodeRunCount).toEqual(1); expect(cNodeRunCount).toEqual(1); expect(dNodeRunCount).toEqual(2); }, ); testRecoil( 'selector does not re-run to completion when one of its async deps resolves to a previously cached value', async () => { const testSnapshot = freshSnapshot(); testSnapshot.retain(); const atomA = atom({ key: 'atomc-rerun-opt-test', default: -3, }); const selectorB = selector({ key: 'selb-rerun-opt-test', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await Promise.resolve(); return Math.abs(get(atomA)); }, }); let numTimesCStartedToRun = 0; let numTimesCRanToCompletion = 0; const selectorC = selector({ key: 'sela-rerun-opt-test', // $FlowFixMe[missing-local-annot] get: ({get}) => { numTimesCStartedToRun++; const ret = get(selectorB); /** * The placement of numTimesCRan is important as this optimization * prevents the execution of selectorC _after_ the point where the * selector execution hits a known path of dependencies. In other words, * the lines prior to the get(selectorB) will run twice, but the lines * following get(selectorB) should only run once given that we are * setting up this test so that selectorB resolves to a previously seen * value the second time that it runs. */ numTimesCRanToCompletion++; return ret; }, }); testSnapshot.getLoadable(selectorC); /** * Run selector chain so that selectorC is cached with a dep of selectorB * set to "3" */ await flushPromisesAndTimers(); const loadableA = testSnapshot.getLoadable(selectorC); expect(loadableA.contents).toBe(3); expect(numTimesCRanToCompletion).toBe(1); /** * It's expected that C started to run twice so far (the first is the first * time that the selector was called and suspended, the second was when B * resolved and C re-ran because of the async dep resolution) */ expect(numTimesCStartedToRun).toBe(2); const mappedSnapshot = testSnapshot.map(({set}) => { set(atomA, 3); }); mappedSnapshot.getLoadable(selectorC); /** * Run selector chain so that selectorB recalculates as a result of atomA * being changed to "3" */ mappedSnapshot.retain(); await flushPromisesAndTimers(); const loadableB = mappedSnapshot.getLoadable(selectorC); expect(loadableB.contents).toBe(3); /** * If selectors are correctly optimized, selectorC will not re-run because * selectorB resolved to "3", which is a value that selectorC has previously * cached for its selectorB dependency. */ expect(numTimesCRanToCompletion).toBe(1); /** * TODO: * in the ideal case this should be: * * expect(numTimesCStartedToRun).toBe(2); */ expect(numTimesCStartedToRun).toBe(3); }, ); testRecoil( 'async dep that changes from loading to value triggers re-execution', async () => { const baseAtom = atom({ key: 'baseAtom', default: 0, }); const asyncSel = selector({ key: 'asyncSel', // $FlowFixMe[missing-local-annot] get: ({get}) => { const atomVal = get(baseAtom); if (atomVal === 0) { return new Promise(() => {}); } return atomVal; }, }); const selWithAsyncDep = selector({ key: 'selWithAsyncDep', // $FlowFixMe[missing-local-annot] get: ({get}) => { return get(asyncSel); }, }); const snapshot = freshSnapshot(); snapshot.retain(); const loadingValLoadable = snapshot.getLoadable(selWithAsyncDep); expect(loadingValLoadable.state).toBe('loading'); const mappedSnapshot = snapshot.map(({set}) => { set(baseAtom, 10); }); const newAtomVal = mappedSnapshot.getLoadable(baseAtom); expect(newAtomVal.valueMaybe()).toBe(10); const valLoadable = mappedSnapshot.getLoadable(selWithAsyncDep); expect(valLoadable.valueMaybe()).toBe(10); }, ); testRecoil('Dynamic deps discovered after await', async () => { const testSnapshot = freshSnapshot(); testSnapshot.retain(); const myAtom = atom({ key: 'selector dynamic dep after await atom', default: 0, }); let selectorRunCount = 0; let selectorRunCompleteCount = 0; const mySelector = selector({ key: 'selector dynamic dep after await selector', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await Promise.resolve(); get(myAtom); selectorRunCount++; await new Promise(() => {}); selectorRunCompleteCount++; }, }); testSnapshot.getLoadable(mySelector); expect(testSnapshot.getLoadable(mySelector).state).toBe('loading'); await flushPromisesAndTimers(); expect(selectorRunCount).toBe(1); expect(selectorRunCompleteCount).toBe(0); const mappedSnapshot = testSnapshot.map(({set}) => set(myAtom, prev => prev + 1), ); expect(mappedSnapshot.getLoadable(mySelector).state).toBe('loading'); await flushPromisesAndTimers(); expect(selectorRunCount).toBe(2); expect(selectorRunCompleteCount).toBe(0); }); testRecoil('Dynamic deps discovered after pending', async () => { const pendingSelector = loadingAsyncSelector(); const testSnapshot = freshSnapshot(); testSnapshot.retain(); const myAtom = atom({ key: 'selector dynamic dep after pending atom', default: 0, }); let selectorRunCount = 0; let selectorRunCompleteCount = 0; const mySelector = selector({ key: 'selector dynamic dep after pending selector', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await Promise.resolve(); get(myAtom); selectorRunCount++; get(pendingSelector); selectorRunCompleteCount++; }, }); testSnapshot.getLoadable(mySelector); expect(testSnapshot.getLoadable(mySelector).state).toBe('loading'); await flushPromisesAndTimers(); expect(selectorRunCount).toBe(1); expect(selectorRunCompleteCount).toBe(0); const mappedSnapshot = testSnapshot.map(({set}) => set(myAtom, prev => prev + 1), ); expect(mappedSnapshot.getLoadable(mySelector).state).toBe('loading'); await flushPromisesAndTimers(); expect(selectorRunCount).toBe(2); expect(selectorRunCompleteCount).toBe(0); }); }); describe('Async Selector Set', () => { testRecoil('async set not supported', () => { const myAtom = stringAtom(); // $FlowFixMe[incompatible-call] const mySelector = selector({ key: 'selector async set', get: () => null, // $FlowFixMe[missing-local-annot] set: async ({set}, x) => set(myAtom, x), }); expect(() => setValue(mySelector, 'ERROR')).toThrow('Async'); }); testRecoil('async set call not supported', async () => { const myAtom = atom({ key: 'selector / async not supported / other atom', default: 'DEFAULT', }); // $FlowFixMe[incompatible-call] const mySelector = selector({ key: 'selector / async set not supported / async set method', get: () => myAtom, // Set should not be async, this test checks that it throws an error. // $FlowFixMe[missing-local-annot] set: async ({set, reset}, newVal) => { await Promise.resolve(); newVal instanceof DefaultValue ? reset(myAtom) : set(myAtom, 'SET'); }, }); let setAttempt, resetAttempt; const mySelector2 = selector({ key: 'selector / async set not supported / async upstream call', get: () => myAtom, // $FlowFixMe[missing-local-annot] set: ({set, reset}, newVal) => { if (newVal instanceof DefaultValue) { resetAttempt = Promise.resolve().then(() => { reset(myAtom); }); } else { setAttempt = Promise.resolve().then(() => { set(myAtom, 'SET'); }); } }, }); const testSnapshot = freshSnapshot(); testSnapshot.retain(); expect(() => testSnapshot.map(({set}) => { set(mySelector, 'SET'); }), ).toThrow(); expect(() => testSnapshot.map(({reset}) => { reset(mySelector); }), ).toThrow(); const setSnapshot = testSnapshot.map(({set, reset}) => { set(mySelector2, 'SET'); reset(mySelector2); }); setSnapshot.retain(); await flushPromisesAndTimers(); expect(setSnapshot.getLoadable(mySelector2).contents).toEqual('DEFAULT'); await expect(setAttempt).rejects.toThrowError(); await expect(resetAttempt).rejects.toThrowError(); }); testRecoil('set tries to get async value', () => { const myAtom = atom({key: 'selector set get async atom'}); const mySelector = selector({ key: 'selector set get async selector', get: () => myAtom, set: ({get}) => { get(myAtom); }, }); expect(() => setValue(mySelector, 'ERROR')).toThrow( 'selector set get async', ); }); }); describe('User-thrown promises', () => { testRecoil( 'selectors with user-thrown loadable promises execute to completion as expected', async () => { const [asyncDep, resolveAsyncDep] = asyncSelector(); const selWithUserThrownPromise = selector({ key: 'selWithUserThrownPromise', // $FlowFixMe[missing-local-annot] get: ({get}) => { const loadable = get(noWait(asyncDep)); if (loadable.state === 'loading') { throw loadable.toPromise(); } return loadable.valueOrThrow(); }, }); const loadable = getLoadable(selWithUserThrownPromise); const promise = loadable.toPromise(); expect(loadable.state).toBe('loading'); resolveAsyncDep('RESOLVED'); await flushPromisesAndTimers(); const val: mixed = await promise; expect(val).toBe('RESOLVED'); }, ); testRecoil( 'selectors with user-thrown loadable promises execute to completion as expected', async () => { const myAtomA = atom({ key: 'myatoma selectors user-thrown promise', default: 'A', }); const myAtomB = atom({ key: 'myatomb selectors user-thrown promise', default: 'B', }); let isResolved = false; let resolve = () => {}; const myPromise = new Promise(localResolve => { resolve = () => { isResolved = true; localResolve(); }; }); const selWithUserThrownPromise = selector({ key: 'selWithUserThrownPromise', // $FlowFixMe[missing-local-annot] get: ({get}) => { const a = get(myAtomA); if (!isResolved) { throw myPromise; } const b = get(myAtomB); return `${a}${b}`; }, }); const loadable = getLoadable(selWithUserThrownPromise); const promise = loadable.toPromise(); expect(loadable.state).toBe('loading'); resolve(); await flushPromisesAndTimers(); const val: mixed = await promise; expect(val).toBe('AB'); }, ); testRecoil( 'selectors with nested user-thrown loadable promises execute to completion as expected', async () => { const [asyncDep, resolveAsyncDep] = asyncSelector(); const selWithUserThrownPromise = selector({ key: 'selWithUserThrownPromise', // $FlowFixMe[missing-local-annot] get: ({get}) => { const loadable = get(noWait(asyncDep)); if (loadable.state === 'loading') { throw loadable.toPromise(); } return loadable.valueOrThrow(); }, }); const selThatDependsOnSelWithUserThrownPromise = selector({ key: 'selThatDependsOnSelWithUserThrownPromise', // $FlowFixMe[missing-local-annot] get: ({get}) => get(selWithUserThrownPromise), }); const loadable = getLoadable(selThatDependsOnSelWithUserThrownPromise); const promise = loadable.toPromise(); expect(loadable.state).toBe('loading'); resolveAsyncDep('RESOLVED'); await flushPromisesAndTimers(); const val: mixed = await promise; expect(val).toBe('RESOLVED'); }, ); }); testRecoil('selectors cannot mutate values in get() or set()', () => { const devStatus = window.__DEV__; window.__DEV__ = true; const userState = atom({ key: 'userState', default: { name: 'john', address: { road: '103 road', nested: { value: 'someNestedValue', }, }, }, }); const userSelector = selector({ key: 'userSelector', // $FlowFixMe[missing-local-annot] get: ({get}) => { const user = get(userState); user.address.road = '301 road'; return user; }, set: ({set, get}) => { const user = get(userState); user.address.road = 'narrow road'; return set(userState, user); }, }); const testSnapshot = freshSnapshot(); testSnapshot.retain(); expect(() => testSnapshot.map(({set}) => { set(userSelector, { name: 'matt', address: { road: '103 road', nested: { value: 'someNestedValue', }, }, }); }), ).toThrow(); expect(testSnapshot.getLoadable(userSelector).state).toBe('hasError'); window.__DEV__ = devStatus; }); describe('getCallback', () => { testRecoil('Selector getCallback', async () => { const myAtom = atom({ key: 'selector - getCallback atom', default: 'DEFAULT', }); const mySelector = selector({ key: 'selector - getCallback', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => { return { onClick: getCallback( ({snapshot}) => async () => await snapshot.getPromise(myAtom), ), }; }, }); const menuItem = getValue(mySelector); expect(getValue(myAtom)).toEqual('DEFAULT'); await expect(menuItem.onClick()).resolves.toEqual('DEFAULT'); act(() => setValue(myAtom, 'SET')); expect(getValue(myAtom)).toEqual('SET'); await expect(menuItem.onClick()).resolves.toEqual('SET'); act(() => setValue(myAtom, 'SET2')); expect(getValue(myAtom)).toEqual('SET2'); await expect(menuItem.onClick()).resolves.toEqual('SET2'); }); testRecoil('snapshot', async () => { const otherSelector = constSelector('VALUE'); const mySelector = selector({ key: 'selector getCallback snapshot', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => getCallback(({snapshot}) => param => ({ param, loadable: snapshot.getLoadable(otherSelector), promise: snapshot.getPromise(otherSelector), })), }); expect(getValue(mySelector)(123).param).toBe(123); expect(getValue(mySelector)(123).loadable.getValue()).toBe('VALUE'); await expect(getValue(mySelector)(123).promise).resolves.toBe('VALUE'); }); testRecoil('set', () => { const myAtom = atom({ key: 'selector getCallback set atom', default: 'DEFAULT', }); const setSelector = selector({ key: 'selector getCallback set', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => getCallback(({set}) => param => { set(myAtom, param); }), }); const resetSelector = selector({ key: 'selector getCallback reset', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => getCallback(({reset}) => () => { reset(myAtom); }), }); expect(getValue(myAtom)).toBe('DEFAULT'); getValue(setSelector)('SET'); expect(getValue(myAtom)).toBe('SET'); getValue(resetSelector)(); expect(getValue(myAtom)).toBe('DEFAULT'); }); testRecoil('transaction', () => { const myAtom = atom({ key: 'selector getCallback transact atom', default: 'DEFAULT', }); const setSelector = selector({ key: 'selector getCallback transact set', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => getCallback(({transact_UNSTABLE}) => param => { transact_UNSTABLE(({set, get}) => { expect(get(myAtom)).toBe('DEFAULT'); set(myAtom, 'TMP'); expect(get(myAtom)).toBe('TMP'); set(myAtom, param); }); }), }); const resetSelector = selector({ key: 'selector getCallback transact', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => getCallback(({transact_UNSTABLE}) => () => { transact_UNSTABLE(({reset}) => reset(myAtom)); }), }); expect(getValue(myAtom)).toBe('DEFAULT'); getValue(setSelector)('SET'); expect(getValue(myAtom)).toBe('SET'); getValue(resetSelector)(); expect(getValue(myAtom)).toBe('DEFAULT'); }); testRecoil('node', () => { const mySelector = selector({ key: 'selector getCallback node', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => getCallback(({node, snapshot}) => param => ({ param, loadable: snapshot.getLoadable(node), promise: snapshot.getPromise(node), })), }); expect(getValue(mySelector)(123).param).toBe(123); expect(getValue(mySelector)(123).loadable.getValue()(456).param).toBe(456); }); testRecoil('refresh', async () => { let externalValue = 0; const mySelector = selector({ key: 'selector getCallback node refresh', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => { const cachedExternalValue = externalValue; return getCallback(({node, refresh}) => () => ({ cached: cachedExternalValue, current: externalValue, refresh: () => refresh(node), })); }, }); expect(getValue(mySelector)().current).toBe(0); expect(getValue(mySelector)().cached).toBe(0); externalValue = 1; expect(getValue(mySelector)().current).toBe(1); expect(getValue(mySelector)().cached).toBe(0); getValue(mySelector)().refresh(); expect(getValue(mySelector)().current).toBe(1); expect(getValue(mySelector)().cached).toBe(1); }); testRecoil('Guard against calling during selector evaluation', async () => { const mySelector = selector({ key: 'selector getCallback guard', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => { const callback = getCallback(() => () => {}); expect(() => callback()).toThrow(); return 'THROW'; }, }); expect(getValue(mySelector)).toBe('THROW'); const myAsyncSelector = selector({ key: 'selector getCallback guard async', // $FlowFixMe[missing-local-annot] get: async ({getCallback}) => { const callback = getCallback(() => () => {}); await Promise.resolve(); expect(() => callback()).toThrow(); return 'THROW'; }, }); await expect(getPromise(myAsyncSelector)).resolves.toBe('THROW'); }); testRecoil('Callback can be used from thrown error', async () => { const mySelector = selector({ key: 'selector getCallback from error', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => { // eslint-disable-next-line no-throw-literal throw {callback: getCallback(() => x => x)}; }, }); // $FlowExpectedError[incompatible-use]] expect(getLoadable(mySelector).errorOrThrow().callback(123)).toEqual(123); const myAsyncSelector = selector({ key: 'selector getCallback from error async', // $FlowFixMe[missing-local-annot] get: ({getCallback}) => { return Promise.reject({callback: getCallback(() => x => x)}); }, }); await expect( getPromise(myAsyncSelector).catch(({callback}) => callback(123)), ).resolves.toEqual(123); }); }); testRecoil('Report error with inconsistent values', () => { const depA = stringAtom(); const depB = stringAtom(); // NOTE This is an illegal selector because it can provide different values // with the same input dependency values. let invalidInput = null; const mySelector = selector({ key: 'selector report invalid change', // $FlowFixMe[missing-local-annot] get: ({get}) => { get(depA); if (invalidInput) { return invalidInput; } return get(depB); }, }); expect(getValue(mySelector)).toBe('DEFAULT'); const DEV = window.__DEV__; let msg; const consoleError = console.error; // $FlowIssue[cannot-write] console.error = (...args) => { msg = args[0]; consoleError(...args); }; window.__DEV__ = true; invalidInput = 'INVALID'; setValue(depB, 'SET'); // Reset logic will still allow selector to work by resetting cache. expect(getValue(mySelector)).toBe('INVALID'); expect(msg).toEqual(expect.stringContaining('consistent values')); // $FlowIssue[cannot-write] console.error = consoleError; window.__DEV__ = DEV; }); testRecoil('Selector values are frozen', async () => { const devStatus = window.__DEV__; window.__DEV__ = true; const frozenSelector = selector({ key: 'selector frozen', get: () => ({state: 'frozen', nested: {state: 'frozen'}}), }); expect(Object.isFrozen(getValue(frozenSelector))).toBe(true); expect(Object.isFrozen(getValue(frozenSelector).nested)).toBe(true); const thawedSelector = selector({ key: 'selector frozen thawed', get: () => ({state: 'thawed', nested: {state: 'thawed'}}), dangerouslyAllowMutability: true, }); expect(Object.isFrozen(getValue(thawedSelector))).toBe(false); expect(Object.isFrozen(getValue(thawedSelector).nested)).toBe(false); const asyncFrozenSelector = selector({ key: 'selector frozen async', get: () => Promise.resolve({state: 'frozen', nested: {state: 'frozen'}}), }); await expect( getPromise(asyncFrozenSelector).then(x => Object.isFrozen(x)), ).resolves.toBe(true); expect(Object.isFrozen(getValue(asyncFrozenSelector).nested)).toBe(true); const asyncThawedSelector = selector({ key: 'selector frozen thawed async', get: () => Promise.resolve({state: 'thawed', nested: {state: 'thawed'}}), dangerouslyAllowMutability: true, }); await expect( getPromise(asyncThawedSelector).then(x => Object.isFrozen(x)), ).resolves.toBe(false); expect(Object.isFrozen(getValue(asyncThawedSelector).nested)).toBe(false); const upstreamFrozenSelector = selector({ key: 'selector frozen upstream', get: () => ({state: 'frozen', nested: {state: 'frozen'}}), }); const fwdFrozenSelector = selector({ key: 'selector frozen fwd', get: () => upstreamFrozenSelector, }); expect(Object.isFrozen(getValue(fwdFrozenSelector))).toBe(true); expect(Object.isFrozen(getValue(fwdFrozenSelector).nested)).toBe(true); const upstreamThawedSelector = selector({ key: 'selector frozen thawed upstream', get: () => ({state: 'thawed', nested: {state: 'thawed'}}), dangerouslyAllowMutability: true, }); const fwdThawedSelector = selector({ key: 'selector frozen thawed fwd', get: () => upstreamThawedSelector, dangerouslyAllowMutability: true, }); expect(Object.isFrozen(getValue(fwdThawedSelector))).toBe(false); expect(Object.isFrozen(getValue(fwdThawedSelector).nested)).toBe(false); // Selectors should not freeze their upstream dependencies const upstreamMixedSelector = selector({ key: 'selector frozen mixed upstream', get: () => ({state: 'thawed', nested: {state: 'thawed'}}), dangerouslyAllowMutability: true, }); const fwdMixedSelector = selector({ key: 'selector frozen mixed fwd', // $FlowFixMe[missing-local-annot] get: ({get}) => { get(upstreamMixedSelector); return {state: 'frozen'}; }, }); expect(Object.isFrozen(getValue(fwdMixedSelector))).toBe(true); expect(Object.isFrozen(getValue(upstreamMixedSelector))).toBe(false); expect(Object.isFrozen(getValue(upstreamMixedSelector).nested)).toBe(false); window.__DEV__ = devStatus; }); ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_selectorFamily-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilValue} from '../../core/Recoil_RecoilValue'; import type {RecoilState} from 'Recoil_RecoilValue'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let atom, DefaultValue, selectorFamily, getRecoilValueAsLoadable, setRecoilValue, store, myAtom; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); atom = require('../Recoil_atom'); ({DefaultValue} = require('../../core/Recoil_Node')); selectorFamily = require('../Recoil_selectorFamily'); ({ getRecoilValueAsLoadable, setRecoilValue, } = require('../../core/Recoil_RecoilValueInterface')); store = makeStore(); myAtom = atom({ key: 'atom', default: 0, }); }); function getValue(recoilValue: RecoilValue): T { return getRecoilValueAsLoadable(store, recoilValue).valueOrThrow(); } function set(recoilValue: RecoilState, value: number) { setRecoilValue(store, recoilValue, value); } testRecoil('selectorFamily - number parameter', () => { const mySelector = selectorFamily({ key: 'selectorFamily/number', get: ( // $FlowFixMe[missing-local-annot] multiplier, ) => // $FlowFixMe[missing-local-annot] ({get}) => get(myAtom) * multiplier, }); set(myAtom, 1); expect(getValue(mySelector(10))).toBe(10); expect(getValue(mySelector(100))).toBe(100); set(myAtom, 2); expect(getValue(mySelector(10))).toBe(20); expect(getValue(mySelector(100))).toBe(200); }); testRecoil('selectorFamily - array parameter', () => { const mySelector = selectorFamily({ key: 'selectorFamily/array', // $FlowFixMe[missing-local-annot] get: numbers => () => numbers.reduce((x, y) => x + y, 0), }); expect(getValue(mySelector([]))).toBe(0); expect(getValue(mySelector([1, 2, 3]))).toBe(6); expect(getValue(mySelector([0, 1, 1, 2, 3, 5]))).toBe(12); }); testRecoil('selectorFamily - object parameter', () => { const mySelector = selectorFamily({ key: 'selectorFamily/object', get: ( // $FlowFixMe[missing-local-annot] {multiplier}, ) => // $FlowFixMe[missing-local-annot] ({get}) => get(myAtom) * multiplier, }); set(myAtom, 1); expect(getValue(mySelector({multiplier: 10}))).toBe(10); expect(getValue(mySelector({multiplier: 100}))).toBe(100); set(myAtom, 2); expect(getValue(mySelector({multiplier: 10}))).toBe(20); expect(getValue(mySelector({multiplier: 100}))).toBe(200); }); testRecoil('selectorFamily - date parameter', () => { const mySelector = selectorFamily({ key: 'selectorFamily/date', get: ( // $FlowFixMe[missing-local-annot] date, ) => // $FlowFixMe[missing-local-annot] ({get}) => { const daysToAdd = get(myAtom); const returnDate = new Date(date); returnDate.setDate(returnDate.getDate() + daysToAdd); return returnDate; }, }); set(myAtom, 1); expect(getValue(mySelector(new Date(2021, 2, 25))).getDate()).toBe(26); set(myAtom, 2); expect(getValue(mySelector(new Date(2021, 2, 25))).getDate()).toBe(27); }); testRecoil('Works with supersets', () => { const mySelector = selectorFamily({ key: 'selectorFamily/supersets', get: ( // $FlowFixMe[missing-local-annot] {multiplier}, ) => // $FlowFixMe[missing-local-annot] ({get}) => get(myAtom) * multiplier, }); set(myAtom, 1); expect(getValue(mySelector({multiplier: 10}))).toBe(10); expect(getValue(mySelector({multiplier: 100}))).toBe(100); expect(getValue(mySelector({multiplier: 100, extra: 'foo'}))).toBe(100); }); testRecoil('selectorFamily - writable', () => { const mySelector = selectorFamily({ key: 'selectorFamily/writable', get: ( // $FlowFixMe[missing-local-annot] {multiplier}, ) => // $FlowFixMe[missing-local-annot] ({get}) => get(myAtom) * multiplier, set: ( // $FlowFixMe[missing-local-annot] {multiplier}, ) => // $FlowFixMe[missing-local-annot] ({set}, num) => set(myAtom, num instanceof DefaultValue ? num : num / multiplier), }); set(myAtom, 1); expect(getValue(mySelector({multiplier: 10}))).toBe(10); set(mySelector({multiplier: 10}), 20); expect(getValue(myAtom)).toBe(2); set(mySelector({multiplier: 10}), 30); expect(getValue(myAtom)).toBe(3); set(mySelector({multiplier: 100}), 400); expect(getValue(myAtom)).toBe(4); }); testRecoil('selectorFamily - value caching', () => { let evals = 0; const mySelector = selectorFamily({ key: 'selectorFamily/value caching', get: ( // $FlowFixMe[missing-local-annot] {multiplier}, ) => // $FlowFixMe[missing-local-annot] ({get}) => { evals++; return get(myAtom) * multiplier; }, }); expect(evals).toBe(0); set(myAtom, 1); expect(getValue(mySelector({multiplier: 10}))).toBe(10); expect(evals).toBe(1); expect(getValue(mySelector({multiplier: 10}))).toBe(10); expect(evals).toBe(1); expect(getValue(mySelector({multiplier: 100}))).toBe(100); expect(evals).toBe(2); expect(getValue(mySelector({multiplier: 100}))).toBe(100); expect(evals).toBe(2); expect(getValue(mySelector({multiplier: 10}))).toBe(10); expect(evals).toBe(2); set(myAtom, 2); expect(getValue(mySelector({multiplier: 10}))).toBe(20); expect(evals).toBe(3); expect(getValue(mySelector({multiplier: 10}))).toBe(20); expect(evals).toBe(3); expect(getValue(mySelector({multiplier: 100}))).toBe(200); expect(evals).toBe(4); expect(getValue(mySelector({multiplier: 100}))).toBe(200); expect(evals).toBe(4); }); testRecoil('selectorFamily - reference caching', () => { let evals = 0; const mySelector = selectorFamily({ key: 'selectorFamily/reference caching', get: ( // $FlowFixMe[missing-local-annot] {multiplier}, ) => // $FlowFixMe[missing-local-annot] ({get}) => { evals++; return get(myAtom) * multiplier; }, cachePolicyForParams_UNSTABLE: { equality: 'reference', }, }); expect(evals).toBe(0); set(myAtom, 1); expect(getValue(mySelector({multiplier: 10}))).toBe(10); expect(evals).toBe(1); expect(getValue(mySelector({multiplier: 10}))).toBe(10); expect(evals).toBe(2); expect(getValue(mySelector({multiplier: 100}))).toBe(100); expect(evals).toBe(3); expect(getValue(mySelector({multiplier: 100}))).toBe(100); expect(evals).toBe(4); expect(getValue(mySelector({multiplier: 10}))).toBe(10); expect(evals).toBe(5); set(myAtom, 2); expect(getValue(mySelector({multiplier: 10}))).toBe(20); expect(evals).toBe(6); expect(getValue(mySelector({multiplier: 10}))).toBe(20); expect(evals).toBe(7); expect(getValue(mySelector({multiplier: 100}))).toBe(200); expect(evals).toBe(8); expect(getValue(mySelector({multiplier: 100}))).toBe(200); expect(evals).toBe(9); const multiply10 = {multiplier: 10}; const multiply100 = {multiplier: 100}; set(myAtom, 1); expect(getValue(mySelector(multiply10))).toBe(10); expect(evals).toBe(10); expect(getValue(mySelector(multiply10))).toBe(10); expect(evals).toBe(10); expect(getValue(mySelector(multiply100))).toBe(100); expect(evals).toBe(11); expect(getValue(mySelector(multiply100))).toBe(100); expect(evals).toBe(11); expect(getValue(mySelector(multiply10))).toBe(10); expect(evals).toBe(11); set(myAtom, 2); expect(getValue(mySelector(multiply10))).toBe(20); expect(evals).toBe(12); expect(getValue(mySelector(multiply10))).toBe(20); expect(evals).toBe(12); expect(getValue(mySelector(multiply100))).toBe(200); expect(evals).toBe(13); expect(getValue(mySelector(multiply100))).toBe(200); expect(evals).toBe(13); }); // Parameterized selector results should be frozen unless // dangerouslyAllowMutability is set testRecoil('selectorFamily - mutability', () => { const myImmutableSelector = selectorFamily({ key: 'selectorFamily/immutable', get: ( // $FlowFixMe[missing-local-annot] {key}, ) => // $FlowFixMe[missing-local-annot] ({get}) => ({[key]: get(myAtom)}), }); set(myAtom, 42); const immutableResult: {[string]: number, ...} = getValue( myImmutableSelector({key: 'foo'}), ); expect(immutableResult).toEqual({foo: 42}); expect(() => { immutableResult.foo = 2600; }).toThrow(); const myMutableSelector = selectorFamily({ key: 'selectorFamily/mutable', get: ( // $FlowFixMe[missing-local-annot] {key}, ) => // $FlowFixMe[missing-local-annot] ({get}) => ({[key]: get(myAtom)}), dangerouslyAllowMutability: true, }); set(myAtom, 42); const mutableResult: {[string]: number, ...} = getValue( myMutableSelector({key: 'foo'}), ); expect(mutableResult).toEqual({foo: 42}); mutableResult.foo = 2600; expect(mutableResult).toEqual({foo: 2600}); }); testRecoil('selectorFamily - evaluate to RecoilValue', () => { const atomA = atom({key: 'selectorFamily/const atom A', default: 'A'}); const atomB = atom({key: 'selectorFamily/const atom B', default: 'B'}); const mySelector = selectorFamily({ key: 'selectorFamily/', get: param => () => (param === 'a' ? atomA : atomB), }); expect(getValue(mySelector('a'))).toEqual('A'); expect(getValue(mySelector('b'))).toEqual('B'); }); testRecoil('selectorFamily - invalid parameter error message', () => { const mySelector = selectorFamily<_, {foo: () => void}>({ key: 'function in parameter', get: () => () => {}, }); expect(() => getValue(mySelector({foo: () => {}}))).toThrow( 'function in parameter', ); }); ================================================ FILE: packages/recoil/recoil_values/__tests__/Recoil_selectorHooks-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ /* eslint-disable fb-www/react-no-useless-fragment */ 'use strict'; import type { RecoilState, RecoilValue, RecoilValueReadOnly, } from '../../core/Recoil_RecoilValue'; import type {PersistenceSettings} from '../../recoil_values/Recoil_atom'; import type {Loadable} from 'Recoil_Loadable'; import type {WrappedValue} from 'Recoil_Wrapper'; import type {Node} from 'react'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useEffect, useState, Profiler, act, batchUpdates, RecoilRoot, atom, constSelector, errorSelector, selector, noWait, ReadsAtom, asyncSelector, loadingAsyncSelector, resolvingAsyncSelector, errorThrowingAsyncSelector, stringAtom, flushPromisesAndTimers, renderElements, renderUnwrappedElements, renderElementsWithSuspenseCount, componentThatReadsAndWritesAtom, useRecoilState, useRecoilValue, useRecoilValueLoadable, useSetRecoilState, useResetRecoilState, useRecoilCallback, reactMode, invariant, nullthrows; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useEffect, useState, Profiler} = require('react')); ({act} = require('ReactTestUtils')); ({batchUpdates} = require('../../core/Recoil_Batching')); ({RecoilRoot} = require('../../core/Recoil_RecoilRoot')); atom = require('../../recoil_values/Recoil_atom'); constSelector = require('../../recoil_values/Recoil_constSelector'); errorSelector = require('../../recoil_values/Recoil_errorSelector'); ({noWait} = require('../../recoil_values/Recoil_WaitFor')); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, asyncSelector, loadingAsyncSelector, resolvingAsyncSelector, errorThrowingAsyncSelector, stringAtom, flushPromisesAndTimers, renderElements, renderUnwrappedElements, renderElementsWithSuspenseCount, componentThatReadsAndWritesAtom, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({reactMode} = require('../../core/Recoil_ReactMode')); ({ useRecoilState, useRecoilValue, useRecoilValueLoadable, useSetRecoilState, useResetRecoilState, } = require('../../hooks/Recoil_Hooks')); ({useRecoilCallback} = require('../../hooks/Recoil_useRecoilCallback')); invariant = require('recoil-shared/util/Recoil_invariant'); nullthrows = require('recoil-shared/util/Recoil_nullthrows'); }); let nextID = 0; function counterAtom(persistence?: PersistenceSettings) { return atom({ key: `atom${nextID++}`, default: 0, persistence_UNSTABLE: persistence, }); } function booleanAtom(persistence?: PersistenceSettings) { return atom({ key: `atom${nextID++}`, default: false, persistence_UNSTABLE: persistence, }); } function plusOneSelector(dep: RecoilValue) { const fn = jest.fn(x => x + 1); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => fn(get(dep)), }); return [sel, fn]; } function plusOneAsyncSelector( dep: RecoilValue, ): [RecoilValueReadOnly, (number) => void] { let nextTimeoutAmount = 100; const fn = jest.fn(x => { return new Promise(resolve => { setTimeout(() => { resolve(x + 1); }, nextTimeoutAmount); }); }); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => fn(get(dep)), }); return [ sel, x => { nextTimeoutAmount = x; }, ]; } function additionSelector( depA: RecoilValue, depB: RecoilValue, ) { const fn = jest.fn((a, b) => a + b); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => fn(get(depA), get(depB)), }); return [sel, fn]; } function asyncSelectorThatPushesPromisesOntoArray( dep: RecoilValue, ): [RecoilValue, $ReadOnlyArray<[(T) => void, (mixed) => void]>] { const promises: Array<[(T) => void, (mixed) => void]> = []; const sel = selector({ key: `selector${nextID++}`, get: ({get}) => { get(dep); let resolve: T => void = () => invariant(false, 'bug in test code'); // make flow happy with initialization let reject: mixed => void = () => invariant(false, 'bug in test code'); const p = new Promise((res, rej) => { resolve = res; reject = rej; }); promises.push([resolve, reject]); return p; }, }); return [sel, promises]; } function componentThatWritesAtom( recoilState: RecoilState, // flowlint-next-line unclear-type:off ): [any, ((T => T) | T) => void] { let updateValue; const Component = jest.fn(() => { updateValue = useSetRecoilState(recoilState); return null; }); // flowlint-next-line unclear-type:off return [(Component: any), x => updateValue(x)]; } function componentThatReadsAtomWithCommitCount( recoilState: RecoilValueReadOnly, ) { const commit = jest.fn(() => {}); function ReadAtom() { return ( // $FlowFixMe[invalid-tuple-arity] {useRecoilValue(recoilState)} ); } return [ReadAtom, commit]; } function componentThatToggles(a: Node, b: null) { const toggle = {current: () => invariant(false, 'bug in test code')}; const Toggle = () => { const [value, setValue] = useState(false); // $FlowFixMe[incompatible-type] toggle.current = () => setValue(v => !v); return value ? b : a; }; return [Toggle, toggle]; } function advanceTimersBy(ms: number) { // Jest does the right thing for runAllTimers but not advanceTimersByTime: act(() => { jest.runAllTicks(); jest.runAllImmediates(); jest.advanceTimersByTime(ms); jest.runAllImmediates(); // order seems backwards but matches jest.runAllTimers(). jest.runAllTicks(); }); } function baseRenderCount(gks: Array): number { return reactMode().mode === 'LEGACY' && !gks.includes('recoil_suppress_rerender_in_callback') ? 1 : 0; } testRecoil('static selector', () => { const staticSel = constSelector('HELLO'); const c = renderElements(); expect(c.textContent).toEqual('"HELLO"'); }); describe('Updates', () => { testRecoil('Selectors are updated when upstream atoms change', () => { const anAtom = counterAtom(); const [aSelector, _] = plusOneSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); expect(container.textContent).toEqual('1'); act(() => updateValue(1)); expect(container.textContent).toEqual('2'); }); testRecoil('Selectors can depend on other selectors', () => { const anAtom = counterAtom(); const [selectorA, _] = plusOneSelector(anAtom); const [selectorB, __] = plusOneSelector(selectorA); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); expect(container.textContent).toEqual('2'); act(() => updateValue(1)); expect(container.textContent).toEqual('3'); }); testRecoil('Selectors can depend on async selectors', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [selectorA, _] = plusOneAsyncSelector(anAtom); const [selectorB, __] = plusOneSelector(selectorA); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('2'); act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('3'); }); testRecoil('Async selectors can depend on async selectors', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [selectorA, _] = plusOneAsyncSelector(anAtom); const [selectorB, __] = plusOneAsyncSelector(selectorA); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); if (reactMode().mode !== 'LEGACY') { await flushPromisesAndTimers(); expect(container.textContent).toEqual('2'); act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); await flushPromisesAndTimers(); expect(container.textContent).toEqual('3'); } else { // we need to test the useRecoilValueLoadable_LEGACY method expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('2'); act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('3'); } }); testRecoil('Dep of upstream selector can change while pending', async () => { const anAtom = counterAtom(); const [upstreamSel, upstreamResolvers] = asyncSelectorThatPushesPromisesOntoArray<$FlowFixMe | number, _>(anAtom); const [downstreamSel, downstreamResolvers] = asyncSelectorThatPushesPromisesOntoArray<$FlowFixMe | number, _>( upstreamSel, ); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); // Initially, upstream has returned a promise so there is one upstream resolver. // Downstream is waiting on upstream so it hasn't returned anything yet. expect(container.textContent).toEqual('loading'); expect(upstreamResolvers.length).toEqual(1); expect(downstreamResolvers.length).toEqual(0); // Resolve upstream; downstream should now have returned a new promise: upstreamResolvers[0][0](123); await flushPromisesAndTimers(); expect(downstreamResolvers.length).toEqual(1); // Update atom to a new value while downstream is pending: act(() => updateValue(1)); await flushPromisesAndTimers(); // Upstream returns a new promise for the new atom value. // Downstream is once again waiting on upstream so it hasn't returned a new // promise for the new value. expect(upstreamResolvers.length).toEqual(2); expect(downstreamResolvers.length).toEqual(1); // Resolve the new upstream promise: upstreamResolvers[1][0](123); await flushPromisesAndTimers(); // Downstream can now return its new promise: expect(downstreamResolvers.length).toEqual(2); // If we resolve downstream's new promise we should see the result: downstreamResolvers[1][0](123); await flushPromisesAndTimers(); expect(container.textContent).toEqual('123'); }); testRecoil('Errors are propogated through selectors', () => { const errorThrower = errorSelector('ERROR'); const [downstreamSelector] = plusOneSelector(errorThrower); const container = renderElements( <> , ); expect(container.textContent).toEqual('error'); }); testRecoil( 'Rejected promises are propogated through selectors (immediate rejection)', async () => { const anAtom = counterAtom(); const errorThrower = errorThrowingAsyncSelector( 'ERROR', anAtom, ); const [downstreamSelector] = plusOneAsyncSelector(errorThrower); const container = renderElements( <> , ); expect(container.textContent).toEqual('loading'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(container.textContent).toEqual('error'); }, ); testRecoil( 'Rejected promises are propogated through selectors (later rejection)', async () => { const anAtom = counterAtom(); const [errorThrower, _resolve, reject] = asyncSelector(anAtom); const [downstreamSelector] = plusOneAsyncSelector(errorThrower); const container = renderElements( <> , ); expect(container.textContent).toEqual('loading'); act(() => reject(new Error())); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(container.textContent).toEqual('error'); }, ); }); testRecoil('Selectors can be invertible', () => { const anAtom = counterAtom(); const aSelector = selector({ key: 'invertible1', // $FlowFixMe[missing-local-annot] get: ({get}) => get(anAtom), // $FlowFixMe[missing-local-annot] set: ({set}, newValue) => set(anAtom, newValue), }); const [Component, updateValue] = componentThatWritesAtom(aSelector); const container = renderElements( <> , ); expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('1'); }); describe('Dynamic Dependencies', () => { testRecoil('Selector dependencies can change over time', () => { const atomA = counterAtom(); const atomB = counterAtom(); const aSelector = selector({ key: 'depsChange', // $FlowFixMe[missing-local-annot] get: ({get}) => { const a = get(atomA); if (a === 1337) { const b = get(atomB); return b; } else { return a; } }, }); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); const container = renderElements( <> , ); expect(container.textContent).toEqual('0'); act(() => updateValueA(1337)); expect(container.textContent).toEqual('0'); act(() => updateValueB(1)); expect(container.textContent).toEqual('1'); act(() => updateValueA(2)); expect(container.textContent).toEqual('2'); }); testRecoil('Selectors can gain and lose depnedencies', ({gks}) => { const BASE_CALLS = baseRenderCount(gks); const switchAtom = booleanAtom(); const inputAtom = counterAtom(); // Depends on inputAtom only when switchAtom is true: const aSelector = selector({ key: 'gainsDeps', get: ({get}) => { if (get(switchAtom)) { return get(inputAtom); } else { return Infinity; } }, }); const [ComponentA, setSwitch] = componentThatWritesAtom(switchAtom); const [ComponentB, setInput] = componentThatWritesAtom(inputAtom); const [ComponentC, commit] = componentThatReadsAtomWithCommitCount(aSelector); const container = renderElements( <> , ); expect(container.textContent).toEqual('Infinity'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); // Input is not a dep yet, so this has no effect: act(() => setInput(1)); expect(container.textContent).toEqual('Infinity'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); // Flip switch: act(() => setSwitch(true)); expect(container.textContent).toEqual('1'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); // Now changing input causes a re-render: act(() => setInput(2)); expect(container.textContent).toEqual('2'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 3); // Now that we've added the dep, we can remove it... act(() => setSwitch(false)); expect(container.textContent).toEqual('Infinity'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 4); // ... and again changing input will not cause a re-render: act(() => setInput(3)); expect(container.textContent).toEqual('Infinity'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 4); }); testRecoil('Selector depedencies are updated transactionally', () => { const atomA = counterAtom(); const atomB = counterAtom(); const atomC = counterAtom(); const [observedSelector, selectorFn] = plusOneSelector(atomC); const aSelector = selector({ key: 'transactionally', // $FlowFixMe[missing-local-annot] get: ({get}) => { const a = get(atomA); const b = get(atomB); return a !== 0 && b === 0 ? get(observedSelector) // We want to test this never happens : null; }, }); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); const [ComponentC, updateValueC] = componentThatWritesAtom(atomC); renderElements( <> , ); act(() => { batchUpdates(() => { updateValueA(1); updateValueB(1); }); }); // observedSelector wasn't evaluated: expect(selectorFn).toHaveBeenCalledTimes(0); // nor were any subscriptions created for it: act(() => { updateValueC(1); }); expect(selectorFn).toHaveBeenCalledTimes(0); }); testRecoil( 'selector is able to track dependencies discovered asynchronously', async () => { const anAtom = atom({ key: 'atomTrackedAsync', default: 'Async Dep Value', }); const selectorWithAsyncDeps = selector({ key: 'selectorTrackDepsIncrementally', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await Promise.resolve(); // needed to simulate discovering a dependency asynchronously return get(anAtom); }, }); let setAtom; function Component() { [, setAtom] = useRecoilState(anAtom); const selVal = useRecoilValue(selectorWithAsyncDeps); return selVal; } const container = renderElements(); expect(container.textContent).toEqual('loading'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(container.textContent).toEqual('Async Dep Value'); act(() => setAtom('CHANGED Async Dep')); expect(container.textContent).toEqual('loading'); await flushPromisesAndTimers(); expect(container.textContent).toEqual('CHANGED Async Dep'); }, ); /** * This test is an extension of the 'selector is able to track dependencies * discovered asynchronously' test: in addition to testing that a selector * responds to changes in dependencies that were discovered asynchronously, the * selector should run through the entire selector in response to those changes. */ testRecoil( 'selector should rerun entire selector when a dep changes', async () => { const resolvingSel1 = resolvingAsyncSelector(1); const resolvingSel2 = resolvingAsyncSelector(2); const anAtom3 = atom({key: 'atomTrackedAsync3', default: 3}); const selectorWithAsyncDeps = selector({ key: 'selectorNotCacheIncDeps', // $FlowFixMe[missing-local-annot] get: async ({get}) => { const val1 = get(resolvingSel1); await Promise.resolve(); const val2 = get(resolvingSel2); await Promise.resolve(); const val3 = get(anAtom3); return val1 + val2 + val3; }, }); let setAtom; function Component() { [, setAtom] = useRecoilState(anAtom3); const selVal = useRecoilValue(selectorWithAsyncDeps); return selVal; } const container = renderElements(); expect(container.textContent).toEqual('loading'); await flushPromisesAndTimers(); // HACK: not sure why but these are needed in OSS await flushPromisesAndTimers(); await flushPromisesAndTimers(); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('6'); act(() => setAtom(4)); expect(container.textContent).toEqual('loading'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('7'); }, ); testRecoil( 'async selector with changing dependencies finishes execution using original state', async () => { const [asyncDep, resolveAsyncDep] = asyncSelector(); const anAtom = atom({key: 'atomChangingDeps', default: 3}); const anAsyncSelector = selector({ key: 'selectorWithChangingDeps', // $FlowFixMe[missing-local-annot] get: ({get}) => { const atomValueBefore = get(anAtom); get(asyncDep); const atomValueAfter = get(anAtom); expect(atomValueBefore).toBe(atomValueAfter); return atomValueBefore + atomValueAfter; }, }); let loadableSoFar; let setAtom; const MyComponent = () => { const setAtomLocal = useSetRecoilState(anAtom); const asyncSelLoadable = useRecoilValueLoadable(anAsyncSelector); setAtom = setAtomLocal; loadableSoFar = asyncSelLoadable; return asyncSelLoadable.state; }; renderElements(); const loadableBeforeChangingAnything = nullthrows(loadableSoFar); expect(loadableBeforeChangingAnything.contents).toBeInstanceOf(Promise); act(() => setAtom(10)); const loadableAfterChangingAtom = nullthrows(loadableSoFar); expect(loadableAfterChangingAtom.contents).toBeInstanceOf(Promise); expect(loadableBeforeChangingAnything.contents).not.toBe( loadableAfterChangingAtom.contents, ); act(() => resolveAsyncDep('')); await flushPromisesAndTimers(); await Promise.all([ expect(loadableBeforeChangingAnything.toPromise()).resolves.toBe(3 + 3), expect(loadableAfterChangingAtom.toPromise()).resolves.toBe(10 + 10), ]); }, ); testRecoil( 'Selector deps are saved when a component mounts due to a non-recoil change at the same time that a selector is first read', () => { // Regression test for an issue where selector dependencies were not saved // in this circumstance. In this situation dependencies are discovered for // a selector when reading from a non-latest graph. This tests that these deps // are carried forward instead of being forgotten. let show, setShow, setAnotherAtom; function Parent() { [show, setShow] = useState(false); setAnotherAtom = useSetRecoilState(anotherAtom); if (show) { return ; } else { return null; } } const anAtom = atom({key: 'anAtom', default: 0}); const anotherAtom = atom({key: 'anotherAtom', default: 0}); const aSelector = selector({ key: 'aSelector', // $FlowFixMe[missing-local-annot] get: ({get}) => { return get(anAtom); }, }); function SelectorUser() { const setter = useSetRecoilState(anAtom); useEffect(() => { setter(1); }); return useRecoilValue(aSelector); } const c = renderElements(); expect(c.textContent).toEqual(''); act(() => { setShow(true); setAnotherAtom(1); }); expect(c.textContent).toEqual('1'); }, ); testRecoil('Dynamic deps will refresh', async () => { const myAtom = atom({ key: 'selector dynamic deps atom', default: 'DEFAULT', }); const myAtomA = atom({ key: 'selector dynamic deps atom A', default: new Promise(() => {}), }); const myAtomB = atom({ key: 'selector dynamic deps atom B', default: 'B', }); const myAtomC = atom({ key: 'selector dynamic deps atom C', default: new Promise(() => {}), }); let selectorEvaluations = 0; const mySelector = selector({ key: 'selector dynamic deps selector', // $FlowFixMe[missing-local-annot] get: async ({get}) => { selectorEvaluations++; await Promise.resolve(); const sw = get(myAtom); if (sw === 'A') { return 'RESOLVED_' + get(myAtomA); } if (sw === 'B') { return 'RESOLVED_' + get(myAtomB); } if (sw === 'C') { return 'RESOLVED_' + get(myAtomC); } await new Promise(() => {}); }, }); // This wrapper selector is important so that the subscribing component // doesn't suspend while the selector is pending async results. // Otherwise the component may trigger re-evaluations when it wakes up // and provide a false-positive. const wrapperSelector = selector({ key: 'selector dynamic deps wrapper', // $FlowFixMe[missing-local-annot] get: ({get}) => { const loadable = get(noWait(mySelector)); return loadable.state === 'loading' ? 'loading' : loadable.contents; }, }); let setAtom, setAtomA, setAtomB; function ComponentSetter() { setAtom = useSetRecoilState(myAtom); setAtomA = useSetRecoilState(myAtomA); setAtomB = useSetRecoilState(myAtomB); return null; } const c = renderElements( <> , ); await flushPromisesAndTimers(); expect(c.textContent).toBe('"loading"'); expect(selectorEvaluations).toBe(1); // Cause re-evaluation to pending state act(() => setAtom('TMP')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"loading"'); expect(selectorEvaluations).toBe(2); // Add atomA dependency, which is pending act(() => setAtom('A')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"loading"'); expect(selectorEvaluations).toBe(3); // change to atomB dependency act(() => setAtom('B')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RESOLVED_B"'); expect(selectorEvaluations).toBe(4); // Set atomB act(() => setAtomB('SETB-0')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RESOLVED_SETB-0"'); expect(selectorEvaluations).toBe(5); // Change back to atomA dependency act(() => setAtom('A')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"loading"'); expect(selectorEvaluations).toBe(6); // Setting B is currently ignored act(() => setAtomB('SETB-IGNORE')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"loading"'); expect(selectorEvaluations).toBe(6); // Set atomA act(() => setAtomA('SETA')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RESOLVED_SETA"'); expect(selectorEvaluations).toBe(7); // Setting atomB is ignored act(() => setAtomB('SETB-LATER')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RESOLVED_SETA"'); expect(selectorEvaluations).toBe(7); // Change to atomC, which is pending act(() => setAtom('C')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"loading"'); expect(selectorEvaluations).toBe(8); // Setting atomA is ignored act(() => setAtomA('SETA-LATER')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"loading"'); expect(selectorEvaluations).toBe(8); // change back to atomA for new value act(() => setAtom('A')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RESOLVED_SETA-LATER"'); expect(selectorEvaluations).toBe(9); // Change back to atomB act(() => setAtom('B')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RESOLVED_SETB-LATER"'); expect(selectorEvaluations).toBe(10); // Set atomB act(() => setAtomB('SETB-1')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RESOLVED_SETB-1"'); expect(selectorEvaluations).toBe(11); }); }); describe('Catching Deps', () => { testRecoil('selector catching exceptions', () => { const throwingSel = errorSelector< | RecoilValue | Promise | Loadable | WrappedValue | string, >('MY ERROR'); const c1 = renderElements(); expect(c1.textContent).toEqual('error'); const catchingSelector = selector({ key: 'useRecoilState/catching selector', // $FlowFixMe[missing-local-annot] get: ({get}) => { try { return get(throwingSel); } catch (e) { expect(e instanceof Error).toBe(true); expect(e.message).toContain('MY ERROR'); return 'CAUGHT'; } }, }); const c2 = renderElements(); expect(c2.textContent).toEqual('"CAUGHT"'); }); testRecoil('selector catching exceptions (non Errors)', () => { const throwingSel = selector< | RecoilValue | Promise | Loadable | WrappedValue | string, >({ key: '__error/non Error thrown', get: () => { // eslint-disable-next-line no-throw-literal throw 'MY ERROR'; }, }); const c1 = renderElements(); expect(c1.textContent).toEqual('error'); const catchingSelector = selector({ key: 'useRecoilState/catching selector', // $FlowFixMe[missing-local-annot] get: ({get}) => { try { return get(throwingSel); } catch (e) { expect(e).toBe('MY ERROR'); return 'CAUGHT'; } }, }); const c2 = renderElements(); expect(c2.textContent).toEqual('"CAUGHT"'); }); testRecoil('selector catching loads', async () => { const resolvingSel = resolvingAsyncSelector('READY'); const bypassSelector = selector({ key: 'useRecoilState/bypassing selector', // $FlowFixMe[missing-local-annot] get: ({get}) => { try { const value = get(resolvingSel); expect(value).toBe('READY'); return value; } catch (promise) { expect(promise instanceof Promise).toBe(true); return 'BYPASS'; } }, }); // On first read the dependency is not yet available, but the // selector catches and bypasses it. const c3 = renderElements(); expect(c3.textContent).toEqual('"BYPASS"'); // When the dependency does resolve, the selector re-evaluates // with the new data. act(() => jest.runAllTimers()); expect(c3.textContent).toEqual('"READY"'); }); testRecoil('selector catching all of 2 loads', async () => { const [resolvingSel1, res1] = asyncSelector(); const [resolvingSel2, res2] = asyncSelector(); const bypassSelector = selector({ key: 'useRecoilState/bypassing selector all', // $FlowFixMe[missing-local-annot] get: ({get}) => { let ready = 0; try { const value1 = get(resolvingSel1); expect(value1).toBe('READY1'); ready++; const value2 = get(resolvingSel2); expect(value2).toBe('READY2'); ready++; return ready; } catch (promise) { expect(promise instanceof Promise).toBe(true); return ready; } }, }); // On first read the dependency is not yet available, but the // selector catches and bypasses it. const c3 = renderElements(); expect(c3.textContent).toEqual('0'); // After the first resolution, we're still waiting on the second res1('READY1'); act(() => jest.runAllTimers()); expect(c3.textContent).toEqual('1'); // When both are available, we are done! res2('READY2'); act(() => jest.runAllTimers()); expect(c3.textContent).toEqual('2'); }); testRecoil('selector catching any of 2 loads', async () => { const resolvingSel1 = resolvingAsyncSelector('READY'); const resolvingSel2 = resolvingAsyncSelector('READY'); const bypassSelector = selector({ key: 'useRecoilState/bypassing selector any', // $FlowFixMe[missing-local-annot] get: ({get}) => { let ready = 0; for (const resolvingSel of [resolvingSel1, resolvingSel2]) { try { const value = get(resolvingSel); expect(value).toBe('READY'); ready++; } catch (promise) { expect(promise instanceof Promise).toBe(true); ready = ready; } } return ready; }, }); // On first read the dependency is not yet available, but the // selector catches and bypasses it. const c3 = renderElements(); expect(c3.textContent).toEqual('0'); // Because both dependencies are tried, they should both resolve // in parallel after one event loop. act(() => jest.runAllTimers()); expect(c3.textContent).toEqual('2'); }); // Test the ability to catch a promise for a pending dependency that we can // then handle by returning an async promise. testRecoil( 'selector catching promise and resolving asynchronously', async () => { const [originalDep, resolveOriginal] = asyncSelector(); const [bypassDep, resolveBypass] = asyncSelector(); const catchPromiseSelector = selector({ key: 'useRecoilState/catch then async', // $FlowFixMe[missing-local-annot] get: ({get}) => { try { return get(originalDep); } catch (promise) { expect(promise instanceof Promise).toBe(true); return bypassDep; } }, }); const c = renderElements(); expect(c.textContent).toEqual('loading'); act(() => jest.runAllTimers()); expect(c.textContent).toEqual('loading'); resolveBypass('BYPASS'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"BYPASS"'); resolveOriginal('READY'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"READY"'); }, ); // This tests ability to catch a pending result as a promise and // that the promise resolves to the dependency's value and it is handled // as an asynchronous selector testRecoil('selector catching promise 2', async () => { let dependencyPromiseTest; const resolvingSel = resolvingAsyncSelector('READY'); const catchPromiseSelector = selector({ key: 'useRecoilState/catch then async 2', // $FlowFixMe[missing-local-annot] get: ({get}) => { try { return get(resolvingSel); } catch (promise) { expect(promise instanceof Promise).toBe(true); // eslint-disable-next-line jest/valid-expect dependencyPromiseTest = expect(promise).resolves.toBe('READY'); return promise.then(pending => { const result = pending.value; expect(result).toBe('READY'); return result.value + ' NOW'; }); } }, }); const c = renderElements(); expect(c.textContent).toEqual('loading'); await flushPromisesAndTimers(); // NOTE!!! // The output here may be "READY NOW" if we optimize the selector to // cache the result of the async evaluation when the dependency is available // in the cache key with the dependency being available. Currently it does not // So, when the dependency is ready and the component re-renders it will // re-evaluate. At that point the dependency is now READY and thus we only // get READY and not READY NOW. // expect(c.textContent).toEqual('"READY NOW"'); expect(c.textContent).toEqual('"READY"'); // Test that the promise for the dependency that we got actually resolved // to the dependency's value. await dependencyPromiseTest; }); }); describe('Async Selectors', () => { testRecoil('Resolving async selector', async () => { const resolvingSel = resolvingAsyncSelector('READY'); // On first read it is blocked on the async selector const c1 = renderElements(); expect(c1.textContent).toEqual('loading'); // When that resolves the data is ready act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(c1.textContent).toEqual('"READY"'); }); testRecoil('Blocked on dependency', async () => { const resolvingSel = resolvingAsyncSelector('READY'); const blockedSelector = selector({ key: 'useRecoilState/blocked selector', // $FlowFixMe[missing-local-annot] get: ({get}) => get(resolvingSel), }); // On first read, the selectors dependency is still loading const c2 = renderElements(); expect(c2.textContent).toEqual('loading'); // When the dependency resolves, the data is ready act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(c2.textContent).toEqual('"READY"'); }); testRecoil('Basic async selector test', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [aSelector, _] = plusOneAsyncSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); // Begins in loading state, then shows initial value: expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('1'); // Changing dependency makes it go back to loading, then to show new value: act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('2'); // Returning to a seen value does not cause the loading state: act(() => updateValue(0)); expect(container.textContent).toEqual('1'); }); testRecoil('async dependency', async () => { const sel2 = selector({ key: 'MySelector2', get: async () => 'READY', }); const sel1 = selector({ key: 'MySelector', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await Promise.resolve(); return get(sel2); }, }); const el = renderElements(); expect(el.textContent).toEqual('loading'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(el.textContent).toEqual('"READY"'); }); testRecoil('Ability to not use Suspense', () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [aSelector, _] = plusOneAsyncSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); function ReadsAtomWithoutSuspense({ state, }: $TEMPORARY$object<{state: RecoilValueReadOnly}>) { const loadable = useRecoilValueLoadable(state); if (loadable.state === 'loading') { return 'loading not with suspense'; } else if (loadable.state === 'hasValue') { return loadable.contents; } else { throw loadable.contents; } } const container = renderElements( <> , ); // Begins in loading state, then shows initial value: expect(container.textContent).toEqual('loading not with suspense'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('1'); // Changing dependency makes it go back to loading, then to show new value: act(() => updateValue(1)); expect(container.textContent).toEqual('loading not with suspense'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('2'); // Returning to a seen value does not cause the loading state: act(() => updateValue(0)); expect(container.textContent).toEqual('1'); }); testRecoil( 'Ability to not use Suspense - with value instead of loadable', () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [aSelector, _] = plusOneAsyncSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); function ReadsAtomWithoutSuspense({ state, }: $TEMPORARY$object<{state: RecoilValueReadOnly}>) { return ( useRecoilValueLoadable(state).valueMaybe() ?? 'loading not with suspense' ); } const container = renderElements( <> , ); // Begins in loading state, then shows initial value: expect(container.textContent).toEqual('loading not with suspense'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('1'); // Changing dependency makes it go back to loading, then to show new value: act(() => updateValue(1)); expect(container.textContent).toEqual('loading not with suspense'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('2'); // Returning to a seen value does not cause the loading state: act(() => updateValue(0)); expect(container.textContent).toEqual('1'); }, ); testRecoil( 'Selector can alternate between synchronous and asynchronous', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const aSelector = selector({ key: 'alternatingSelector', // $FlowFixMe[missing-local-annot] get: ({get}) => { const x = get(anAtom); if (x === 1337) { return new Promise(() => {}); } if (x % 2 === 0) { return x; } else { return new Promise(resolve => { setTimeout(() => resolve(x), 100); }); } }, }); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); // Transition from sync to async: expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); advanceTimersBy(101); expect(container.textContent).toEqual('1'); // Transition from async to sync (with async being in hasValue state): act(() => updateValue(2)); expect(container.textContent).toEqual('2'); // Transition from async to sync (with async being in loading state): act(() => updateValue(1337)); expect(container.textContent).toEqual('loading'); act(() => updateValue(4)); await flushPromisesAndTimers(); expect(container.textContent).toEqual('4'); // Transition from sync to async with still unresolved promise from before: act(() => updateValue(5)); expect(container.textContent).toEqual('loading'); advanceTimersBy(101); await flushPromisesAndTimers(); expect(container.textContent).toEqual('5'); }, ); testRecoil( 'Async selectors do not re-query when re-subscribed from having no subscribers', async () => { const anAtom = counterAtom(); const [sel, resolvers] = asyncSelectorThatPushesPromisesOntoArray< $FlowFixMe | string, _, >(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const [Toggle, toggle] = componentThatToggles( , null, ); const container = renderElements( <> , ); expect(container.textContent).toEqual('loading'); expect(resolvers.length).toBe(1); act(() => updateValue(2)); await flushPromisesAndTimers(); expect(resolvers.length).toBe(2); resolvers[1][0]('hello'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(container.textContent).toEqual('"hello"'); // Cause sel to have no subscribers: act(() => toggle.current()); expect(container.textContent).toEqual(''); // Once it's used again, it should not issue another request: act(() => toggle.current()); expect(resolvers.length).toBe(2); expect(container.textContent).toEqual('"hello"'); }, ); testRecoil('Can move out of suspense by changing deps', async () => { const anAtom = counterAtom(); const [aSelector, resolvers] = asyncSelectorThatPushesPromisesOntoArray< $FlowFixMe | string, _, >(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements( <> , ); // While still waiting for first request, let a second faster request happen: expect(container.textContent).toEqual('loading'); expect(resolvers.length).toEqual(1); act(() => updateValue(1)); await flushPromisesAndTimers(); expect(resolvers.length).toEqual(2); expect(container.textContent).toEqual('loading'); // When the faster second request resolves, we should see its result: resolvers[1][0]('hello'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(container.textContent).toEqual('"hello"'); }); testRecoil('Can use an already-resolved promise', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [Component, updateValue] = componentThatWritesAtom(anAtom); const sel = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => { const x = get(anAtom); return Promise.resolve(x + 1); }, }); const container = renderElements( <> , ); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(container.textContent).toEqual('1'); act(() => updateValue(1)); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(container.textContent).toEqual('2'); }); testRecoil( 'Wakeup from Suspense to previous value', async ({gks, strictMode, concurrentMode}) => { const BASE_CALLS = baseRenderCount(gks); const sm = strictMode && concurrentMode ? 2 : 1; const myAtom = atom({ key: `atom${nextID++}`, default: {value: 0}, }); const mySelector = selector({ key: `selector${nextID++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom).value, }); const [Component, updateValue] = componentThatWritesAtom(myAtom); const [ReadComp, commit] = componentThatReadsAtomWithCommitCount(mySelector); const [container, suspense] = renderElementsWithSuspenseCount( <> , ); // Render initial state "0" act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('0'); expect(suspense).toHaveBeenCalledTimes(0 * sm); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); // Set selector to a pending state should cause component to suspend // $FlowFixMe[incompatible-call] act(() => updateValue({value: new Promise(() => {})})); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('loading'); expect(suspense).toHaveBeenCalledTimes(1 * sm); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); // Setting selector back to the previous state before it was pending should // wake it up and render in previous state act(() => updateValue({value: 0})); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('0'); expect(suspense).toHaveBeenCalledTimes(1 * sm); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); // Setting selector to a new state "1" should update and re-render act(() => updateValue({value: 1})); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('1'); expect(suspense).toHaveBeenCalledTimes(1 * sm); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 3); // Setting selector to the same value "1" should avoid a re-render act(() => updateValue({value: 1})); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('1'); expect(suspense).toHaveBeenCalledTimes(1 * sm); expect(commit).toHaveBeenCalledTimes( BASE_CALLS + 3 + (reactMode().mode === 'LEGACY' && !gks.includes('recoil_suppress_rerender_in_callback') ? 1 : 0), ); }, ); describe('Async selector resolution notifies all stores that read pending', () => { // Regression tests for #534: selectors used to only notify whichever store // originally caused a promise to be returned, not any stores that also read // the selector in that pending state. testRecoil('Selectors read in a snapshot notify all stores', async () => { // This version of the test uses the store inside of a Snapshot as its second store. const switchAtom = atom({ key: 'notifiesAllStores/snapshots/switch', default: false, }); const selectorA = selector({ key: 'notifiesAllStores/snapshots/a', get: () => 'foo', }); let resolve: string => void = () => { throw new Error('error in test'); }; const selectorB = selector({ key: 'notifiesAllStores/snapshots/b', get: async () => new Promise(r => { resolve = r; }), }); let doIt; function TestComponent() { const shouldQuery = useRecoilValue(switchAtom); const query = useRecoilValueLoadable( shouldQuery ? selectorB : selectorA, ); doIt = useRecoilCallback(({snapshot, set}) => () => { /** * this is required as we need the selector accessed below to outlive * the end of this callback so that the async resolution notifies the * store of the resolution and a re-render is triggered. Otherwise, the * selector will be cleaned up at the end of the callback, meaning the * resolution of the selector will not result in a re-render. */ snapshot.retain(); snapshot.getLoadable(selectorB); // cause query to be triggered in context of snapshot store set(switchAtom, true); // cause us to then read from the pending selector }); // $FlowFixMe[incompatible-type] return query.state === 'hasValue' ? query.contents : 'loading'; } // $FlowFixMe[incompatible-type-arg] const c = renderElements(); expect(c.textContent).toEqual('foo'); act(doIt); expect(c.textContent).toEqual('loading'); act(() => resolve('bar')); await act(flushPromisesAndTimers); await act(flushPromisesAndTimers); // Double flush for open source environment expect(c.textContent).toEqual('bar'); }); testRecoil('Selectors read in another root notify all roots', async () => { // This version of the test uses another RecoilRoot as its second store const switchAtom = atom({ key: 'notifiesAllStores/twoRoots/switch', default: false, }); const selectorA = selector({ key: 'notifiesAllStores/twoRoots/a', get: () => 'SELECTOR A', }); let resolve: string => void = () => { throw new Error('error in test'); }; const selectorB = selector({ key: 'notifiesAllStores/twoRoots/b', get: async () => new Promise(r => { resolve = r; }), }); function TestComponent({ setSwitch, }: { setSwitch: ((boolean) => void) => void, }) { const [shouldQuery, setShouldQuery] = useRecoilState(switchAtom); const query = useRecoilValueLoadable( shouldQuery ? selectorB : selectorA, ); setSwitch(setShouldQuery); // $FlowFixMe[incompatible-type] return query.state === 'hasValue' ? query.contents : 'loading'; } let setRootASelector; const rootA = renderElements( // $FlowFixMe[incompatible-type-arg] { setRootASelector = setSelector; }} />, ); let setRootBSelector; const rootB = renderElements( // $FlowFixMe[incompatible-type-arg] { setRootBSelector = setSelector; }} />, ); expect(rootA.textContent).toEqual('SELECTOR A'); expect(rootB.textContent).toEqual('SELECTOR A'); act(() => setRootASelector(true)); // cause rootA to read the selector expect(rootA.textContent).toEqual('loading'); expect(rootB.textContent).toEqual('SELECTOR A'); act(() => setRootBSelector(true)); // cause rootB to read the selector expect(rootA.textContent).toEqual('loading'); expect(rootB.textContent).toEqual('loading'); act(() => resolve('SELECTOR B')); await flushPromisesAndTimers(); expect(rootA.textContent).toEqual('SELECTOR B'); expect(rootB.textContent).toEqual('SELECTOR B'); }); }); describe('Async Selector Set', () => { testRecoil('set tries to get async value', () => { const myAtom = atom({key: 'selector set get async atom'}); const mySelector = selector({ key: 'selector set get async selector', get: () => myAtom, set: ({get}) => { get(myAtom); }, }); const [Comp, setState] = componentThatReadsAndWritesAtom(mySelector); renderElements(); expect(() => setState()).toThrow('selector set get async'); }); }); }); // Test the following scenario: // 0. Recoil state version 1 with A=FOO and B=BAR // 1. Component renders with A for a value of FOO // 2. Component renders with B for a value of BAR // 3. Recoil state updated to version 2 with A=FOO and B=FOO // // Step 2 may be problematic if we attempt to suppress re-renders and don't // properly keep track of previous component values when the mutable source changes. testRecoil('Updating with changed selector', ({gks}) => { if (!gks.includes('recoil_suppress_rerender_in_callback')) { return; } const atomA = atom({ key: 'selector change rerender / atomA', default: {value: 'FOO'}, }); const atomB = atom({ key: 'selector change rerender / atomB', default: {value: 'BAR'}, }); const selectorA = selector({ key: 'selector change rerender / selectorA', // $FlowFixMe[missing-local-annot] get: ({get}) => get(atomA).value, }); const selectorB = selector({ key: 'selector change rerender / selectorB', // $FlowFixMe[missing-local-annot] get: ({get}) => get(atomB).value, }); let setSide; let setB; function SelectorComponent() { const [side, setSideState] = useState('A'); setSide = setSideState; // $FlowFixMe[missing-local-annot] setB = useRecoilCallback(({snapshot, gotoSnapshot}) => value => { gotoSnapshot( snapshot.map(({set}) => { set(atomB, {value}); }), ); }); return useRecoilValue(side === 'A' ? selectorA : selectorB); } const c = renderElements(); expect(c.textContent).toEqual('FOO'); // When we change the selector we are looking up it will render other atom's value act(() => setSide('B')); expect(c.textContent).toEqual('BAR'); // When we change Recoil state the component should re-render with new value. // True even if we keep track of previous renders values to suppress re-renders when they don't change. // If we don't keep track properly when the atom changes, this may break. act(() => setB('FOO')); expect(c.textContent).toEqual('FOO'); // When we swap back to atomA it now has the same value as atomB. act(() => setSide('A')); expect(c.textContent).toEqual('FOO'); }); testRecoil('Change component prop to suspend and wake', () => { const awakeSelector = constSelector('WAKE'); const suspendedSelector = loadingAsyncSelector(); function TestComponent({ side, }: $TEMPORARY$object<{ side: $TEMPORARY$string<'AWAKE'> | $TEMPORARY$string<'SLEEP'>, }>) { return ( useRecoilValue(side === 'AWAKE' ? awakeSelector : suspendedSelector) ?? 'LOADING' ); } let setSide; const SelectorComponent = function () { const [side, setSideState] = useState('AWAKE'); setSide = setSideState; return ; }; const c = renderElements(); expect(c.textContent).toEqual('WAKE'); act(() => setSide('SLEEP')); expect(c.textContent).toEqual('loading'); act(() => setSide('AWAKE')); expect(c.textContent).toEqual('WAKE'); }); testRecoil( "Releasing snapshot doesn't invalidate pending selector", async () => { const [mySelector, resolveSelector] = asyncSelector(); // Initialize selector with snapshot first so it is initialized for both // snapshot and root and has separate cleanup handlers for both. function Component() { const callback = useRecoilCallback(({snapshot}) => () => { snapshot.getLoadable(mySelector); }); callback(); // First initialize with snapshot return useRecoilValue(mySelector); // Second initialize with RecoilRoot } const c = renderElements(); // Wait to allow the snapshot in the callback to release and call the // selector node cleanup functions. await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => resolveSelector('RESOLVE')); await flushPromisesAndTimers(); expect(c.textContent).toBe('RESOLVE'); }, ); describe('Multiple stores', () => { testRecoil('sync in multiple', () => { const myAtom = atom({key: 'selector stores sync atom', default: 'DEFAULT'}); const mySelector = selector({ key: 'selector stores sync selector', get: () => myAtom, // $FlowFixMe[missing-local-annot] set: ({set}, newValue) => set(myAtom, newValue), }); const [ComponentA, setAtomA] = componentThatReadsAndWritesAtom(mySelector); const [ComponentB, setAtomB] = componentThatReadsAndWritesAtom(mySelector); const c = renderElements( <> , ); expect(c.textContent).toBe('"DEFAULT""DEFAULT"'); act(() => setAtomA('A')); expect(c.textContent).toBe('"A""DEFAULT"'); act(() => setAtomB('B')); expect(c.textContent).toBe('"A""B"'); }); testRecoil('async in multiple', async () => { const resolvers = {}; const promises = { DEFAULT: new Promise(resolve => { // $FlowFixMe[prop-missing] resolvers.DEFAULT = resolve; }), STALE: new Promise(resolve => { // $FlowFixMe[prop-missing] resolvers.STALE = resolve; }), UPDATE: new Promise(resolve => { // $FlowFixMe[prop-missing] resolvers.UPDATE = resolve; }), }; const myAtom = atom({ key: 'selector stores async atom', default: 'DEFAULT', }); const mySelector = selector({ key: 'selector stores async selector', // $FlowFixMe[missing-local-annot] get: async ({get}) => { const side = get(myAtom); const str = await promises[side]; return side + ':' + str; }, // $FlowFixMe[missing-local-annot] set: ({set}, newValue) => set(myAtom, newValue), }); const [ComponentA, setAtomA] = componentThatReadsAndWritesAtom(mySelector); const [ComponentB, setAtomB] = componentThatReadsAndWritesAtom(mySelector); const c = renderElements( <> LOADING_A
}>
LOADING_B
}>
, ); expect(c.textContent).toBe('LOADING_ALOADING_B'); act(() => setAtomA('STALE')); expect(c.textContent).toBe('LOADING_ALOADING_B'); act(() => setAtomA('UPDATE')); expect(c.textContent).toBe('LOADING_ALOADING_B'); // $FlowFixMe[prop-missing] act(() => resolvers.STALE('STALE')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('LOADING_ALOADING_B'); // $FlowFixMe[prop-missing] act(() => resolvers.UPDATE('RESOLVE_A')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('"UPDATE:RESOLVE_A"LOADING_B'); // $FlowFixMe[prop-missing] act(() => resolvers.DEFAULT('RESOLVE_B')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('"UPDATE:RESOLVE_A""DEFAULT:RESOLVE_B"'); act(() => setAtomB('UPDATE')); expect(c.textContent).toBe('"UPDATE:RESOLVE_A""UPDATE:RESOLVE_A"'); }); testRecoil('derived in multiple', async () => { let resolveA; const atomA = atom({ key: 'selector stores derived atom A', default: new Promise(resolve => { resolveA = resolve; }), }); let resolveB; const atomB = atom({ key: 'selector stores derived atom B', default: new Promise(resolve => { resolveB = resolve; }), }); let resolveStale; const atomStale = atom({ key: 'selector stores derived atom Stale', default: new Promise(resolve => { resolveStale = resolve; }), }); const switchAtom = atom({ key: 'selector stores derived atom Switch', default: 'A', }); const mySelector = selector({ key: 'selector stores derived selector', // $FlowFixMe[missing-local-annot] get: async ({get}) => { const side = get(switchAtom); return ( side + ':' + (side === 'STALE' ? get(atomStale) : side === 'A' ? get(atomA) : get(atomB)) ); }, // $FlowFixMe[missing-local-annot] set: ({set}, newValue) => set(switchAtom, newValue), }); const [ComponentA, setAtomA] = componentThatReadsAndWritesAtom(mySelector); const [ComponentB, setAtomB] = componentThatReadsAndWritesAtom(mySelector); const c = renderElements( <> LOADING_A}> LOADING_B}> , ); expect(c.textContent).toBe('LOADING_ALOADING_B'); act(() => setAtomB('STALE')); expect(c.textContent).toBe('LOADING_ALOADING_B'); act(() => setAtomB('B')); expect(c.textContent).toBe('LOADING_ALOADING_B'); act(() => resolveStale('STALE')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('LOADING_ALOADING_B'); act(() => resolveB('RESOLVE_B')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('LOADING_A"B:RESOLVE_B"'); act(() => resolveA('RESOLVE_A')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('"A:RESOLVE_A""B:RESOLVE_B"'); act(() => setAtomA('B')); expect(c.textContent).toBe('"B:RESOLVE_B""B:RESOLVE_B"'); }); testRecoil('dynamic dependencies in multiple', async () => { const myAtom = stringAtom(); const resolvers = {}; const promises = { // $FlowFixMe[prop-missing] DEFAULT: new Promise(resolve => (resolvers.DEFAULT = resolve)), // $FlowFixMe[prop-missing] SET: new Promise(resolve => (resolvers.SET = resolve)), // $FlowFixMe[prop-missing] OTHER: new Promise(resolve => (resolvers.OTHER = resolve)), }; const mySelector = selector({ key: 'selector stores dynamic deps', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await Promise.resolve(); const x = get(myAtom); const y = await promises[x]; return x + ':' + y; }, }); // This wrapper selector is important so that the subscribing component // doesn't suspend while the selector is pending async results. // Otherwise the component may trigger re-evaluations when it wakes up // and provide a false-positive. const wrapperSelector = selector({ key: 'selector stores dynamic deps wrapper', // $FlowFixMe[missing-local-annot] get: ({get}) => { const loadable = get(noWait(mySelector)); return loadable.state === 'loading' ? 'loading' : loadable.contents; }, }); const [AtomA, setAtomA] = componentThatReadsAndWritesAtom(myAtom); let setAtomB; function SetAtomB() { setAtomB = useSetRecoilState(myAtom); return null; } const c = renderElements( <> , ); // Initial render has both stores with same pending execution await flushPromisesAndTimers(); expect(c.textContent).toBe('"DEFAULT""loading""loading"'); // Change store A to a different execution act(() => setAtomA('SET')); expect(c.textContent).toBe('"SET""loading""loading"'); // Update stoore B to test if dynamic dependency worked act(() => setAtomB('OTHER')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"SET""loading""loading"'); // Resolving original promise does nothing // $FlowFixMe[prop-missing] act(() => resolvers.DEFAULT('IGNORE')); expect(c.textContent).toBe('"SET""loading""loading"'); // Resolving store B // $FlowFixMe[prop-missing] act(() => resolvers.OTHER('OTHER')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('"SET""loading""OTHER:OTHER"'); // Resolving store A // $FlowFixMe[prop-missing] act(() => resolvers.SET('RESOLVE')); await flushPromisesAndTimers(); expect(c.textContent).toBe('"SET""SET:RESOLVE""OTHER:OTHER"'); }); // Test when multiple roots have a shared async selector with nested // dependency on an atom initialized to a promise. This stresses the // logic for getting the current pending execution across other roots. // (i.e. getExecutionInfoOfInProgressExecution() ) testRecoil('Nested atoms', async () => { const myAtom = atom({ key: 'selector stores nested atom', default: 'DEFAULT', effects: [ ({setSelf}) => { setSelf(new Promise(() => {})); }, ], }); const innerSelector = selector({ key: 'selector stores nested atom inner', get: () => myAtom, }); const outerSelector = selector({ key: 'selector stores nested atom outer', get: () => innerSelector, }); let setAtomA; function SetAtomA() { setAtomA = useSetRecoilState(myAtom); return null; } let setAtomB; function SetAtomB() { setAtomB = useSetRecoilState(myAtom); return null; } const c = renderUnwrappedElements( <> , ); expect(c.textContent).toBe('LOAD_A LOAD_B '); act(() => { setAtomA('SETA'); setAtomB('SETB'); }); await flushPromisesAndTimers(); expect(c.textContent).toBe('"SETA""SETB"'); }); // Test that when a store is re-using another store's execution of a selector // that async dependencies are updated so it can stop re-using it if state // diverges from the original store. testRecoil('Diverging shared selectors', async () => { const myAtom = stringAtom(); atom({ key: 'selector stores diverging atom', default: 'DEFAULT', }); const mySelector = selector({ key: 'selector stores diverging selector', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await Promise.resolve(); const value = get(myAtom); await Promise.resolve(); // So resolution occurs during act() if (value === 'RESOLVE') { return value; } await new Promise(() => {}); }, }); let setAtomA; function SetAtomA() { setAtomA = useSetRecoilState(myAtom); return null; } let setAtomB; function SetAtomB() { setAtomB = useSetRecoilState(myAtom); return null; } const c = renderUnwrappedElements( <> , ); expect(c.textContent).toBe('LOAD_A LOAD_B '); act(() => { setAtomA('SETA'); }); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('LOAD_A LOAD_B '); act(() => { setAtomB('RESOLVE'); }); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('LOAD_A "RESOLVE"'); }); testRecoil('Diverged shared selectors', async () => { const myAtom = stringAtom(); atom({ key: 'selector stores diverged atom', default: 'DEFAULT', }); let addDeps; const addDepsPromise = new Promise(resolve => { addDeps = resolve; }); const mySelector = selector({ key: 'selector stores diverged selector', // $FlowFixMe[missing-local-annot] get: async ({get}) => { await addDepsPromise; const value = get(myAtom); await Promise.resolve(); // So resolution occurs during act() if (value === 'RESOLVE') { return value; } await new Promise(() => {}); }, }); let setAtomA; function SetAtomA() { setAtomA = useSetRecoilState(myAtom); return null; } let setAtomB; function SetAtomB() { setAtomB = useSetRecoilState(myAtom); return null; } const c = renderUnwrappedElements( <> , ); expect(c.textContent).toBe('LOAD_A LOAD_B '); act(() => { setAtomA('SETA'); setAtomB('RESOLVE'); }); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('LOAD_A LOAD_B '); await act(async () => { addDeps(); }); await flushPromisesAndTimers(); await flushPromisesAndTimers(); // Double flush for open source environment expect(c.textContent).toBe('LOAD_A "RESOLVE"'); }); }); describe('Counts', () => { describe('Evaluation', () => { testRecoil('Selector functions are evaluated just once', () => { const anAtom = counterAtom(); const [aSelector, selectorFn] = plusOneSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); renderElements( <> , ); expect(selectorFn).toHaveBeenCalledTimes(1); act(() => updateValue(1)); expect(selectorFn).toHaveBeenCalledTimes(2); }); testRecoil( 'Selector functions are evaluated just once even if multiple upstreams change', () => { const atomA = counterAtom(); const atomB = counterAtom(); const [aSelector, selectorFn] = additionSelector(atomA, atomB); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); renderElements( <> , ); expect(selectorFn).toHaveBeenCalledTimes(1); act(() => { batchUpdates(() => { updateValueA(1); updateValueB(1); }); }); expect(selectorFn).toHaveBeenCalledTimes(2); }, ); /** * This test ensures that we are not running the selector's get() an unnecessary * number of times in response to async selectors resolving (i.e. by retrying * more times than we have to or creating numerous promises that retry). */ testRecoil( 'async selector runs the minimum number of times required', async () => { const [asyncDep1, resolveAsyncDep1] = asyncSelector(); const [asyncDep2, resolveAsyncDep2] = asyncSelector(); let numTimesRan = 0; const selectorWithAsyncDeps = selector({ key: 'selectorRunsMinTimes', // $FlowFixMe[missing-local-annot] get: async ({get}) => { numTimesRan++; return get(asyncDep1) + get(asyncDep2); }, }); const container = renderElements( , ); expect(numTimesRan).toBe(1); act(() => resolveAsyncDep1('a')); await flushPromisesAndTimers(); expect(numTimesRan).toBe(2); act(() => resolveAsyncDep2('b')); await flushPromisesAndTimers(); expect(numTimesRan).toBe(3); await flushPromisesAndTimers(); expect(container.textContent).toEqual('"ab"'); }, ); }); describe('Render', () => { testRecoil("Updating with same value doesn't rerender", ({gks}) => { if (!gks.includes('recoil_suppress_rerender_in_callback')) { return; } const myAtom = atom({ key: 'selector same value rerender / atom', default: {value: 'DEFAULT'}, }); const mySelector = selector({ key: 'selector - same value rerender', // $FlowFixMe[missing-local-annot] get: ({get}) => get(myAtom).value, }); let setAtom; let resetAtom; let renders = 0; function SelectorComponent() { const value = useRecoilValue(mySelector); const setAtomValue = useSetRecoilState(myAtom); const resetAtomValue = useResetRecoilState(myAtom); setAtom = (x: $TEMPORARY$string<'CHANGE'> | $TEMPORARY$string<'SET'>) => setAtomValue({value: x}); resetAtom = resetAtomValue; return value; } expect(renders).toEqual(0); const c = renderElements( { renders++; }}> , ); // Initial render happens one time in www and 2 times in oss. // resetting the counter to 1 after the initial render to make them // the same in both repos. 2 renders probably need to be looked into. renders = 1; expect(c.textContent).toEqual('DEFAULT'); act(() => setAtom('SET')); expect(c.textContent).toEqual('SET'); expect(renders).toEqual(2); act(() => setAtom('SET')); expect(c.textContent).toEqual('SET'); expect(renders).toEqual(2); act(() => setAtom('CHANGE')); expect(c.textContent).toEqual('CHANGE'); expect(renders).toEqual(3); act(resetAtom); expect(c.textContent).toEqual('DEFAULT'); expect(renders).toEqual(4); act(resetAtom); expect(c.textContent).toEqual('DEFAULT'); expect(renders).toEqual(4); }); testRecoil( 'Resolution of suspense causes render just once', async ({gks, strictMode, concurrentMode}) => { const BASE_CALLS = baseRenderCount(gks); const sm = strictMode && concurrentMode ? 2 : 1; jest.useFakeTimers(); const anAtom = counterAtom(); const [aSelector, _] = plusOneAsyncSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const [ReadComp, commit] = componentThatReadsAtomWithCommitCount(aSelector); const [__, suspense] = renderElementsWithSuspenseCount( <> , ); // Begins in loading state, then shows initial value: act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(suspense).toHaveBeenCalledTimes(1 * sm); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); // Changing dependency makes it go back to loading, then to show new value: act(() => updateValue(1)); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(suspense).toHaveBeenCalledTimes(2 * sm); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); // Returning to a seen value does not cause the loading state: act(() => updateValue(0)); await flushPromisesAndTimers(); expect(suspense).toHaveBeenCalledTimes(2 * sm); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 3); }, ); }); }); ================================================ FILE: packages/recoil-relay/RecoilRelay_Environments.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Snapshot, StoreID} from 'Recoil'; import type {IEnvironment} from 'relay-runtime'; const {useRecoilStoreID} = require('Recoil'); const React = require('react'); const {useEffect} = require('react'); const {RelayEnvironmentProvider} = require('react-relay'); const err = require('recoil-shared/util/Recoil_err'); class EnvironmentKey { _name: string; constructor(name: string) { this._name = name; } toJSON(): string { return this._name; } } const environmentStore: Map< StoreID, Map, > = new Map(); const cleanupHandlers: Map> = new Map(); function registerRelayEnvironment( storeID: StoreID, environment: IEnvironment, environmentKey: EnvironmentKey, ): () => void { if (!environmentStore.has(storeID)) { environmentStore.set(storeID, new Map()); } const previousEnvironment = environmentStore .get(storeID) ?.get(environmentKey); if (previousEnvironment != null && previousEnvironment !== environment) { throw err( `A consistent Relay environment should be used with the same Recoil store and EnvironmentKey "${environmentKey.toJSON()}"`, ); } environmentStore.get(storeID)?.set(environmentKey, environment); // Cleanup registered Relay Environments when they are no longer used to // avoid memory leaks. However, defer it for an event look in case we are // running in which may call the effects, cleanup the effects, // and then call the effects of other components which try to re-query before // this effect to re-register gets re-called. This should be safe because in // production the environment registered should never change. const pendingCleanup = cleanupHandlers.get(storeID)?.get(environmentKey); if (pendingCleanup != null) { window.clearTimeout(pendingCleanup); cleanupHandlers.get(storeID)?.delete(environmentKey); } return () => { const cleanupHandle = window.setTimeout(() => { environmentStore.get(storeID)?.delete(environmentKey); }, 0); const oldHandler = cleanupHandlers.get(storeID)?.get(environmentKey); if (oldHandler != null) { window.clearTimeout(oldHandler); } if (!cleanupHandlers.has(storeID)) { cleanupHandlers.set(storeID, new Map()); } cleanupHandlers.get(storeID)?.set(environmentKey, cleanupHandle); }; } /** * @explorer-desc * Associates a RelayEnvironment with an EnvironmentKey for this . */ function RecoilRelayEnvironment({ environmentKey, environment, children, }: { environmentKey: EnvironmentKey, environment: IEnvironment, children: React.Node, }): React.Node { const storeID = useRecoilStoreID(); registerRelayEnvironment(storeID, environment, environmentKey); // Cleanup to avoid leaking retaining Relay Environments. useEffect( () => registerRelayEnvironment(storeID, environment, environmentKey), [storeID, environment, environmentKey], ); return children; } /** * A provider which sets up the Relay environment for its children and * registers that environment for any Recoil atoms or selectors using that * environmentKey. * * This is basically a wrapper around both and * . */ function RecoilRelayEnvironmentProvider({ environmentKey, environment, children, }: { environmentKey: EnvironmentKey, environment: IEnvironment, children: React.Node, }): React.Node { return ( {children} ); } function registerRecoilSnapshotRelayEnvironment( snapshot: Snapshot, environmentKey: EnvironmentKey, environment: IEnvironment, ): () => void { const storeID = snapshot.getStoreID(); return registerRelayEnvironment(storeID, environment, environmentKey); } function getRelayEnvironment( environmentOpt: IEnvironment | EnvironmentKey, storeID: StoreID, parentStoreID?: StoreID, ): IEnvironment { if (environmentOpt instanceof EnvironmentKey) { const environment = environmentStore.get(storeID)?.get(environmentOpt) ?? (parentStoreID != null ? environmentStore.get(parentStoreID)?.get(environmentOpt) : null); if (environment == null) { throw err( ` must be used at the top of your with the same EnvironmentKey "${environmentOpt.toJSON()}" to register the Relay environment.`, ); } return environment; } return environmentOpt; } module.exports = { EnvironmentKey, RecoilRelayEnvironment, RecoilRelayEnvironmentProvider, registerRecoilSnapshotRelayEnvironment, getRelayEnvironment, }; ================================================ FILE: packages/recoil-relay/RecoilRelay_graphQLMutationEffect.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {EnvironmentKey} from './RecoilRelay_Environments'; import type {AtomEffect, RecoilState} from 'Recoil'; import type {Variables} from 'react-relay'; import type { IEnvironment, Mutation, SelectorStoreUpdater, UploadableMap, } from 'relay-runtime'; const {getRelayEnvironment} = require('./RecoilRelay_Environments'); const {commitMutation} = require('react-relay'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); // TODO Use async atom support to set atom to error state. // For now, log as a recoverableViolation() so errors aren't lost. function logError(node: RecoilState, msg: string) { recoverableViolation( `Error syncing atom "${node.key}" with GraphQL: ${msg}`, 'recoil', ); } /** graphQLMutationEffect() * Commit a GraphQL mutation each time the atom value is mutated. * - `environment`: The Relay Environment or an EnvironmentKey to match with * the environment provided with ``. * - `mutation`: The GraphQL mutation. * - `variables`: Variables object provided as input to GraphQL mutation. It is * a callback that receives the updated atom value as the parameter. * If null, then skip mutation. * - `updater`: Optional `updater()` function passed to `commitMutation()`. * - `optimisticUpdater`: Optional `optimisticUpdater()` function passed to `commitMutation()`. * - `optimisticResponse`: Optional optimistic response passed to `commitMutation()`. * - `uploadables`: Optional `uploadables` passed to `commitMutation()`. */ function graphQLMutationEffect< TVariables: Variables, T, TResponse: $ReadOnly<{[string]: mixed}> = {}, TRawResponse = void, >({ environment: environmentOpt, mutation, variables, updater_UNSTABLE: updater, optimisticUpdater_UNSTABLE: optimisticUpdater, optimisticResponse_UNSTABLE: optimisticResponse, uploadables_UNSTABLE: uploadables, }: { environment: IEnvironment | EnvironmentKey, mutation: Mutation, variables: T => TVariables | null, updater_UNSTABLE?: SelectorStoreUpdater, optimisticUpdater_UNSTABLE?: SelectorStoreUpdater, optimisticResponse_UNSTABLE?: T => TResponse, uploadables_UNSTABLE?: UploadableMap, }): AtomEffect { let currentMutationID = 0; return ({node, onSet, setSelf, storeID, parentStoreID_UNSTABLE}) => { const environment = getRelayEnvironment( environmentOpt, storeID, parentStoreID_UNSTABLE, ); // Local atom mutations will sync to update remote state // Treat as write-through cache, so local atom will update immediatly // and then write through to GraphQL mutation. onSet((newValue, oldValue) => { const mutationID = ++currentMutationID; const mutationVariables = variables(newValue); if (mutationVariables != null) { commitMutation(environment, { mutation, variables: mutationVariables, onError: error => { logError(node, error.message ?? 'GraphQL Mutation'); // TODO, use logError() to set atom to error state instead? if (mutationID === currentMutationID) { setSelf(potentialyBadValue => // Avoid reverting value if atom was set in the meantime even if // the newer commitMutation() hasn't started yet. potentialyBadValue === newValue ? oldValue : potentialyBadValue, ); } }, updater, optimisticUpdater, /* $FlowFixMe[incompatible-call] error exposed when improving flow * typing of commitMutation */ optimisticResponse: optimisticResponse?.(newValue), uploadables, }); } }); }; } module.exports = graphQLMutationEffect; ================================================ FILE: packages/recoil-relay/RecoilRelay_graphQLQueryEffect.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {EnvironmentKey} from './RecoilRelay_Environments'; import type {AtomEffect, RecoilState} from 'Recoil'; import type {Variables} from 'react-relay'; import type {GraphQLSubscription, IEnvironment, Query} from 'relay-runtime'; const {getRelayEnvironment} = require('./RecoilRelay_Environments'); const {fetchQuery} = require('react-relay'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); const { createOperationDescriptor, getRequest, handlePotentialSnapshotErrors, } = require('relay-runtime'); // TODO Use async atom support to set atom to error state. // For now, log as a recoverableViolation() so errors aren't lost. function logError(node: RecoilState, msg: string) { recoverableViolation( `Error syncing atom "${node.key}" with GraphQL: ${msg}`, 'recoil', ); } function subscibeToLocalRelayCache< TVariables: Variables, TData: $ReadOnly<{[string]: mixed}>, TRawResponse = void, >( environment: IEnvironment, query: | Query | GraphQLSubscription, variables: TVariables, onNext: TData => void, ): () => void { const request = getRequest(query); const operation = createOperationDescriptor(request, variables); const operationDisposable = environment.retain(operation); const snapshot = environment.lookup(operation.fragment); const subscriptionDisposable = environment.subscribe( snapshot, newSnapshot => { handlePotentialSnapshotErrors( environment, newSnapshot.missingRequiredFields, newSnapshot.relayResolverErrors, ); if (!newSnapshot.isMissingData && newSnapshot.data != null) { // $FlowExpectedError[incompatible-call] onNext(newSnapshot.data); } }, ); return () => { operationDisposable?.dispose(); subscriptionDisposable?.dispose(); }; } /** graphQLQueryEffect() * Initialize an atom based on the results of a GraphQL query. * - `environment`: The Relay Environment or an EnvironmentKey to match with * the environment provided with ``. * - `query`: The GraphQL query to query. * - `variables`: Variables object provided as input to GraphQL query. * If null, then skip query and use default value. * - `mapResponse`: Callback to map the query response to the atom value. * - `subscribeToLocalMutations_UNSTABLE` - By default this effect will subscribe to * mutations from local `commitMutation()` or `graphQLMutationEffect()` for the * same part of the graph. If you also need to subscribe to remote mutations, * then use `graphQLSubscriptionEffect()`. */ function graphQLQueryEffect< TVariables: Variables, TData: $ReadOnly<{[string]: mixed}>, T = TData, TRawResponse = void, >({ environment: environmentOpt, query, variables, mapResponse, subscribeToLocalMutations_UNSTABLE = true, }: { environment: IEnvironment | EnvironmentKey, query: Query, variables: TVariables | null, mapResponse: TData => T, subscribeToLocalMutations_UNSTABLE?: boolean, }): AtomEffect { return ({node, setSelf, trigger, storeID, parentStoreID_UNSTABLE}) => { if (variables == null) { return; } let querySubscription, localSubscriptionCleanup; const environment = getRelayEnvironment( environmentOpt, storeID, parentStoreID_UNSTABLE, ); // Initialize value if (trigger === 'get') { let initialResolve, initialReject; setSelf( new Promise((resolve, reject) => { initialResolve = resolve; initialReject = reject; }), ); querySubscription = fetchQuery(environment, query, variables, { fetchPolicy: 'store-or-network', }).subscribe({ next: response => { const data = mapResponse(response); initialResolve(data); setSelf(data); }, // TODO use Async atom support to set atom to error state on // subsequent errors during incremental updates. error: error => { initialReject(error); logError(node, error.message ?? 'Error'); }, }); } // Subscribe to local changes to update atom state. // To get remote mutations please use graphQLSubscriptionEffect() if (subscribeToLocalMutations_UNSTABLE) { localSubscriptionCleanup = subscibeToLocalRelayCache( environment, query, variables, data => setSelf(mapResponse(data)), ); } return () => { querySubscription?.unsubscribe(); localSubscriptionCleanup?.(); }; }; } module.exports = graphQLQueryEffect; ================================================ FILE: packages/recoil-relay/RecoilRelay_graphQLSelector.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {GetRecoilValue, RecoilState} from 'Recoil'; import type {EnvironmentKey} from 'RecoilRelay_Environments'; import type {Variables} from 'react-relay'; import type { GraphQLSubscription, IEnvironment, Mutation, Query, } from 'relay-runtime'; const graphQLSelectorFamily = require('./RecoilRelay_graphQLSelectorFamily'); /** * graphQLSelector() implements a Recoil selector that syncs with a * GraphQL Query or Subscription. Other upstream atoms/selectors can be used * to help define the query variables or transform the results. * * The selector is writable to act as a local cache of the server. * A GraphQL Mutation may also be provided to use the selector as a * write-through cache for updates to commit to the server. * * - `key` - Unique key string to identify this graphQLSelectorFamily * - `environment`: The Relay Environment or an EnvironmentKey to match with * the environment provided with ``. * - `query` - GraphQL Query or Subscription * - `variables` - Callback to get the Variables to use for this query that may * be based on other upstream atoms or selectors. * Return `null` to avoid the query if appropriate and use the `default` value. * - `default` - Optional default value to use if the query is skipped. * - `mapResponse` - Optional callback to transform the results based on * other upstream atoms or selectors. * - `mutations` - Optional GraphQL Mutation and variables to commit when the * selector is written to. */ function graphQLSelector< TVariables: Variables, TData: $ReadOnly<{[string]: mixed}>, T = TData, TRawResponse = void, TMutationVariables: Variables = {}, TMutationData: $ReadOnly<{[string]: mixed}> = {}, TMutationRawResponse = void, >({ variables, mutations, ...options }: { environment: IEnvironment | EnvironmentKey, key: string, query: | Query | GraphQLSubscription, // The order of union members below is important to prevent errors at the call-site variables: (({get: GetRecoilValue}) => TVariables | null) | TVariables, mapResponse: (TData, {get: GetRecoilValue, variables: TVariables}) => T, // The default value to use if variables returns null default?: T, mutations?: { mutation: Mutation, variables: T => TMutationVariables | null, }, }): RecoilState { // $FlowFixMe[incompatible-call] return graphQLSelectorFamily({ ...options, variables: () => cbs => typeof variables === 'function' ? variables(cbs) : variables, mutations: mutations == null ? undefined : {...mutations}, })(); } module.exports = graphQLSelector; ================================================ FILE: packages/recoil-relay/RecoilRelay_graphQLSelectorFamily.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {EnvironmentKey} from './RecoilRelay_Environments'; import type {GetRecoilValue, Parameter, RecoilState} from 'Recoil'; import type {Variables} from 'react-relay'; import type { GraphQLSubscription, IEnvironment, Mutation, Query, } from 'relay-runtime'; const {DefaultValue, atomFamily, selectorFamily} = require('Recoil'); const graphQLMutationEffect = require('./RecoilRelay_graphQLMutationEffect'); const graphQLQueryEffect = require('./RecoilRelay_graphQLQueryEffect'); const graphQLSubscriptionEffect = require('./RecoilRelay_graphQLSubscriptionEffect'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); /** * graphQLSelectorFamily() implements a selectorFamily() that syncs with a * GraphQL Query or Subscription. The family parameter or other upstream * atoms/selectors can be used to define the query variables or transform the * results. * * The selector is writable to act as a local cache of the server. * A GraphQL Mutation may also be provided to use the selector as a * write-through cache for updates to commit to the server. * * - `key` - Unique key string to identify this graphQLSelectorFamily * - `environment`: The Relay Environment or an EnvironmentKey to match with * the environment provided with ``. * - `query` - GraphQL Query or Subscription * - `variables` - Callback to get the Variables to use for this query based * on the family parameters and/or other upstream atoms or selectors. * Return `null` to avoid the query if appropriate and use the `default` value. * - `default` - Optional default value to use if the query is skipped. * - `mapResponse` - Optional callback to transform the results based on the * family parameters or upstream atoms or selectors. * - `mutations` - Optional GraphQL Mutation and variables to commit when the * selector is written to. */ function graphQLSelectorFamily< TVariables: Variables, TData: $ReadOnly<{[string]: mixed}>, P: Parameter = TVariables, T = TData, TRawResponse = void, TMutationVariables: Variables = {}, TMutationData: $ReadOnly<{[string]: mixed}> = {}, TMutationRawResponse = void, >({ key, environment, query, variables, mapResponse, mutations, ...options }: { key: string, environment: IEnvironment | EnvironmentKey, query: | Query | GraphQLSubscription, variables: | TVariables | (P => TVariables | null | (({get: GetRecoilValue}) => TVariables | null)), mapResponse: ( TData, {get: GetRecoilValue, variables: TVariables}, ) => T | (P => T), // The default value to use if variables returns null default?: T | (P => T), mutations?: { mutation: Mutation, variables: T => | TMutationVariables | null | (P => TMutationVariables | null), }, }): P => RecoilState { const internalAtoms = atomFamily< | {source: 'local', parameter: P, data: T} | {source: 'remote', response: TData} | DefaultValue, TVariables | null, // $FlowFixMe[incompatible-call] >({ key, default: new DefaultValue(), effects: vars => [ query.params.operationKind === 'query' ? graphQLQueryEffect({ environment, variables: vars, // $FlowIssue[incompatible-call] Type is opaque, no way to refine query, mapResponse: response => ({source: 'remote', response}), }) : graphQLSubscriptionEffect({ environment, variables: vars, // $FlowIssue[incompatible-call] Type is opaque, no way to refine subscription: query, mapResponse: response => ({source: 'remote', response}), }), mutations && graphQLMutationEffect({ environment, mutation: mutations.mutation, // $FlowFixMe[missing-local-annot] variables: localUpdate => { if ( // commit mutation only if atom is updated locally localUpdate.source === 'local' && // Avoid mutation operation if user issued a reset and // did not provide a default value. !(localUpdate instanceof DefaultValue) ) { const variablesIntermediate = mutations.variables( localUpdate.data, ); return typeof variablesIntermediate === 'function' ? variablesIntermediate(localUpdate.parameter) : variablesIntermediate; } else { return null; } }, }), ].filter(Boolean), }); function getVariables(parameter: P, get: GetRecoilValue) { const variablesIntermediate: | null | TVariables | (({get: GetRecoilValue}) => TVariables | null) = typeof variables === 'function' ? variables(parameter) : variables; return typeof variablesIntermediate === 'function' ? variablesIntermediate({get}) : variablesIntermediate; } const defaultValue: P => T = parameter => typeof options.default === 'function' ? // $FlowIssue[incompatible-use] options.default(parameter) : // $FlowIssue[incompatible-type] options.default; return selectorFamily({ key: `${key}__Wrapper`, get: parameter => ({get}) => { const vars = getVariables(parameter, get); const result = get(internalAtoms(vars)); if (result instanceof DefaultValue) { return 'default' in options ? defaultValue(parameter) : new Promise(() => {}); } if (result.source === 'local') { return result.data; } const mapped = mapResponse(result.response, { get, variables: nullthrows(vars), }); return typeof mapped === 'function' ? // $FlowIssue[incompatible-use] mapped(parameter) : mapped; }, set: parameter => ({set, get}, newValue) => set( internalAtoms(getVariables(parameter, get)), newValue instanceof DefaultValue ? 'default' in options ? {source: 'local', parameter, data: defaultValue(parameter)} : newValue : {source: 'local', parameter, data: newValue}, ), }); } module.exports = graphQLSelectorFamily; ================================================ FILE: packages/recoil-relay/RecoilRelay_graphQLSubscriptionEffect.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {EnvironmentKey} from './RecoilRelay_Environments'; import type {AtomEffect, RecoilState} from 'Recoil'; import type {Variables} from 'react-relay'; import type {GraphQLSubscription, IEnvironment} from 'relay-runtime'; const {getRelayEnvironment} = require('./RecoilRelay_Environments'); const {requestSubscription} = require('react-relay'); const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); // TODO Use async atom support to set atom to error state. // For now, log as a recoverableViolation() so errors aren't lost. function logError(node: RecoilState, msg: string) { recoverableViolation( `Error syncing atom "${node.key}" with GraphQL: ${msg}`, 'recoil', ); } /** graphQLSubscriptionEffect() * Initialize and subscribe an atom to a GraphQL subscription. * - `environment`: The Relay Environment or an EnvironmentKey to match with * the environment provided with ``. * - `subscription`: The GraphQL subscription to query. * - `variables`: Variables object provided as input to GraphQL subscription. * If null, then skip subscription and use default value. * - `mapResponse`: Callback to map the subscription response to the atom value. */ function graphQLSubscriptionEffect< TVariables: Variables, TData: $ReadOnly<{[string]: mixed}>, T = TData, TRawResponse = void, >({ environment: environmentOpt, subscription, variables, mapResponse, }: { environment: IEnvironment | EnvironmentKey, subscription: GraphQLSubscription, variables: TVariables | null, mapResponse: TData => T, }): AtomEffect { return ({node, setSelf, trigger, storeID, parentStoreID_UNSTABLE}) => { if (variables == null) { return; } const environment = getRelayEnvironment( environmentOpt, storeID, parentStoreID_UNSTABLE, ); let initialResolve, initialReject; if (trigger === 'get') { setSelf( new Promise((resolve, reject) => { initialResolve = resolve; initialReject = reject; }), ); } // Subscribe to remote changes to update atom state const graphQLSubscriptionDisposable = requestSubscription(environment, { subscription, variables, onNext: response => { if (response != null) { const data = mapResponse(response); initialResolve?.(data); setSelf(data); } }, // TODO use Async atom support to set atom to error state on // subsequent errors during incremental updates. onError: error => { initialReject?.(error); logError(node, error.message ?? 'Error'); }, }); return () => { graphQLSubscriptionDisposable.dispose(); }; }; } module.exports = graphQLSubscriptionEffect; ================================================ FILE: packages/recoil-relay/RecoilRelay_index.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { EnvironmentKey, RecoilRelayEnvironment, RecoilRelayEnvironmentProvider, registerRecoilSnapshotRelayEnvironment, } = require('./RecoilRelay_Environments'); const graphQLMutationEffect = require('./RecoilRelay_graphQLMutationEffect'); const graphQLQueryEffect = require('./RecoilRelay_graphQLQueryEffect'); const graphQLSelector = require('./RecoilRelay_graphQLSelector'); const graphQLSelectorFamily = require('./RecoilRelay_graphQLSelectorFamily'); const graphQLSubscriptionEffect = require('./RecoilRelay_graphQLSubscriptionEffect'); module.exports = { EnvironmentKey, RecoilRelayEnvironment, RecoilRelayEnvironmentProvider, registerRecoilSnapshotRelayEnvironment, graphQLQueryEffect, graphQLSubscriptionEffect, graphQLMutationEffect, graphQLSelector, graphQLSelectorFamily, }; ================================================ FILE: packages/recoil-relay/__test_utils__/RecoilRelay_mockRelayEnvironment.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Snapshot} from 'Recoil'; const {snapshot_UNSTABLE} = require('Recoil'); const { EnvironmentKey, RecoilRelayEnvironment, registerRecoilSnapshotRelayEnvironment, } = require('../RecoilRelay_Environments'); const React = require('react'); const { renderElements: renderRecoilElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const {createMockEnvironment} = require('relay-test-utils'); type RelayMockEnvironment = $Call; function mockRelayEnvironment(): { environment: RelayMockEnvironment, mockEnvironmentKey: EnvironmentKey, renderElements: React.Node => HTMLDivElement, snapshot: Snapshot, } { const environment = createMockEnvironment(); const mockEnvironmentKey = new EnvironmentKey('Mock'); function renderElements(elements: React.Node) { return renderRecoilElements( {elements} , ); } const snapshot = snapshot_UNSTABLE(); snapshot.retain(); registerRecoilSnapshotRelayEnvironment( snapshot, mockEnvironmentKey, environment, ); return { environment, mockEnvironmentKey, renderElements, snapshot, }; } module.exports = mockRelayEnvironment; ================================================ FILE: packages/recoil-relay/__tests__/RecoilRelay_RecoilRelayEnvironment-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RelayMockEnvironment} from '../../../../relay/oss/relay-test-utils/RelayModernMockEnvironment'; import type {Node} from 'react'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, MockPayloadGenerator, createMockEnvironment, testFeedbackQuery, graphQLQueryEffect, EnvironmentKey, ReadsAtom, ErrorBoundary, renderRecoilElements, useState, atom, atomFamily, useRelayEnvironment, graphQLSelectorFamily, RecoilRelayEnvironment, RecoilRelayEnvironmentProvider, flushPromisesAndTimers; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); ({atom, atomFamily} = require('Recoil')); ({useRelayEnvironment} = require('react-relay')); ({act} = require('ReactTestUtils')); ({MockPayloadGenerator, createMockEnvironment} = require('relay-test-utils')); ({ ReadsAtom, flushPromisesAndTimers, ErrorBoundary, renderElements: renderRecoilElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({testFeedbackQuery} = require('./mock-graphql/RecoilRelay_MockQueries')); ({ EnvironmentKey, RecoilRelayEnvironment, RecoilRelayEnvironmentProvider, } = require('../RecoilRelay_Environments')); graphQLSelectorFamily = require('../RecoilRelay_graphQLSelectorFamily'); graphQLQueryEffect = require('../RecoilRelay_graphQLQueryEffect'); }); describe('Multiple Environments', () => { testRecoil('graphQLQueryEffect()', async () => { const environmentA = createMockEnvironment(); const environmentB = createMockEnvironment(); const envA = new EnvironmentKey('A'); const envB = new EnvironmentKey('B'); const myAtoms = atomFamily({ key: 'graphql multiple environments', effects: id => [ graphQLQueryEffect({ environment: id === 'A' ? envA : envB, query: testFeedbackQuery, variables: {id}, mapResponse: ({feedback}) => feedback?.seen_count, }), ], }); function AssertEnvironment({ environment, }: $TEMPORARY$object<{environment: RelayMockEnvironment}>) { expect(environment).toBe(useRelayEnvironment()); return null; } let swapEnvironments; function RegisterRelayEnvironments({ children, }: $TEMPORARY$object<{children: Array}>) { const [changeEnv, setChangeEnv] = useState(false); swapEnvironments = () => setChangeEnv(true); return ( {children} ); } const c = renderRecoilElements( e.message}> , ); expect(c.textContent).toBe('loading'); act(() => environmentA.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { Feedback: () => ({seen_count: 123}), }), ), ); act(() => environmentB.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { Feedback: () => ({seen_count: 456}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123456'); act(swapEnvironments); expect(c.textContent).toEqual(expect.stringContaining('EnvironmentKey')); }); testRecoil('graphQLSelectorFamily', async () => { const environmentA = createMockEnvironment(); const environmentB = createMockEnvironment(); const envA = new EnvironmentKey('A'); const envB = new EnvironmentKey('B'); const queryA = graphQLSelectorFamily({ key: 'graphql multiple environments A', environment: envA, // $FlowFixMe[incompatible-call] query: testFeedbackQuery, // $FlowFixMe[incompatible-function-indexer] variables: id => ({id}), mapResponse: data => data.feedback?.seen_count, }); const queryB = graphQLSelectorFamily({ key: 'graphql multiple environments B', environment: envB, // $FlowFixMe[incompatible-call] query: testFeedbackQuery, // $FlowFixMe[incompatible-function-indexer] variables: id => ({id}), mapResponse: data => data.feedback?.seen_count, }); function AssertEnvironment({ environment, }: $TEMPORARY$object<{environment: RelayMockEnvironment}>) { expect(environment).toBe(useRelayEnvironment()); return null; } let swapEnvironments; function RegisterRelayEnvironments({ children, }: $TEMPORARY$object<{children: Array}>) { const [changeEnv, setChangeEnv] = useState(false); swapEnvironments = () => setChangeEnv(true); return ( {children} ); } const c = renderRecoilElements( e.message}> {/* $FlowFixMe[incompatible-call] */} {/* $FlowFixMe[incompatible-call] */} , ); expect(c.textContent).toBe('loading'); act(() => environmentA.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { Feedback: () => ({seen_count: 123}), }), ), ); act(() => environmentB.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { Feedback: () => ({seen_count: 456}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123456'); act(swapEnvironments); expect(c.textContent).toEqual(expect.stringContaining('EnvironmentKey')); }); }); // Confirm there is no memory leak by releasing Relay environments testRecoil('Relay environment is unloaded', async () => { const environment = createMockEnvironment(); const enviornmentKey = new EnvironmentKey('env'); const queryA = atom({ key: 'graphql query preloaded 1', effects: [ graphQLQueryEffect({ environment: enviornmentKey, query: testFeedbackQuery, variables: {id: 'ID'}, // $FlowFixMe[incompatible-call] mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); const queryB = atom({ key: 'graphql query preloaded 2', effects: [ graphQLQueryEffect({ environment: enviornmentKey, query: testFeedbackQuery, variables: {id: 'ID'}, // $FlowFixMe[incompatible-call] mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); environment.mock.queueOperationResolver(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ); environment.mock.queuePendingOperation(testFeedbackQuery, {id: 'ID'}); let unmountEnvironment, issueNewQuery; function Component() { const [unmount, setUnmount] = useState(false); const [newQuery, setNewQuery] = useState(false); unmountEnvironment = () => setUnmount(true); issueNewQuery = () => setNewQuery(true); return unmount ? ( newQuery ? ( e.message}> ) : (
) ) : ( ); } const c = renderRecoilElements(); // Confirm data is available synchronously with the first render expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":123}}'); // Unmount and give it a chance to cleanup act(unmountEnvironment); await flushPromisesAndTimers(); // Confirm environment is not retained and an error is thrown with query attempt act(issueNewQuery); expect(c.textContent).toEqual( expect.stringContaining('RecoilRelayEnvironment'), ); }); ================================================ FILE: packages/recoil-relay/__tests__/RecoilRelay_graphQLMutationEffect-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, MockPayloadGenerator, mockRelayEnvironment, testFeedbackMutation, atom, graphQLMutationEffect, componentThatReadsAndWritesAtom, flushPromisesAndTimers; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({atom} = require('Recoil')); ({act} = require('ReactTestUtils')); ({MockPayloadGenerator} = require('relay-test-utils')); ({ componentThatReadsAndWritesAtom, flushPromisesAndTimers, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); mockRelayEnvironment = require('../__test_utils__/RecoilRelay_mockRelayEnvironment'); ({testFeedbackMutation} = require('./mock-graphql/RecoilRelay_MockQueries')); graphQLMutationEffect = require('../RecoilRelay_graphQLMutationEffect'); }); // Test that mutating an atom will commit a mutation operation testRecoil('Atom Mutation', async () => { const {environment, renderElements} = mockRelayEnvironment(); const myAtom = atom({ key: 'graphql atom mutation', default: 'DEFAULT', effects: [ graphQLMutationEffect({ environment, mutation: testFeedbackMutation, // $FlowFixMe[missing-local-annot] variables: actor_id => ({data: {feedback_id: 'ID', actor_id}}), }), ], }); const [ReadAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('"DEFAULT"'); act(() => setAtom('SET')); expect(c.textContent).toBe('"SET"'); expect( environment.mock.getMostRecentOperation().request.variables.data, ).toEqual({feedback_id: 'ID', actor_id: 'SET'}); // Mutation error reverts atom to previous value. act(() => environment.mock.rejectMostRecentOperation(() => new Error('ERROR')), ); expect(c.textContent).toBe('"DEFAULT"'); // Rejecting a previous set won't revert the value. act(() => setAtom('SET2')); expect(c.textContent).toBe('"SET2"'); act(() => setAtom('SET3')); expect(c.textContent).toBe('"SET3"'); expect(environment.mock.getAllOperations().length).toBe(2); act(() => environment.mock.reject( environment.mock.getAllOperations()[0], new Error('ERROR2'), ), ); expect(c.textContent).toBe('"SET3"'); // Reset atom act(resetAtom); expect(c.textContent).toBe('"DEFAULT"'); expect( environment.mock.getMostRecentOperation().request.variables.data, ).toEqual({feedback_id: 'ID', actor_id: 'DEFAULT'}); }); testRecoil('Updaters', async () => { const {environment, renderElements} = mockRelayEnvironment(); const updater = jest.fn((store, data) => { expect(data?.feedback_like?.feedback?.id).toEqual('ID'); expect(data?.feedback_like?.liker?.id).toEqual('ACTOR'); const feedback = store.get('ID'); expect(feedback?.getValue('id')).toBe('ID'); const liker = store.get('ACTOR'); expect(liker?.getValue('id')).toBe('ACTOR'); }); const optimisticUpdater = jest.fn((store, data) => { expect(data?.feedback_like?.feedback?.id).toEqual('ID'); expect(data?.feedback_like?.liker?.id).toEqual('OPTIMISTIC_SET'); const feedback = store.get('ID'); expect(feedback?.getValue('id')).toBe('ID'); const liker = store.get('OPTIMISTIC_SET'); expect(liker?.getValue('id')).toBe('OPTIMISTIC_SET'); }); const myAtom = atom({ key: 'graphql atom mutation updater', default: 'DEFAULT', effects: [ graphQLMutationEffect({ environment, mutation: testFeedbackMutation, // $FlowFixMe[missing-local-annot] variables: actor_id => ({data: {feedback_id: 'ID', actor_id}}), updater_UNSTABLE: updater, optimisticUpdater_UNSTABLE: optimisticUpdater, // $FlowFixMe[missing-local-annot] optimisticResponse_UNSTABLE: actor_id => ({ feedback_like: { feedback: {id: 'ID'}, liker: {id: 'OPTIMISTIC_' + actor_id, __typename: 'Actor'}, }, }), }), ], }); const [ReadAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('"DEFAULT"'); act(() => setAtom('SET')); expect(c.textContent).toBe('"SET"'); expect( environment.mock.getMostRecentOperation().request.variables.data, ).toEqual({feedback_id: 'ID', actor_id: 'SET'}); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { Feedback: () => ({id: 'ID'}), Actor: () => ({id: 'ACTOR'}), }), ), ); expect(c.textContent).toBe('"SET"'); // Errors in updaters will revert value expect(optimisticUpdater).toHaveBeenCalledTimes(1); expect(updater).toHaveBeenCalledTimes(1); }); testRecoil('Aborted mutation', async () => { const {environment, renderElements} = mockRelayEnvironment(); const myAtom = atom({ key: 'graphql atom mutation abort', default: 'DEFAULT', effects: [ // $FlowFixMe[underconstrained-implicit-instantiation] graphQLMutationEffect({ environment, mutation: testFeedbackMutation, variables: () => null, }), ], }); const [ReadAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('"DEFAULT"'); act(() => setAtom('SET')); expect(c.textContent).toBe('"SET"'); expect(environment.mock.getAllOperations().length).toBe(0); }); ================================================ FILE: packages/recoil-relay/__tests__/RecoilRelay_graphQLQueryEffect-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, mockRelayEnvironment, testFeedbackQuery, commitLocalUpdate, MockPayloadGenerator, useState, atom, atomFamily, graphQLQueryEffect, ReadsAtom, flushPromisesAndTimers; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); ({atom, atomFamily} = require('Recoil')); ({commitLocalUpdate} = require('react-relay')); ({act} = require('ReactTestUtils')); ({ ReadsAtom, flushPromisesAndTimers, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); mockRelayEnvironment = require('../__test_utils__/RecoilRelay_mockRelayEnvironment'); ({testFeedbackQuery} = require('./mock-graphql/RecoilRelay_MockQueries')); ({MockPayloadGenerator} = require('relay-test-utils')); graphQLQueryEffect = require('../RecoilRelay_graphQLQueryEffect'); }); testRecoil('Relay Query with ', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql query', default: {feedback: null}, // $FlowFixMe[missing-local-annot] effects: variables => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables, // $FlowFixMe[incompatible-variance] // $FlowFixMe[incompatible-call] mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":123}}'); }); testRecoil('Relay Query with Snapshot', async () => { const {environment, mockEnvironmentKey, snapshot} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql snapshot query', // $FlowFixMe[missing-local-annot] effects: variables => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables, mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); expect(snapshot.getLoadable(query({id: 'ID'})).state).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); expect(snapshot.getLoadable(query({id: 'ID'})).getValue()).toEqual({ feedback: {id: 'ID', seen_count: 123}, }); }); testRecoil('Relay Query Error with ', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql query error', default: {feedback: null}, // $FlowFixMe[missing-local-annot] effects: variables => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables, // $FlowFixMe[incompatible-variance] // $FlowFixMe[incompatible-call] mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => environment.mock.rejectMostRecentOperation(new Error('ERROR'))); await flushPromisesAndTimers(); expect(c.textContent).toBe('error'); }); testRecoil('Relay Query Error with Snapshot', async () => { const {environment, mockEnvironmentKey, snapshot} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql snapshot query error', // $FlowFixMe[missing-local-annot] effects: variables => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables, mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); expect(snapshot.getLoadable(query({id: 'ID'})).state).toBe('loading'); act(() => environment.mock.rejectMostRecentOperation(new Error('ERROR'))); await flushPromisesAndTimers(); expect(() => snapshot.getLoadable(query({id: 'ID'})).getValue()).toThrow( 'ERROR', ); }); testRecoil('Relay Query that is preloaded', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); // Use two atoms to avoid mocking up Entry Points to test preloading. // The first atom will consume the queued network resolver in the mock environment // The second atom will load from the Relay store cache as a pre-load would. const queryA = atom({ key: 'graphql query preloaded 1', effects: [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables: {id: 'ID'}, // $FlowFixMe[incompatible-call] mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); const queryB = atom({ key: 'graphql query preloaded 2', effects: [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables: {id: 'ID'}, // $FlowFixMe[incompatible-call] mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); // This third atom will confirm that we can still load the pre-loaded data // after subsequent updates. const queryC = atom({ key: 'graphql query preloaded 3', effects: [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables: {id: 'ID'}, // $FlowFixMe[incompatible-call] mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); environment.mock.queueOperationResolver(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ); environment.mock.queuePendingOperation(testFeedbackQuery, {id: 'ID'}); let enableQueryC; function Component() { const [state, setState] = useState(false); enableQueryC = () => setState(true); return ( <> {state && } ); } const c = renderElements(); // Confirm data is available synchronously with the first render expect(c.textContent).toBe( '{"feedback":{"id":"ID","seen_count":123}}{"feedback":{"id":"ID","seen_count":123}}', ); // Confirm data is still synchronously available after updates. await flushPromisesAndTimers(); act(enableQueryC); expect(c.textContent).toBe( '{"feedback":{"id":"ID","seen_count":123}}{"feedback":{"id":"ID","seen_count":123}}{"feedback":{"id":"ID","seen_count":123}}', ); }); // TODO Test with graphQL that actually contains @defer // TODO Test a "live query" testRecoil('Relay Query Deferred', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql query deferred', default: {mode: {id: 'DEFAULT'}}, // $FlowFixMe[missing-local-annot] effects: variables => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables, // $FlowFixMe[prop-missing] mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: false, }), ], }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); const operation = environment.mock.getMostRecentOperation(); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":123}}'); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 456}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":456}}'); act(() => environment.mock.resolve( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 789}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":789}}'); }); testRecoil('mapResponse', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const myAtom = atomFamily({ key: 'graphql mapResponse', effects: id => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables: {id}, mapResponse: ({feedback}) => feedback?.seen_count, }), ], }); const c = renderElements(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123'); }); testRecoil('null variables', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql null variables', effects: id => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables: id != null ? {id} : null, mapResponse: ({feedback}) => feedback?.seen_count, }), ], }); const c = renderElements(); expect(c.textContent).toBe('loading'); expect(environment.mock.getAllOperations().length).toBe(0); }); testRecoil('null variables with default', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql null variables with default', default: 789, effects: id => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables: id != null ? {id} : null, mapResponse: ({feedback}) => feedback?.seen_count, }), ], }); const c = renderElements(); expect(c.textContent).toBe('789'); expect(environment.mock.getAllOperations().length).toBe(0); }); test('Subscribe to local mutations', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql local subscriptions', // $FlowFixMe[missing-local-annot] effects: variables => [ graphQLQueryEffect({ environment: mockEnvironmentKey, query: testFeedbackQuery, variables, mapResponse: data => data, subscribeToLocalMutations_UNSTABLE: true, }), ], }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => { return MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }); }), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":123}}'); act(() => commitLocalUpdate(environment, store => { const feedback = store.get('ID'); expect(feedback?.getValue('seen_count')).toBe(123); feedback?.setValue(456, 'seen_count'); }), ); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":456}}'); }); ================================================ FILE: packages/recoil-relay/__tests__/RecoilRelay_graphQLSelector-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, MockPayloadGenerator, readInlineData, stringAtom, testFeedbackQuery, testFeedbackSubscription, testFeedbackMutation, testFeedbackFragment, testFeedbackFragmentQuery, mockRelayEnvironment, graphQLSelector, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({act} = require('ReactTestUtils')); ({readInlineData} = require('relay-runtime')); ({MockPayloadGenerator} = require('relay-test-utils')); ({ ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, stringAtom, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); mockRelayEnvironment = require('../__test_utils__/RecoilRelay_mockRelayEnvironment'); ({ testFeedbackQuery, testFeedbackMutation, testFeedbackSubscription, testFeedbackFragment, testFeedbackFragmentQuery, } = require('./mock-graphql/RecoilRelay_MockQueries')); graphQLSelector = require('../RecoilRelay_graphQLSelector'); }); // Sanity test for graphQLSelector(), which is just a wrapper. // Most functionality is test as part of graphQLSelectorFamily() testRecoil('Sanity Query', async () => { const {environment, renderElements} = mockRelayEnvironment(); const myAtom = stringAtom(); const query = graphQLSelector({ key: 'graphql derived state', environment, query: testFeedbackQuery, variables: ({get}) => ({id: 'ID-' + get(myAtom)}), mapResponse: ({feedback}, {get, variables}) => { expect(variables).toEqual({id: 'ID-' + get(myAtom)}); return `${feedback?.id ?? ''}:${get(myAtom)}-${ feedback?.seen_count ?? '' }`; }, mutations: { mutation: testFeedbackMutation, // $FlowFixMe[incompatible-call] variables: count => ({ data: {feedback_id: 'ID', actor_id: count?.toString()}, }), }, }); const [Atom, setAtom] = componentThatReadsAndWritesAtom(query); const c = renderElements(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('"ID-DEFAULT:DEFAULT-123"'); act(() => setAtom('SET')); expect(c.textContent).toBe('"SET"'); expect( environment.mock.getMostRecentOperation().request.variables.data, ).toEqual({feedback_id: 'ID', actor_id: 'SET'}); }); testRecoil('Sanity Subscription', async () => { const {environment, renderElements} = mockRelayEnvironment(); const query = graphQLSelector({ key: 'graphql remote subscription', environment, query: testFeedbackSubscription, variables: {input: {feedback_id: 'ID'}}, mapResponse: ({feedback_like_subscribe}) => feedback_like_subscribe?.feedback?.seen_count, }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); const operation = environment.mock.getMostRecentOperation(); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.input.feedback_id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123'); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.input.feedback_id, Feedback: () => ({seen_count: 456}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('456'); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.input.feedback_id, Feedback: () => ({seen_count: 789}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('789'); }); testRecoil('GraphQL Fragments', async () => { const {environment, renderElements} = mockRelayEnvironment(); const query = graphQLSelector({ key: 'graphql fragment query', environment, query: testFeedbackFragmentQuery, variables: {id: 'ID1'}, mapResponse: ({feedback: response}, {variables}) => { expect(variables).toEqual({id: 'ID1'}); const feedback = readInlineData(testFeedbackFragment, response); return feedback?.seen_count; }, }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123'); }); ================================================ FILE: packages/recoil-relay/__tests__/RecoilRelay_graphQLSelectorFamily-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { RecoilRelayMockQueriesFeedbackQuery$data, RecoilRelayMockQueriesFeedbackQuery$variables, } from 'RecoilRelayMockQueriesFeedbackQuery.graphql'; import type { RecoilRelayMockQueriesMutation$data, RecoilRelayMockQueriesMutation$rawResponse, RecoilRelayMockQueriesMutation$variables, } from 'RecoilRelayMockQueriesMutation.graphql'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, MockPayloadGenerator, stringAtom, testFeedbackQuery, testFeedbackMutation, mockRelayEnvironment, graphQLSelectorFamily, ReadsAtom, componentThatReadsAndWritesAtom, useState, useRecoilCallback, flushPromisesAndTimers; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); ({useRecoilCallback} = require('Recoil')); ({act} = require('ReactTestUtils')); ({MockPayloadGenerator} = require('relay-test-utils')); ({ ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, stringAtom, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); mockRelayEnvironment = require('../__test_utils__/RecoilRelay_mockRelayEnvironment'); ({ testFeedbackQuery, testFeedbackMutation, } = require('./mock-graphql/RecoilRelay_MockQueries')); graphQLSelectorFamily = require('../RecoilRelay_graphQLSelectorFamily'); }); testRecoil('Relay Query with ', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, RecoilRelayMockQueriesFeedbackQuery$variables, >({ key: 'graphql query', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: vars => vars, mapResponse: data => data, }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":123}}'); }); testRecoil('Relay Query with Snapshot Preloaded', async () => { const {environment, mockEnvironmentKey, snapshot} = mockRelayEnvironment(); environment.mock.queueOperationResolver(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, RecoilRelayMockQueriesFeedbackQuery$variables, >({ key: 'graphql snapshot query preloaded', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: vars => vars, mapResponse: data => data, }); expect(snapshot.getLoadable(query({id: 'ID'})).getValue()).toEqual({ feedback: {id: 'ID', seen_count: 123}, }); }); testRecoil('Relay Query Error with ', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, RecoilRelayMockQueriesFeedbackQuery$variables, >({ key: 'graphql query error', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: vars => vars, mapResponse: data => data, }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => environment.mock.rejectMostRecentOperation(new Error('ERROR'))); await flushPromisesAndTimers(); expect(c.textContent).toBe('error'); }); testRecoil('Relay Query Error with Snapshot', async () => { const {environment, mockEnvironmentKey, snapshot} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, RecoilRelayMockQueriesFeedbackQuery$variables, >({ key: 'graphql snapshot query error', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: vars => vars, mapResponse: data => data, }); expect(snapshot.getLoadable(query({id: 'ID'})).state).toBe('loading'); act(() => environment.mock.rejectMostRecentOperation(new Error('ERROR'))); await flushPromisesAndTimers(); expect(() => snapshot.getLoadable(query({id: 'ID'})).getValue()).toThrow( 'ERROR', ); }); testRecoil('Relay Query that is already loaded', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, void, >({ key: 'graphql query preloaded', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: {id: 'ID'}, mapResponse: data => data, }); environment.mock.queueOperationResolver(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":123}}'); }); testRecoil('Relay Query Deferred', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, RecoilRelayMockQueriesFeedbackQuery$variables, >({ key: 'graphql query deferred', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: vars => vars, mapResponse: data => data, }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); const operation = environment.mock.getMostRecentOperation(); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":123}}'); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 456}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":456}}'); act(() => environment.mock.resolve( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 789}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('{"feedback":{"id":"ID","seen_count":789}}'); }); testRecoil('mapResponse', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, string, ?number, >({ key: 'graphql mapResponse', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: id => ({id}), mapResponse: (data, {variables}) => id => { expect(variables).toEqual({id}); return data.feedback?.seen_count; }, }); const c = renderElements(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123'); }); testRecoil('Using derived state', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const myAtom = stringAtom(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, string, string, >({ key: 'graphql derived state', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: id => ({get}) => ({id: id + '-' + get(myAtom)}), mapResponse: ({feedback}, {get}) => id => `${id}=${feedback?.id ?? ''}:${get(myAtom)}-${ feedback?.seen_count ?? '' }`, }); const c = renderElements(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('"ID=ID-DEFAULT:DEFAULT-123"'); }); testRecoil('null variables', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, RecoilRelayMockQueriesFeedbackQuery$variables | null, RecoilRelayMockQueriesFeedbackQuery$data, >({ key: 'graphql null variables', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: vars => vars, mapResponse: data => data, }); const c = renderElements(); expect(c.textContent).toBe('loading'); expect(environment.mock.getAllOperations().length).toBe(0); }); testRecoil('null variables with default', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, string, RecoilRelayMockQueriesFeedbackQuery$data | string, >({ key: 'graphql null variables with default', environment: mockEnvironmentKey, query: testFeedbackQuery, default: id => 'DEFAULT-' + id, variables: () => null, mapResponse: data => data, }); const c = renderElements(); expect(c.textContent).toBe('"DEFAULT-ID"'); expect(environment.mock.getAllOperations().length).toBe(0); }); testRecoil('pre-fetch query', async () => { const {environment, renderElements} = mockRelayEnvironment(); const myQuery = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, string, ?number, >({ key: 'graphql prefetch', environment, query: testFeedbackQuery, variables: id => ({id}), mapResponse: data => id => { expect(data.feedback?.id).toBe(id); return data.feedback?.seen_count; }, }); let setID; function Component() { const [id, setIDState] = useState('ID1'); // $FlowFixMe[missing-local-annot] setID = useRecoilCallback(({snapshot}) => async newID => { // Pre-fetch the query // $FlowFixMe[unused-promise] snapshot.getPromise(myQuery(newID)); // Mock the query response environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 456}), }), ); await flushPromisesAndTimers(); // Actually update the state to the new ID setIDState(newID); }); return ( <> {id} -{' '} ); } environment.mock.queueOperationResolver(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ); const c = renderElements(); expect(c.textContent).toBe('ID1 - 123'); // If atom is not prefetched, then the wrapping it will trigger await act(() => setID('ID2')); expect(c.textContent).toBe('ID2 - 456'); }); describe('mutations', () => { testRecoil('Local cache', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, string, ?number, >({ key: 'graphql query local cache', environment: mockEnvironmentKey, query: testFeedbackQuery, variables: id => ({id}), mapResponse: data => data.feedback?.seen_count, }); const [Atom, setAtom] = componentThatReadsAndWritesAtom(query('ID')); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123'); // Test that atom can be set as a local cache act(() => setAtom(456)); expect(c.textContent).toBe('456'); expect(environment.mock.getAllOperations().length).toBe(0); }); testRecoil('Write-through cache', async () => { const {environment, mockEnvironmentKey, renderElements} = mockRelayEnvironment(); const query = graphQLSelectorFamily< RecoilRelayMockQueriesFeedbackQuery$variables, RecoilRelayMockQueriesFeedbackQuery$data, string, ?string | number, _, RecoilRelayMockQueriesMutation$variables, RecoilRelayMockQueriesMutation$data, RecoilRelayMockQueriesMutation$rawResponse, >({ key: 'graphql query write-through cache', environment: mockEnvironmentKey, query: testFeedbackQuery, default: 'DEFAULT', variables: id => ({id}), mapResponse: data => data.feedback?.seen_count, mutations: { mutation: testFeedbackMutation, variables: count => id => ({ data: {feedback_id: id, actor_id: count?.toString()}, }), }, }); const [Atom, setAtom, resetAtom] = componentThatReadsAndWritesAtom( query('ID'), ); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); act(() => environment.mock.resolveMostRecentOperation(operation => MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123'); act(() => setAtom('SET')); expect(c.textContent).toBe('"SET"'); expect( environment.mock.getMostRecentOperation().request.variables.data, ).toEqual({feedback_id: 'ID', actor_id: 'SET'}); // Mutation error reverts atom to previous value. act(() => environment.mock.rejectMostRecentOperation(() => new Error('ERROR')), ); expect(c.textContent).toBe('123'); // Rejecting a previous set won't revert the value. act(() => setAtom('SET2')); expect(c.textContent).toBe('"SET2"'); act(() => setAtom('SET3')); expect(c.textContent).toBe('"SET3"'); expect(environment.mock.getAllOperations().length).toBe(2); act(() => environment.mock.reject( environment.mock.getAllOperations()[0], new Error('ERROR2'), ), ); expect(c.textContent).toBe('"SET3"'); // Reset atom act(resetAtom); expect(c.textContent).toBe('"DEFAULT"'); expect( environment.mock.getMostRecentOperation().request.variables.data, ).toEqual({feedback_id: 'ID', actor_id: 'DEFAULT'}); }); }); ================================================ FILE: packages/recoil-relay/__tests__/RecoilRelay_graphQLSubscriptionEffect-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, act, testFeedbackSubscription, mockRelayEnvironment, MockPayloadGenerator, atomFamily, graphQLSubscriptionEffect, ReadsAtom, flushPromisesAndTimers; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({atomFamily} = require('Recoil')); ({act} = require('ReactTestUtils')); ({ ReadsAtom, flushPromisesAndTimers, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({MockPayloadGenerator} = require('relay-test-utils')); mockRelayEnvironment = require('../__test_utils__/RecoilRelay_mockRelayEnvironment'); ({ testFeedbackSubscription, } = require('./mock-graphql/RecoilRelay_MockQueries')); graphQLSubscriptionEffect = require('../RecoilRelay_graphQLSubscriptionEffect'); }); testRecoil('GraphQL Subscription', async () => { const {environment, renderElements} = mockRelayEnvironment(); const query = atomFamily({ key: 'graphql remote subscription', // $FlowFixMe[missing-local-annot] effects: ({id}) => [ graphQLSubscriptionEffect({ environment, subscription: testFeedbackSubscription, variables: {input: {feedback_id: id}}, mapResponse: ({feedback_like_subscribe}) => feedback_like_subscribe?.feedback?.seen_count, }), ], }); const c = renderElements(); await flushPromisesAndTimers(); expect(c.textContent).toBe('loading'); const operation = environment.mock.getMostRecentOperation(); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.input.feedback_id, Feedback: () => ({seen_count: 123}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('123'); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.input.feedback_id, Feedback: () => ({seen_count: 456}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('456'); act(() => environment.mock.nextValue( operation, MockPayloadGenerator.generate(operation, { ID: () => operation.request.variables.input.feedback_id, Feedback: () => ({seen_count: 789}), }), ), ); await flushPromisesAndTimers(); expect(c.textContent).toBe('789'); }); ================================================ FILE: packages/recoil-relay/__tests__/mock-graphql/RecoilRelay_MockQueries.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {graphql} = require('relay-runtime'); // eslint-disable-next-line fb-www/relay-no-coarse-eslint-disable /* eslint-disable relay/unused-fields */ const testFeedbackQuery = graphql` query RecoilRelayMockQueriesFeedbackQuery($id: ID!) # @fb_owner(oncall: "recoil") @relay_test_operation { feedback(id: $id) { id seen_count } } `; const testFeedbackSubscription = graphql` subscription RecoilRelayMockQueriesFeedbackSubscription( $input: FeedbackLikeSubscribeData! ) # @fb_owner(oncall: "recoil") @relay_test_operation { feedback_like_subscribe(data: $input) { ... on FeedbackLikeResponsePayload { feedback { id seen_count } } } } `; // TODO Find a better mutation example // eslint-disable-next-line fb-www/relay-mutation-input-name const testFeedbackMutation = graphql` mutation RecoilRelayMockQueriesMutation($data: FeedbackLikeData!) # @fb_owner(oncall: "recoil") @relay_test_operation @raw_response_type { feedback_like(data: $data) { feedback { id } liker { id } } } `; const testFeedbackFragment = graphql` fragment RecoilRelayMockQueriesFeedbackFragment on Feedback @inline { id seen_count } `; const testFeedbackFragmentQuery = graphql` query RecoilRelayMockQueriesFeedbackFragmentQuery($id: ID!) # @fb_owner(oncall: "recoil") @relay_test_operation { feedback(id: $id) { ...RecoilRelayMockQueriesFeedbackFragment } } `; /* eslint-enable relay/unused-fields */ module.exports = { testFeedbackQuery, testFeedbackSubscription, testFeedbackMutation, testFeedbackFragment, testFeedbackFragmentQuery, }; ================================================ FILE: packages/recoil-relay/__tests__/mock-graphql/schema.graphql ================================================ # GraphQL Schema used for OSS unit tests schema { query: Query mutation: Mutation subscription: Subscription } type Query { feedback(id: ID!): Feedback } type Mutation { feedback_like(data: FeedbackLikeData!): FeedbackLikeResponsePayload } type Subscription { feedback_like_subscribe(data: FeedbackLikeSubscribeData!): FeedbackLikeSubscribeResponsePayload } type Feedback { id: ID! seen_count: Int } type Actor { id: ID! } input FeedbackLikeData { feedback_id: ID! actor_id: ID! } type FeedbackLikeResponsePayload { feedback: Feedback liker: Actor } input FeedbackLikeSubscribeData { feedback_id: ID! } union FeedbackLikeSubscribeResponsePayload = FeedbackLikeResponsePayload type FeedbackLikeResponsePayload { feedback: Feedback liker: Actor } ================================================ FILE: packages/recoil-relay/package-for-release.json ================================================ { "name": "recoil-relay", "version": "0.1.0", "description": "recoil-relay helps Recoil perform type safe and efficient queries using GraphQL with Relay", "main": "cjs/index.js", "module": "es/index.js", "unpkg": "umd/index.js", "types": "index.d.ts", "files": ["umd", "es", "cjs", "index.d.ts"], "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT", "dependencies": {}, "peerDependencies": { "recoil": ">=0.7.3", "react-relay": ">=13.2.0", "relay-runtime": ">=13.2.0" } } ================================================ FILE: packages/recoil-relay/package.json ================================================ { "name": "recoil-relay", "description": "This is the internal package.json enabling CommonJS module", "main": "RecoilRelay_index.js", "haste_commonjs": true, "files": [ "RecoilRelay_index.js" ], "directories": { "": "./" }, "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT" } ================================================ FILE: packages/recoil-sync/RecoilSync.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {AtomEffect, Loadable, RecoilState, StoreID} from 'Recoil'; import type {RecoilValueInfo} from 'Recoil_FunctionalCore'; import type {RecoilValue} from 'Recoil_RecoilValue'; import type {Checker} from 'refine'; const { DefaultValue, RecoilLoadable, useRecoilSnapshot, useRecoilStoreID, useRecoilTransaction_UNSTABLE, } = require('Recoil'); const React = require('react'); const {useCallback, useEffect, useRef} = require('react'); const err = require('recoil-shared/util/Recoil_err'); const lazyProxy = require('recoil-shared/util/Recoil_lazyProxy'); type NodeKey = string; export type ItemKey = string; export type StoreKey = string | void; type EffectKey = number; // $FlowIssue[unclear-type] export type ItemDiff = Map; export type ItemSnapshot = Map; export type WriteInterface = { diff: ItemDiff, allItems: ItemSnapshot, }; export type WriteItem = (ItemKey, DefaultValue | T) => void; export type WriteItems = WriteInterface => void; export type ResetItem = ItemKey => void; export type ReadItem = ItemKey => | DefaultValue | Promise | Loadable | mixed; export type UpdateItem = (ItemKey, DefaultValue | T) => void; export type UpdateItems = ItemSnapshot => void; export type UpdateAllKnownItems = ItemSnapshot => void; export type ListenInterface = { updateItem: UpdateItem, updateItems: UpdateItems, updateAllKnownItems: UpdateAllKnownItems, }; export type ListenToItems = ListenInterface => void | (() => void); type ActionOnFailure = 'errorState' | 'defaultValue'; const DEFAULT_VALUE = new DefaultValue(); function setIntersectsMap(a: Set, b: Map): boolean { if (a.size <= b.size) { for (const x of a) { if (b.has(x)) { return true; } } } else { for (const x of b.keys()) { if (a.has(x)) { return true; } } } return false; } type AtomSyncOptions = { ...SyncEffectOptions, // Mark some items as required itemKey: ItemKey, read: ReadAtom, write: WriteAtom, }; type EffectRegistration = { options: AtomSyncOptions, subscribedItemKeys: Set, }; type AtomRegistration = { atom: RecoilState, effects: Map>, // In-flight updates to avoid feedback loops pendingUpdate?: {value: mixed | DefaultValue}, }; type Storage = { write?: WriteItems, read?: ReadItem, }; class Registries { atomRegistries: Map< StoreID, Map< StoreKey, Map>, // flowlint-line unclear-type:off >, > = new Map(); nextEffectKey: EffectKey = 0; getAtomRegistry( recoilStoreID: StoreID, externalStoreKey: StoreKey, // flowlint-next-line unclear-type:off ): Map> { if (!this.atomRegistries.has(recoilStoreID)) { this.atomRegistries.set(recoilStoreID, new Map()); } const storeRegistries = this.atomRegistries.get(recoilStoreID); const registry = storeRegistries?.get(externalStoreKey); if (registry != null) { return registry; } const newRegistry = new Map>(); storeRegistries?.set(externalStoreKey, newRegistry); return newRegistry; } setAtomEffect( recoilStoreID: StoreID, externalStoreKey: StoreKey, node: RecoilState, options: AtomSyncOptions, ): {effectRegistration: EffectRegistration, unregisterEffect: () => void} { const atomRegistry = this.getAtomRegistry(recoilStoreID, externalStoreKey); if (!atomRegistry.has(node.key)) { atomRegistry.set(node.key, {atom: node, effects: new Map()}); } const effectKey = this.nextEffectKey++; const effectRegistration = { options, subscribedItemKeys: new Set([options.itemKey]), }; atomRegistry.get(node.key)?.effects.set(effectKey, effectRegistration); return { effectRegistration, unregisterEffect: () => void atomRegistry.get(node.key)?.effects.delete(effectKey), }; } storageRegistries: Map> = new Map(); getStorage(recoilStoreID: StoreID, externalStoreKey: StoreKey): ?Storage { return this.storageRegistries.get(recoilStoreID)?.get(externalStoreKey); } setStorage( recoilStoreID: StoreID, externalStoreKey: StoreKey, storage: Storage, ): () => void { if (!this.storageRegistries.has(recoilStoreID)) { this.storageRegistries.set(recoilStoreID, new Map()); } this.storageRegistries.get(recoilStoreID)?.set(externalStoreKey, storage); return () => void this.storageRegistries.get(recoilStoreID)?.delete(externalStoreKey); } } const registries: Registries = new Registries(); function validateLoadable( input: | DefaultValue | Promise | Loadable | mixed, {refine, actionOnFailure_UNSTABLE}: AtomSyncOptions, ): Loadable { return RecoilLoadable.of(input).map(x => { if (x instanceof DefaultValue) { return x; } const result = refine(x); if (result.type === 'success') { return result.value; } if (actionOnFailure_UNSTABLE === 'defaultValue') { return new DefaultValue(); } throw err(`[${result.path.toString()}]: ${result.message}`); }); } function readAtomItems( effectRegistration: EffectRegistration, readFromStorage?: ReadItem, diff?: ItemDiff, ): ?Loadable { const {options} = effectRegistration; const readFromStorageRequired = readFromStorage ?? (itemKey => RecoilLoadable.error( `Read functionality not provided for ${ options.storeKey != null ? `"${options.storeKey}" ` : '' }store in useRecoilSync() hook while updating item "${itemKey}".`, )); effectRegistration.subscribedItemKeys = new Set(); const read: ReadItem = itemKey => { effectRegistration.subscribedItemKeys.add(itemKey); const value = diff?.has(itemKey) ? diff?.get(itemKey) : readFromStorageRequired(itemKey); if (RecoilLoadable.isLoadable(value)) { const loadable: Loadable = value; if (loadable.state === 'hasError') { throw loadable.contents; } } return value; }; let value; try { value = options.read({read}); } catch (error) { return RecoilLoadable.error(error); } return value instanceof DefaultValue ? null : validateLoadable(value, options); } function writeAtomItemsToDiff( diff: ItemDiff, options: AtomSyncOptions, readFromStorage?: ReadItem, loadable: ?Loadable, ): ItemDiff { if (loadable != null && loadable?.state !== 'hasValue') { return diff; } const readFromStorageRequired = readFromStorage ?? (_ => { throw err( `Read functionality not provided for ${ options.storeKey != null ? `"${options.storeKey}" ` : '' }store in useRecoilSync() hook while writing item "${ options.itemKey }".`, ); }); const read = (itemKey: ItemKey) => diff.has(itemKey) ? diff.get(itemKey) : readFromStorageRequired(itemKey); // $FlowFixMe[missing-local-annot] const write = (k: ItemKey, l: DefaultValue | S) => void diff.set(k, l); const reset = (k: ItemKey) => void diff.set(k, DEFAULT_VALUE); options.write( {write, reset, read}, loadable == null ? DEFAULT_VALUE : loadable.contents, ); return diff; } const itemsFromSnapshot = ( recoilStoreID: StoreID, storeKey: StoreKey, getInfo: | ((RecoilValue) => RecoilValueInfo) | ((RecoilValue) => RecoilValueInfo), ): ItemSnapshot => { const items: ItemSnapshot = new Map(); for (const [, {atom, effects}] of registries.getAtomRegistry( recoilStoreID, storeKey, )) { for (const [, {options}] of effects) { const atomInfo = getInfo(atom); writeAtomItemsToDiff( items, options, registries.getStorage(recoilStoreID, storeKey)?.read, atomInfo.isSet || options.syncDefault === true ? atomInfo.loadable : null, ); } } return items; }; function getWriteInterface( recoilStoreID: StoreID, storeKey: StoreKey, diff: ItemDiff, getInfo: | ((RecoilValue) => RecoilValueInfo) | ((RecoilValue) => RecoilValueInfo), ): WriteInterface { // Use a Proxy so we only generate `allItems` if it's actually used. return lazyProxy( {diff}, {allItems: () => itemsFromSnapshot(recoilStoreID, storeKey, getInfo)}, ); } /////////////////////// // useRecoilSync() /////////////////////// export type RecoilSyncOptions = { storeKey?: StoreKey, write?: WriteItems, read?: ReadItem, listen?: ListenToItems, }; function useRecoilSync({ storeKey, write, read, listen, }: RecoilSyncOptions): void { const recoilStoreID = useRecoilStoreID(); // Subscribe to Recoil state changes const snapshot = useRecoilSnapshot(); const previousSnapshotRef = useRef(snapshot); useEffect(() => { if (write != null && snapshot !== previousSnapshotRef.current) { previousSnapshotRef.current = snapshot; const diff: ItemDiff = new Map(); const atomRegistry = registries.getAtomRegistry(recoilStoreID, storeKey); const modifiedAtoms = snapshot.getNodes_UNSTABLE({isModified: true}); for (const atom of modifiedAtoms) { const registration = atomRegistry.get(atom.key); if (registration != null) { const atomInfo = snapshot.getInfo_UNSTABLE(registration.atom); // Avoid feedback loops: // Don't write to storage updates that came from listening to storage if ( (atomInfo.isSet && atomInfo.loadable?.contents !== registration.pendingUpdate?.value) || (!atomInfo.isSet && !(registration.pendingUpdate?.value instanceof DefaultValue)) ) { for (const [, {options}] of registration.effects) { writeAtomItemsToDiff( diff, options, read, atomInfo.isSet || options.syncDefault === true ? atomInfo.loadable : null, ); } } delete registration.pendingUpdate; } } if (diff.size) { write( getWriteInterface( recoilStoreID, storeKey, diff, snapshot.getInfo_UNSTABLE, ), ); } } }, [read, recoilStoreID, snapshot, storeKey, write]); const updateItems = useRecoilTransaction_UNSTABLE( ({set, reset}) => (diff: ItemDiff) => { const atomRegistry = registries.getAtomRegistry( recoilStoreID, storeKey, ); // TODO iterating over all atoms registered with the store could be // optimized if we maintain a reverse look-up map of subscriptions. for (const [, atomRegistration] of atomRegistry) { // Iterate through the effects for this storage in reverse order as // the last effect takes priority. for (const [, effectRegistration] of Array.from( atomRegistration.effects, ).reverse()) { const {options, subscribedItemKeys} = effectRegistration; // Only consider updating this atom if it subscribes to any items // specified in the diff. if (setIntersectsMap(subscribedItemKeys, diff)) { const loadable = readAtomItems(effectRegistration, read, diff); if (loadable != null) { switch (loadable.state) { case 'hasValue': if (loadable.contents instanceof DefaultValue) { atomRegistration.pendingUpdate = {value: DEFAULT_VALUE}; reset(atomRegistration.atom); } else { atomRegistration.pendingUpdate = { value: loadable.contents, }; set(atomRegistration.atom, loadable.contents); } break; case 'hasError': if (options.actionOnFailure_UNSTABLE === 'errorState') { // TODO Async atom support to allow setting atom to error state // in the meantime we can just reset it to default value... atomRegistration.pendingUpdate = {value: DEFAULT_VALUE}; reset(atomRegistration.atom); } break; case 'loading': // TODO Async atom support throw err( 'Recoil does not yet support setting atoms to an asynchronous state', ); } // If this effect set the atom, don't bother with lower-priority // effects. But, if the item didn't have a value then reset // below but ontinue falling back on other effects for the same // storage. This can happen if multiple effects are used to // migrate to a new itemKey and we want to read from the // older key as a fallback. break; } else { atomRegistration.pendingUpdate = {value: DEFAULT_VALUE}; reset(atomRegistration.atom); } } } } }, [recoilStoreID, storeKey, read], ); const updateItem = useCallback( (itemKey: ItemKey, newValue: DefaultValue | T) => { updateItems(new Map([[itemKey, newValue]])); }, [updateItems], ); const updateAllKnownItems = useCallback( (itemSnapshot: ItemSnapshot) => { // Reset the value of any items that are registered and not included in // the user-provided snapshot. const atomRegistry = registries.getAtomRegistry(recoilStoreID, storeKey); for (const [, registration] of atomRegistry) { for (const [, {subscribedItemKeys}] of registration.effects) { for (const itemKey of subscribedItemKeys) { if (!itemSnapshot.has(itemKey)) { itemSnapshot.set(itemKey, DEFAULT_VALUE); } } } } updateItems(itemSnapshot); }, [recoilStoreID, storeKey, updateItems], ); useEffect( () => // TODO try/catch errors and set atom to error state if actionOnFailure is errorState listen?.({updateItem, updateItems, updateAllKnownItems}), [updateItem, updateItems, updateAllKnownItems, listen], ); // Register Storage // Save before effects so that we can initialize atoms for initial render registries.setStorage(recoilStoreID, storeKey, {write, read}); useEffect( () => registries.setStorage(recoilStoreID, storeKey, {write, read}), [recoilStoreID, storeKey, read, write], ); } function RecoilSync({ children, ...props }: { ...RecoilSyncOptions, children: React.Node, }): React.Node { useRecoilSync(props); return children; } /////////////////////// // syncEffect() /////////////////////// export type ReadAtomInterface = {read: ReadItem}; export type ReadAtom = ReadAtomInterface => | DefaultValue | Promise | Loadable | mixed; export type WriteAtomInterface = { write: WriteItem, reset: ResetItem, read: ReadItem, }; export type WriteAtom = (WriteAtomInterface, DefaultValue | T) => void; export type SyncEffectOptions = { storeKey?: StoreKey, itemKey?: ItemKey, refine: Checker, read?: ReadAtom, write?: WriteAtom, // Sync actual default value instead of empty when atom is in default state syncDefault?: boolean, // If there is a failure reading or refining the value, should the atom // silently use the default value or be put in an error state actionOnFailure_UNSTABLE?: ActionOnFailure, }; function syncEffect(opt: SyncEffectOptions): AtomEffect { return ({node, trigger, storeID, setSelf, getLoadable, getInfo_UNSTABLE}) => { // Get options with defaults const itemKey = opt.itemKey ?? node.key; const options: AtomSyncOptions = { itemKey, read: ({read}) => read(itemKey), write: ({write}, loadable) => write(itemKey, loadable), syncDefault: false, actionOnFailure_UNSTABLE: 'errorState', ...opt, }; const {storeKey} = options; const storage = registries.getStorage(storeID, storeKey); // Register Atom const {effectRegistration, unregisterEffect} = registries.setAtomEffect( storeID, storeKey, node, options, ); if (trigger === 'get') { // Initialize Atom value const readFromStorage = storage?.read; if (readFromStorage != null) { try { const loadable = readAtomItems(effectRegistration, readFromStorage); if (loadable != null) { switch (loadable.state) { case 'hasValue': if (!(loadable.contents instanceof DefaultValue)) { setSelf(loadable.contents); } break; case 'hasError': if (options.actionOnFailure_UNSTABLE === 'errorState') { throw loadable.contents; } break; case 'loading': setSelf(loadable.toPromise()); break; } } } catch (error) { if (options.actionOnFailure_UNSTABLE === 'errorState') { throw error; } } } // Persist on Initial Read const writeToStorage = storage?.write; if (options.syncDefault === true && writeToStorage != null) { window.setTimeout(() => { const loadable = getLoadable(node); if (loadable.state === 'hasValue') { const diff = writeAtomItemsToDiff( new Map(), options, storage?.read, loadable, ); writeToStorage( getWriteInterface(storeID, storeKey, diff, getInfo_UNSTABLE), ); } }, 0); } } // Cleanup atom effect registration return unregisterEffect; }; } module.exports = { useRecoilSync, RecoilSync, syncEffect, registries_FOR_TESTING: registries, }; ================================================ FILE: packages/recoil-sync/RecoilSync_URL.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { ItemKey, ItemSnapshot, ReadItem, StoreKey, SyncEffectOptions, } from './RecoilSync'; import type {AtomEffect} from 'Recoil'; import type {ListenInterface, WriteInterface} from 'RecoilSync'; import type {CheckerReturnType} from 'refine'; const {DefaultValue, RecoilLoadable} = require('Recoil'); const {syncEffect, useRecoilSync} = require('./RecoilSync'); const React = require('react'); const {useCallback, useEffect, useMemo, useRef} = require('react'); const err = require('recoil-shared/util/Recoil_err'); const {assertion, mixed, writableDict} = require('refine'); type NodeKey = string; // $FlowFixMe[reference-before-declaration] // $FlowFixMe[underconstrained-implicit-instantiation] type ItemState = CheckerReturnType; type AtomRegistration = { history: HistoryOption, itemKeys: Set, }; const registries: Map> = new Map(); const itemStateChecker = writableDict(mixed()); const refineState = assertion(itemStateChecker); const wrapState = (x: mixed): ItemSnapshot => { return new Map(Array.from(Object.entries(refineState(x)))); }; const unwrapState = (state: ItemSnapshot): ItemState => Object.fromEntries( Array.from(state.entries()) // Only serialize atoms in a non-default value state. .filter(([, value]) => !(value instanceof DefaultValue)), ); function parseURL( href: string, loc: LocationOption, deserialize: string => mixed, ): ?ItemSnapshot { const url = new URL(href); switch (loc.part) { case 'href': return wrapState(deserialize(`${url.pathname}${url.search}${url.hash}`)); case 'hash': return url.hash ? wrapState(deserialize(decodeURIComponent(url.hash.substring(1)))) : null; case 'search': return url.search ? wrapState(deserialize(decodeURIComponent(url.search.substring(1)))) : null; case 'queryParams': { const searchParams = new URLSearchParams(url.search); const {param} = loc; if (param != null) { const stateStr = searchParams.get(param); return stateStr != null ? wrapState(deserialize(stateStr)) : new Map(); } return new Map( Array.from(searchParams.entries()).map(([key, value]) => { try { return [key, deserialize(value)]; } catch (error) { return [key, RecoilLoadable.error(error)]; } }), ); } } throw err(`Unknown URL location part: "${loc.part}"`); } function encodeURL( href: string, loc: LocationOption, items: ItemSnapshot, serialize: mixed => string, ): string { const url = new URL(href); switch (loc.part) { case 'href': return serialize(unwrapState(items)); case 'hash': url.hash = encodeURIComponent(serialize(unwrapState(items))); break; case 'search': url.search = encodeURIComponent(serialize(unwrapState(items))); break; case 'queryParams': { const {param} = loc; const searchParams = new URLSearchParams(url.search); if (param != null) { searchParams.set(param, serialize(unwrapState(items))); } else { for (const [itemKey, value] of items.entries()) { value instanceof DefaultValue ? searchParams.delete(itemKey) : searchParams.set(itemKey, serialize(value)); } } url.search = searchParams.toString(); break; } default: throw err(`Unknown URL location part: "${loc.part}"`); } return url.href; } /////////////////////// // useRecoilURLSync() /////////////////////// export type LocationOption = | {part: 'href'} | {part: 'hash'} | {part: 'search'} | {part: 'queryParams', param?: string}; export type BrowserInterface = { replaceURL?: string => void, pushURL?: string => void, getURL?: () => string, listenChangeURL?: (handler: () => void) => () => void, }; export type RecoilURLSyncOptions = { children: React.Node, storeKey?: StoreKey, location: LocationOption, serialize: mixed => string, deserialize: string => mixed, browserInterface?: BrowserInterface, }; const DEFAULT_BROWSER_INTERFACE = { replaceURL: (url: string) => history.replaceState(null, '', url), pushURL: (url: string) => history.pushState(null, '', url), getURL: () => window.document.location, listenChangeURL: (handleUpdate: () => void) => { window.addEventListener('popstate', handleUpdate); return () => window.removeEventListener('popstate', handleUpdate); }, }; function RecoilURLSync({ storeKey, location: loc, serialize, deserialize, browserInterface, children, }: RecoilURLSyncOptions): React.Node { const {getURL, replaceURL, pushURL, listenChangeURL} = { ...DEFAULT_BROWSER_INTERFACE, ...(browserInterface ?? {}), }; // Parse and cache the current state from the URL // Update cached URL parsing if properties of location prop change, but not // based on just the object reference itself. const memoizedLoc = useMemo( () => loc, // Complications with disjoint uniont // $FlowIssue[prop-missing] [loc.part, loc.queryParam], // eslint-disable-line fb-www/react-hooks-deps ); const updateCachedState: () => void = useCallback(() => { cachedState.current = parseURL(getURL(), memoizedLoc, deserialize); }, [getURL, memoizedLoc, deserialize]); const cachedState = useRef(null); // Avoid executing updateCachedState() on each render const firstRender = useRef(true); firstRender.current && updateCachedState(); firstRender.current = false; useEffect(updateCachedState, [updateCachedState]); const write = useCallback( ({diff, allItems}: WriteInterface) => { updateCachedState(); // Just to be safe... // This could be optimized with an itemKey-based registery if necessary to avoid // atom traversal. const atomRegistry = registries.get(storeKey); const itemsToPush = atomRegistry != null ? new Set( Array.from(atomRegistry) .filter( ([, {history, itemKeys}]) => history === 'push' && Array.from(itemKeys).some(key => diff.has(key)), ) .map(([, {itemKeys}]) => itemKeys) .reduce( // $FlowFixMe[missing-local-annot] (itemKeys, keys) => itemKeys.concat(Array.from(keys)), [], ), ) : null; if (itemsToPush?.size && cachedState.current != null) { const replaceItems: ItemSnapshot = cachedState.current; // First, repalce the URL with any atoms that replace the URL history for (const [key, value] of allItems) { if (!itemsToPush.has(key)) { replaceItems.set(key, value); } } replaceURL(encodeURL(getURL(), loc, replaceItems, serialize)); // Next, push the URL with any atoms that caused a new URL history entry pushURL(encodeURL(getURL(), loc, allItems, serialize)); } else { // Just replace the URL with the new state replaceURL(encodeURL(getURL(), loc, allItems, serialize)); } cachedState.current = allItems; }, [getURL, loc, pushURL, replaceURL, serialize, storeKey, updateCachedState], ); const read: ReadItem = useCallback((itemKey: ItemKey) => { return cachedState.current?.has(itemKey) ? cachedState.current?.get(itemKey) : new DefaultValue(); }, []); const listen = useCallback( ({updateAllKnownItems}: ListenInterface) => { function handleUpdate() { updateCachedState(); if (cachedState.current != null) { updateAllKnownItems(cachedState.current); } } return listenChangeURL(handleUpdate); }, [listenChangeURL, updateCachedState], ); useRecoilSync({storeKey, read, write, listen}); return children; } /////////////////////// // urlSyncEffect() /////////////////////// type HistoryOption = 'push' | 'replace'; function urlSyncEffect({ history = 'replace', ...options }: { ...SyncEffectOptions, history?: HistoryOption, }): AtomEffect { const atomEffect = syncEffect(options); return effectArgs => { // Register URL sync options if (!registries.has(options.storeKey)) { registries.set(options.storeKey, new Map()); } const atomRegistry = registries.get(options.storeKey); if (atomRegistry == null) { throw err('Error with atom registration'); } atomRegistry.set(effectArgs.node.key, { history, itemKeys: new Set([options.itemKey ?? effectArgs.node.key]), }); // Wrap syncEffect() atom effect const cleanup = atomEffect(effectArgs); // Cleanup atom option registration return () => { atomRegistry.delete(effectArgs.node.key); cleanup?.(); }; }; } module.exports = { RecoilURLSync, urlSyncEffect, }; ================================================ FILE: packages/recoil-sync/RecoilSync_URLJSON.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilURLSyncOptions} from './RecoilSync_URL'; const {RecoilURLSync} = require('./RecoilSync_URL'); const React = require('react'); const {useCallback} = require('react'); const err = require('recoil-shared/util/Recoil_err'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); export type RecoilURLSyncJSONOptions = $Rest< RecoilURLSyncOptions, { serialize: mixed => string, deserialize: string => mixed, }, >; function RecoilURLSyncJSON(options: RecoilURLSyncJSONOptions): React.Node { if (options.location.part === 'href') { throw err('"href" location is not supported for JSON encoding'); } const serialize = useCallback( (x: mixed) => x === undefined ? '' : nullthrows(JSON.stringify(x), 'Unable to serialize state with JSON'), [], ); const deserialize = useCallback((x: string) => JSON.parse(x), []); return ( ); } module.exports = {RecoilURLSyncJSON}; ================================================ FILE: packages/recoil-sync/RecoilSync_URLTransit.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {RecoilURLSyncOptions} from './RecoilSync_URL'; const {DefaultValue} = require('Recoil'); const {RecoilURLSync} = require('./RecoilSync_URL'); const React = require('react'); const {useCallback, useEffect, useMemo} = require('react'); const err = require('recoil-shared/util/Recoil_err'); const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation'); const usePrevious = require('recoil-shared/util/Recoil_usePrevious'); const transit = require('transit-js'); export type TransitHandler = { tag: string, class: Class, write: T => S, read: S => T, }; export type RecoilURLSyncTransitOptions = $Rest< { ...RecoilURLSyncOptions, // $FlowIssue[unclear-type] handlers?: $ReadOnlyArray>, }, { serialize: mixed => string, deserialize: string => mixed, }, >; const BUILTIN_HANDLERS = [ { tag: 'Date', class: Date, /* $FlowFixMe[missing-local-annot] The type annotation(s) required by * Flow's LTI update could not be added via codemod */ write: x => x.toISOString(), read: (str: $FlowFixMe) => new Date(str), }, { tag: 'Set', class: Set, /* $FlowFixMe[missing-local-annot] The type annotation(s) required by * Flow's LTI update could not be added via codemod */ write: x => Array.from(x), read: (arr: $FlowFixMe) => new Set(arr), }, { tag: 'Map', class: Map, /* $FlowFixMe[missing-local-annot] The type annotation(s) required by * Flow's LTI update could not be added via codemod */ write: x => Array.from(x.entries()), read: (arr: $FlowFixMe) => new Map(arr), }, { tag: '__DV', class: DefaultValue, write: () => 0, // number encodes the smallest in URL read: () => new DefaultValue(), }, ]; function RecoilURLSyncTransit({ handlers: handlersProp, ...options }: RecoilURLSyncTransitOptions): React.Node { if (options.location.part === 'href') { throw err('"href" location is not supported for Transit encoding'); } const previousHandlers = usePrevious(handlersProp); useEffect(() => { if (__DEV__) { if (previousHandlers != null && previousHandlers !== handlersProp) { const message = ` 'handlers' prop was detected to be unstable. It is important that this is a stable or memoized array instance. Otherwise you may miss URL changes as the listener is re-subscribed. `; expectationViolation(message); } } }, [previousHandlers, handlersProp]); const handlers = useMemo( () => [...BUILTIN_HANDLERS, ...(handlersProp ?? [])], [handlersProp], ); const writer = useMemo( () => transit.writer('json', { handlers: transit.map( handlers .map(handler => [ handler.class, transit.makeWriteHandler({ tag: () => handler.tag, rep: handler.write, }), ]) .flat(1), ), }), [handlers], ); const serialize = useCallback((x: mixed) => writer.write(x), [writer]); const reader = useMemo( () => transit.reader('json', { handlers: handlers.reduce<{ [string]: ($FlowFixMe) => $FlowFixMe, }>((c, {tag, read}) => { c[tag] = read; return c; }, {}), mapBuilder: { init: () => ({}), add: (ret, key, val) => { ret[key] = val; return ret; }, finalize: ret => ret, }, }), [handlers], ); const deserialize = useCallback((x: string) => reader.read(x), [reader]); return ( ); } module.exports = {RecoilURLSyncTransit}; ================================================ FILE: packages/recoil-sync/RecoilSync_index.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { ItemKey, ListenToItems, ReadAtom, ReadAtomInterface, ReadItem, RecoilSyncOptions, ResetItem, StoreKey, SyncEffectOptions, WriteAtom, WriteAtomInterface, WriteItems, } from './RecoilSync'; import type {RecoilURLSyncOptions} from './RecoilSync_URL'; import type {RecoilURLSyncJSONOptions} from './RecoilSync_URLJSON'; import type { RecoilURLSyncTransitOptions, TransitHandler, } from './RecoilSync_URLTransit'; const {RecoilSync, syncEffect} = require('./RecoilSync'); const {RecoilURLSync, urlSyncEffect} = require('./RecoilSync_URL'); const {RecoilURLSyncJSON} = require('./RecoilSync_URLJSON'); const {RecoilURLSyncTransit} = require('./RecoilSync_URLTransit'); export type { // Keys ItemKey, StoreKey, // Core useRecoilSync() options RecoilSyncOptions, ReadItem, WriteItems, ListenToItems, // Core syncEffect() options SyncEffectOptions, ReadAtomInterface, ReadAtom, WriteAtomInterface, WriteAtom, ResetItem, // URL Synchronization RecoilURLSyncOptions, RecoilURLSyncJSONOptions, RecoilURLSyncTransitOptions, TransitHandler, }; module.exports = { // Core Recoil Sync RecoilSync, syncEffect, // Recoil Sync URL RecoilURLSync, RecoilURLSyncJSON, RecoilURLSyncTransit, urlSyncEffect, }; ================================================ FILE: packages/recoil-sync/__test_utils__/RecoilSync_MockURLSerialization.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {BrowserInterface, LocationOption} from '../RecoilSync_URL'; const {RecoilURLSync} = require('../RecoilSync_URL'); const React = require('react'); const {useCallback} = require('react'); const { flushPromisesAndTimers, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); // //////////////////////////// // // Mock Serialization // //////////////////////////// function TestURLSync({ storeKey, location, browserInterface, children = null, }: { storeKey?: string, location: LocationOption, browserInterface?: BrowserInterface, children?: React.Node, }): React.Node { const serialize = useCallback( (items: mixed) => { const str = nullthrows(JSON.stringify(items)); return location.part === 'href' ? `/TEST#${encodeURIComponent(str)}` : str; }, [location.part], ); const deserialize = useCallback( (str: string) => { const stateStr = location.part === 'href' ? decodeURIComponent(str.split('#')[1]) : str; // Skip the default URL parts which don't conform to the serialized standard. // 'bar' also doesn't conform, but we want to test coexistence of foreign // query parameters. if (stateStr == null || stateStr === 'anchor' || stateStr === 'foo=bar') { return {}; } try { return JSON.parse(stateStr); } catch { // Catch errors for open source CI tests which tend to keep previous tests alive so they are // still subscribed to URL changes from future tests and may get invalid JSON as a result. return {error: 'PARSE ERROR'}; } }, [location.part], ); return ( {children} ); } function encodeState(obj: {...}) { return encodeURIComponent(JSON.stringify(obj)); } function encodeURLPart(href: string, loc: LocationOption, obj: {...}): string { const url = new URL(href); switch (loc.part) { case 'href': url.pathname = '/TEST'; url.hash = encodeState(obj); break; case 'hash': url.hash = encodeState(obj); break; case 'search': { url.search = encodeState(obj); break; } case 'queryParams': { const {param} = loc; const {searchParams} = url; if (param != null) { searchParams.set(param, JSON.stringify(obj)); } else { for (const [key, value] of Object.entries(obj)) { searchParams.set(key, JSON.stringify(value) ?? ''); } } url.search = searchParams.toString(); break; } } return url.href; } function encodeURL( parts: Array<[LocationOption, {...}]>, url: string = window.location.href, ): string { let href = url; for (const [loc, obj] of parts) { href = encodeURLPart(href, loc, obj); } return href; } function expectURL( parts: Array<[LocationOption, {...}]>, url: string = window.location.href, ) { expect(url).toBe(encodeURL(parts, url)); } async function gotoURL(parts: Array<[LocationOption, {...}]>) { history.replaceState(null, '', encodeURL(parts)); history.pushState(null, '', '/POPSTATE'); history.back(); await flushPromisesAndTimers(); } async function goBack() { history.back(); await flushPromisesAndTimers(); } module.exports = { TestURLSync, encodeURL, expectURL, gotoURL, goBack, }; ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {ItemKey, ItemSnapshot, ListenInterface} from '../RecoilSync'; import type {Loadable} from 'Recoil'; const {act} = require('ReactTestUtils'); const { DefaultValue, RecoilLoadable, RecoilRoot, atom, atomFamily, selectorFamily, useRecoilValue, } = require('Recoil'); const { RecoilSync, registries_FOR_TESTING, syncEffect, useRecoilSync, } = require('../RecoilSync'); const React = require('react'); const {useCallback, useState} = require('react'); const { ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const isPromise = require('recoil-shared/util/Recoil_isPromise'); const {asType, dict, literal, match, number, string} = require('refine'); //////////////////////////// // Mock Storage //////////////////////////// function TestRecoilSync({ storeKey, storage, regListen, allItemsRef, children = null, }: { storeKey?: string, storage: Map, regListen?: ListenInterface => void, allItemsRef?: {current: Map}, children?: React.Node, }) { return ( { if (itemKey === 'error') { throw new Error('READ ERROR'); } if (itemKey === 'reject') { return Promise.reject(new Error('READ REJECT')); } return storage.has(itemKey) ? storage.get(itemKey) : new DefaultValue(); }} write={({diff, allItems}) => { for (const [key, value] of diff.entries()) { value instanceof DefaultValue ? storage.delete(key) : storage.set(key, value); } for (const [itemKey, value] of diff) { expect(allItems.get(itemKey)).toEqual(value); } if (allItemsRef != null) { allItemsRef.current = allItems; } }} listen={listenInterface => { regListen?.(listenInterface); }}> {children} ); } /////////////////////// // Tests /////////////////////// test('Write to storage', async () => { const atomA = atom({ key: 'recoil-sync write A', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const atomB = atom({ key: 'recoil-sync write B', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const ignoreAtom = atom({ key: 'recoil-sync write ignore', default: 'DEFAULT', }); const storage = new Map(); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); const [IgnoreAtom, setIgnore] = componentThatReadsAndWritesAtom(ignoreAtom); const container = renderElements( , ); expect(storage.size).toBe(0); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); act(() => setA('A')); act(() => setB('B')); act(() => setIgnore('IGNORE')); expect(container.textContent).toBe('"A""B""IGNORE"'); expect(storage.size).toBe(2); expect(storage.get('recoil-sync write A')).toBe('A'); expect(storage.get('recoil-sync write B')).toBe('B'); act(() => resetA()); act(() => setB('BB')); expect(container.textContent).toBe('"DEFAULT""BB""IGNORE"'); expect(storage.size).toBe(1); expect(storage.has('recoil-sync write A')).toBe(false); expect(storage.get('recoil-sync write B')).toBe('BB'); }); test('Write to multiple storages', async () => { const atomA = atom({ key: 'recoil-sync multiple storage A', default: 'DEFAULT', effects: [syncEffect({storeKey: 'A', refine: string()})], }); const atomB = atom({ key: 'recoil-sync multiple storage B', default: 'DEFAULT', effects: [syncEffect({storeKey: 'B', refine: string()})], }); const storageA = new Map(); const storageB = new Map(); const [AtomA, setA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); renderElements( <> , ); expect(storageA.size).toBe(0); expect(storageB.size).toBe(0); act(() => setA('A')); act(() => setB('B')); expect(storageA.size).toBe(1); expect(storageB.size).toBe(1); expect(storageA.get('recoil-sync multiple storage A')).toBe('A'); expect(storageB.get('recoil-sync multiple storage B')).toBe('B'); }); test('Read from storage', async () => { const atomA = atom({ key: 'recoil-sync read A', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const atomB = atom({ key: 'recoil-sync read B', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const atomC = atom({ key: 'recoil-sync read C', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const storage = new Map([ ['recoil-sync read A', 'A'], ['recoil-sync read B', 'B'], ]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('"A""B""DEFAULT"'); }); test('Read from storage async', async () => { const atomA = atom({ key: 'recoil-sync read async', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const storage = new Map([['recoil-sync read async', Promise.resolve('A')]]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('loading'); await flushPromisesAndTimers(); expect(container.textContent).toBe('"A"'); }); test('Read from storage error', async () => { const atomA = atom({ key: 'recoil-sync read error A', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const atomB = atom({ key: 'recoil-sync read error B', default: 'DEFAULT', effects: [ syncEffect({refine: string(), actionOnFailure_UNSTABLE: 'defaultValue'}), ], }); const atomC = atom({ key: 'recoil-sync read error C', default: 'DEFAULT', // will throw error if the key is "error" effects: [syncEffect({itemKey: 'error', refine: string()})], }); const atomD = atom({ key: 'recoil-sync read error D', default: 'DEFAULT', // will throw error if the key is "error" effects: [ syncEffect({ itemKey: 'error', refine: string(), actionOnFailure_UNSTABLE: 'defaultValue', }), ], }); const atomE = atom({ key: 'recoil-sync read error E', default: 'DEFAULT', effects: [ syncEffect({ refine: string(), }), ], }); const atomF = atom({ key: 'recoil-sync read error F', default: 'DEFAULT', effects: [ syncEffect({ refine: string(), actionOnFailure_UNSTABLE: 'defaultValue', }), ], }); const atomG = atom({ key: 'recoil-sync read error G', default: 'DEFAULTx', effects: [ syncEffect({ itemKey: 'reject', refine: string(), }), ], }); const mySelector = selectorFamily({ key: 'recoil-sync read error selector', get: ( // $FlowFixMe[missing-local-annot] {myAtom}, ) => // $FlowFixMe[missing-local-annot] ({get}) => { try { return get(myAtom); } catch (e) { if (isPromise(e)) { return e.catch(err => err); } return e.message; } }, }); const storage = new Map([ ['recoil-sync read error A', RecoilLoadable.error(new Error('ERROR A'))], ['recoil-sync read error B', RecoilLoadable.error(new Error('ERROR B'))], ['recoil-sync read error E', 999], ['recoil-sync read error F', 999], ]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); await flushPromisesAndTimers(); expect(container.textContent).toBe( '"ERROR A""DEFAULT""READ ERROR""DEFAULT""[]: value is not a string""DEFAULT""READ REJECT"', ); }); test('Read nullable', async () => { // $FlowFixMe[incompatible-call] const atomUndefinedA = atom({ key: 'recoil-sync read undefined A', default: 'DEFAULT', effects: [syncEffect({refine: literal(undefined)})], }); // $FlowFixMe[incompatible-call] const atomUndefinedB = atom({ key: 'recoil-sync read undefined B', default: 'DEFAULT', effects: [syncEffect({refine: literal(undefined)})], }); // $FlowFixMe[incompatible-call] const atomUndefinedC = atom({ key: 'recoil-sync read undefined C', default: 'DEFAULT', effects: [syncEffect({refine: literal(undefined)})], }); // $FlowFixMe[incompatible-call] const atomNullA = atom({ key: 'recoil-sync read null A', default: 'DEFAULT', effects: [syncEffect({refine: literal(null)})], }); // $FlowFixMe[incompatible-call] const atomNullB = atom({ key: 'recoil-sync read null B', default: 'DEFAULT', effects: [syncEffect({refine: literal(null)})], }); // $FlowFixMe[incompatible-call] const atomNullC = atom({ key: 'recoil-sync read null C', default: 'DEFAULT', effects: [syncEffect({refine: literal(null)})], }); const storage = new Map([ ['recoil-sync read undefined A', undefined], ['recoil-sync read undefined B', Promise.resolve(undefined)], ['recoil-sync read undefined C', RecoilLoadable.of(undefined)], ['recoil-sync read null A', null], ['recoil-sync read null B', Promise.resolve(null)], ['recoil-sync read null C', RecoilLoadable.of(null)], ]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('loading'); await flushPromisesAndTimers(); expect(container.textContent).toBe('nullnullnull'); }); test('Abort read', async () => { const atomA = atom({ key: 'recoil-sync abort read A', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const atomB = atom({ key: 'recoil-sync abort read B', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const atomC = atom({ key: 'recoil-sync abort read C', default: 'DEFAULT', effects: [syncEffect({refine: string()})], }); const storage = new Map([ ['recoil-sync abort read A', new DefaultValue()], ['recoil-sync abort read B', Promise.resolve(new DefaultValue())], ['recoil-sync abort read C', RecoilLoadable.of(new DefaultValue())], ]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('loading'); await flushPromisesAndTimers(); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); }); // TODO These semantics are debatable... test('Abort vs reset', async () => { const atomA = atom({ key: 'recoil-sync abort vs reset A', default: 'DEFAULT', effects: [({setSelf}) => setSelf('INIT'), syncEffect({refine: string()})], }); const atomB = atom({ key: 'recoil-sync abort vs reset B', default: 'DEFAULT', effects: [({setSelf}) => setSelf('INIT'), syncEffect({refine: string()})], }); const atomC = atom({ key: 'recoil-sync abort vs reset C', default: 'DEFAULT', effects: [({setSelf}) => setSelf('INIT'), syncEffect({refine: string()})], }); const atomD = atom({ key: 'recoil-sync abort vs reset D', default: 'DEFAULT', effects: [({setSelf}) => setSelf('INIT'), syncEffect({refine: string()})], }); const storage = new Map([ ['recoil-sync abort vs reset A', new DefaultValue()], ['recoil-sync abort vs reset B', RecoilLoadable.of(new DefaultValue())], ['recoil-sync abort vs reset C', Promise.resolve(new DefaultValue())], [ 'recoil-sync abort vs reset D', RecoilLoadable.of(Promise.resolve(new DefaultValue())), ], ]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('loading'); await flushPromisesAndTimers(); expect(container.textContent).toBe('"INIT""INIT""DEFAULT""DEFAULT"'); }); test('Read from storage upgrade - multiple effects', async () => { // Fail validation const atomA = atom({ key: 'recoil-sync fail validation - multi', default: 'DEFAULT', effects: [ // No matching sync effect syncEffect({refine: string(), actionOnFailure_UNSTABLE: 'defaultValue'}), ], }); // Upgrade from number const atomB = atom({ key: 'recoil-sync upgrade number - multi', default: 'DEFAULT', effects: [ // This sync effect is ignored syncEffect({ refine: asType(string(), () => 'IGNORE'), actionOnFailure_UNSTABLE: 'defaultValue', }), syncEffect({ refine: asType(number(), num => `${num}`), actionOnFailure_UNSTABLE: 'defaultValue', }), // This sync effect is ignored syncEffect({ refine: asType(string(), () => 'IGNORE'), actionOnFailure_UNSTABLE: 'defaultValue', }), ], }); // Upgrade from string const atomC = atom({ key: 'recoil-sync upgrade string - multi', default: 0, effects: [ // This sync effect is ignored syncEffect({ refine: asType(number(), () => 999), actionOnFailure_UNSTABLE: 'defaultValue', }), syncEffect({ refine: asType(string(), Number), actionOnFailure_UNSTABLE: 'defaultValue', }), // This sync effect is ignored syncEffect({ refine: asType(number(), () => 999), actionOnFailure_UNSTABLE: 'defaultValue', }), ], }); // Upgrade from async const atomD = atom({ key: 'recoil-sync upgrade async - multi', default: 'DEFAULT', effects: [ syncEffect({ refine: asType(number(), num => `${num}`), actionOnFailure_UNSTABLE: 'defaultValue', }), ], }); const storage = new Map([ ['recoil-sync fail validation - multi', 123], ['recoil-sync upgrade number - multi', 123], ['recoil-sync upgrade string - multi', '123'], ['recoil-sync upgrade async - multi', Promise.resolve(123)], ]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('loading'); await flushPromisesAndTimers(); expect(container.textContent).toBe('"DEFAULT""123"123"123"'); }); test('Read from storage upgrade', async () => { // Fail validation const atomA = atom({ key: 'recoil-sync fail validation', default: 'DEFAULT', effects: [ // No matching sync effect syncEffect({refine: string(), actionOnFailure_UNSTABLE: 'defaultValue'}), ], }); // Upgrade from number const atomB = atom({ key: 'recoil-sync upgrade number', default: 'DEFAULT', effects: [ syncEffect({ refine: match( asType(string(), () => 'IGNORE'), // This rule is ignored asType(number(), num => `${num}`), asType(string(), () => 'IGNORE'), // This rule is ignored ), }), ], }); // Upgrade from string const atomC = atom({ key: 'recoil-sync upgrade string', default: 0, effects: [ syncEffect({ refine: match( asType(number(), () => 999), // This rule is ignored asType(string(), Number), asType(number(), () => 999), // This rule is ignored ), }), ], }); // Upgrade from async const atomD = atom({ key: 'recoil-sync upgrade async', default: 'DEFAULT', effects: [ syncEffect({ refine: match( string(), asType(number(), num => `${num}`), ), }), ], }); const storage = new Map([ ['recoil-sync fail validation', 123], ['recoil-sync upgrade number', 123], ['recoil-sync upgrade string', '123'], ['recoil-sync upgrade async', Promise.resolve(123)], ]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('loading'); await flushPromisesAndTimers(); expect(container.textContent).toBe('"DEFAULT""123"123"123"'); }); test('Read/Write from storage upgrade', async () => { const atomA = atom({ key: 'recoil-sync read/write upgrade type', default: 'DEFAULT', effects: [ syncEffect({ refine: match( string(), asType(number(), num => `${num}`), ), }), ], }); const atomB = atom({ key: 'recoil-sync read/write upgrade key', default: 'DEFAULT', effects: [ syncEffect({itemKey: 'OLD KEY', refine: string()}), syncEffect({itemKey: 'NEW KEY', refine: string()}), ], }); const atomC = atom({ key: 'recoil-sync read/write upgrade storage', default: 'DEFAULT', effects: [ syncEffect({refine: string()}), syncEffect({storeKey: 'OTHER_SYNC', refine: string()}), ], }); const storage1 = new Map([ ['recoil-sync read/write upgrade type', 123], ['OLD KEY', 'OLD'], ['recoil-sync read/write upgrade storage', 'STR1'], ]); const storage2 = new Map([ ['recoil-sync read/write upgrade storage', 'STR2'], ]); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB, resetB] = componentThatReadsAndWritesAtom(atomB); const [AtomC, setC, resetC] = componentThatReadsAndWritesAtom(atomC); const container = renderElements( <> {/* $FlowFixMe[incompatible-type-arg] */} {/* $FlowFixMe[incompatible-type-arg] */} , ); expect(container.textContent).toBe('"123""OLD""STR2"'); expect(storage1.size).toBe(3); act(() => setA('A')); act(() => setB('B')); act(() => setC('C')); expect(container.textContent).toBe('"A""B""C"'); expect(storage1.size).toBe(4); expect(storage1.get('recoil-sync read/write upgrade type')).toBe('A'); expect(storage1.get('OLD KEY')).toBe('B'); expect(storage1.get('NEW KEY')).toBe('B'); expect(storage1.get('recoil-sync read/write upgrade storage')).toBe('C'); expect(storage2.size).toBe(1); expect(storage2.get('recoil-sync read/write upgrade storage')).toBe('C'); act(() => resetA()); act(() => resetB()); act(() => resetC()); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); expect(storage1.size).toBe(0); expect(storage2.size).toBe(0); }); test('Listen to storage', async () => { const atomA = atom({ key: 'recoil-sync listen', default: 'DEFAULT', effects: [syncEffect({storeKey: 'SYNC_1', refine: string()})], }); const atomB = atom({ key: 'recoil-sync listen to multiple keys', default: 'DEFAULT', effects: [ syncEffect({storeKey: 'SYNC_1', itemKey: 'KEY A', refine: string()}), syncEffect({storeKey: 'SYNC_1', itemKey: 'KEY B', refine: string()}), ], }); const atomC = atom({ key: 'recoil-sync listen to multiple storage', default: 'DEFAULT', effects: [ syncEffect({storeKey: 'SYNC_1', refine: string()}), syncEffect({storeKey: 'SYNC_2', refine: string()}), ], }); const storage1 = new Map([ ['recoil-sync listen', 'A'], ['KEY A', 'B'], ['recoil-sync listen to multiple storage', 'C1'], ]); const storage2 = new Map([['recoil-sync listen to multiple storage', 'C2']]); let updateItem1: ( ItemKey, DefaultValue | Loadable | string, ) => void = () => { throw new Error('Failed to register 1'); }; let updateItems1: ItemSnapshot => void = _ => { throw new Error('Failed to register 1'); }; let updateAll1: ItemSnapshot => void = _ => { throw new Error('Failed to register 1'); }; let updateItem2: (ItemKey, DefaultValue | string) => void = () => { throw new Error('Failed to register 2'); }; const container = renderElements( { updateItem1 = listenInterface.updateItem; updateItems1 = listenInterface.updateItems; updateAll1 = listenInterface.updateAllKnownItems; }}> { updateItem2 = listenInterface.updateItem; }}> , ); expect(container.textContent).toBe('"A""B""C2"'); expect(storage1.size).toBe(3); // Subscribe to new value act(() => updateItem1('recoil-sync listen', 'AA')); expect(container.textContent).toBe('"AA""B""C2"'); // Avoid feedback loops expect(storage1.get('recoil-sync listen')).toBe('A'); // Subscribe to reset act(() => updateItem1('recoil-sync listen', new DefaultValue())); expect(container.textContent).toBe('"DEFAULT""B""C2"'); act(() => updateItem1('recoil-sync listen', 'AA')); // Subscribe to new value from different key act(() => updateItem1('KEY A', 'BB')); expect(container.textContent).toBe('"AA""BB""C2"'); // Neither key in same storage will be updated to avoid feedback loops expect(storage1.get('KEY A')).toBe('B'); expect(storage1.get('KEY B')).toBe(undefined); act(() => updateItem1('KEY B', 'BBB')); expect(container.textContent).toBe('"AA""BBB""C2"'); expect(storage1.get('KEY A')).toBe('B'); expect(storage1.get('KEY B')).toBe(undefined); // Subscribe to new value from different storage act(() => updateItem1('recoil-sync listen to multiple storage', 'CC1')); expect(container.textContent).toBe('"AA""BBB""CC1"'); // Avoid feedback loops, do not update storage based on listening to the storage expect(storage1.get('recoil-sync listen to multiple storage')).toBe('C1'); // But, we should update other storages to stay in sync expect(storage2.get('recoil-sync listen to multiple storage')).toBe('CC1'); act(() => updateItem2('recoil-sync listen to multiple storage', 'CC2')); expect(container.textContent).toBe('"AA""BBB""CC2"'); expect(storage1.get('recoil-sync listen to multiple storage')).toBe('CC2'); expect(storage2.get('recoil-sync listen to multiple storage')).toBe('CC1'); act(() => updateItem1('recoil-sync listen to multiple storage', 'CCC1')); expect(container.textContent).toBe('"AA""BBB""CCC1"'); expect(storage1.get('recoil-sync listen to multiple storage')).toBe('CC2'); expect(storage2.get('recoil-sync listen to multiple storage')).toBe('CCC1'); // Subscribe to reset act(() => updateItem1('recoil-sync listen to multiple storage', new DefaultValue()), ); expect(container.textContent).toBe('"AA""BBB""DEFAULT"'); expect(storage1.get('recoil-sync listen to multiple storage')).toBe('CC2'); expect(storage2.get('recoil-sync listen to multiple storage')).toBe( undefined, ); // Subscribe to error const ERROR = new Error('ERROR'); act(() => updateItem1('recoil-sync listen', RecoilLoadable.error(ERROR))); // TODO Atom should be put in an error state, but is just reset for now. expect(container.textContent).toBe('"DEFAULT""BBB""DEFAULT"'); // expect(storage1.get('recoil-sync listen')?.errorOrThrow()).toBe(ERROR); // Update Items // Set A while keeping B and C act(() => updateItems1(new Map([['recoil-sync listen', 'AAAA']]))); expect(container.textContent).toBe('"AAAA""BBB""DEFAULT"'); // Update All Items // Set A while resetting B act(() => updateAll1(new Map([['recoil-sync listen', 'AAA']]))); expect(container.textContent).toBe('"AAA""DEFAULT""DEFAULT"'); // Update All Items // Setting older Key while newer Key is blank will take value instead of default act(() => updateAll1( new Map([ ['recoil-sync listen', 'AAA'], ['KEY A', 'BBB'], ]), ), ); expect(container.textContent).toBe('"AAA""BBB""DEFAULT"'); // Update All Items // Setting an older and newer key will take the newer key value act(() => updateAll1( new Map([ ['recoil-sync listen', 'AAA'], ['KEY A', 'IGNORE'], ['KEY B', 'BBBB'], ]), ), ); expect(container.textContent).toBe('"AAA""BBBB""DEFAULT"'); // Update All Items // Not providing an item causes it to revert to default act(() => updateAll1(new Map([['recoil-sync listen', 'AAA']]))); expect(container.textContent).toBe('"AAA""DEFAULT""DEFAULT"'); // TODO Async Atom support // act(() => // updateItem1( // 'recoil-sync listen', // (Promise.resolve( 'ASYNC')), // ), // ); // await flushPromisesAndTimers(); // expect(container.textContent).toBe('"ASYNC""BBBB""DEFAULT"'); // act(() => // updateItem1( // 'KEY B', (Promise.reject(new Error('ERROR B'))), // ), // ); // await flushPromisesAndTimers(); // expect(container.textContent).toBe('error'); }); test('Persist on read', async () => { const atomA = atom({ key: 'recoil-sync persist on read default', default: 'DEFAULT', effects: [syncEffect({refine: string(), syncDefault: true})], }); const atomB = atom({ key: 'recoil-sync persist on read init', default: 'DEFAULT', effects: [ ({setSelf}) => setSelf('INIT_BEFORE'), syncEffect({refine: string(), syncDefault: true}), ({setSelf}) => setSelf('INIT_AFTER'), ], }); const storage = new Map(); const container = renderElements( , ); expect(storage.size).toBe(0); expect(container.textContent).toBe('"DEFAULT""INIT_AFTER"'); await flushPromisesAndTimers(); expect(storage.size).toBe(2); expect(storage.get('recoil-sync persist on read default')).toBe('DEFAULT'); expect(storage.get('recoil-sync persist on read init')).toBe('INIT_AFTER'); }); test('Persist on read - async', async () => { let resolveA, resolveB1, resolveB2; const atomA = atom({ key: 'recoil-sync persist on read default async', default: new Promise(resolve => { resolveA = resolve; }), effects: [syncEffect({refine: string(), syncDefault: true})], }); const atomB = atom({ key: 'recoil-sync persist on read init async', default: 'DEFAULT', effects: [ ({setSelf}) => setSelf( new Promise(resolve => { resolveB1 = resolve; }), ), syncEffect({refine: string(), syncDefault: true}), ({setSelf}) => setSelf( new Promise(resolve => { resolveB2 = resolve; }), ), ], }); const storage = new Map(); const container = renderElements( , ); await flushPromisesAndTimers(); expect(storage.size).toBe(0); act(() => { resolveA('ASYNC_DEFAULT'); }); await flushPromisesAndTimers(); expect(storage.size).toBe(1); act(() => { resolveB1('ASYNC_INIT_BEFORE'); }); await flushPromisesAndTimers(); expect(container.textContent).toBe('loading'); expect(storage.size).toBe(1); act(() => { resolveB2('ASYNC_INIT_AFTER'); }); await flushPromisesAndTimers(); expect(container.textContent).toBe('"ASYNC_DEFAULT""ASYNC_INIT_AFTER"'); expect(storage.size).toBe(2); expect(storage.get('recoil-sync persist on read default async')).toBe( 'ASYNC_DEFAULT', ); expect(storage.get('recoil-sync persist on read init async')).toBe( 'ASYNC_INIT_AFTER', ); }); test('Sync based on component props', async () => { function SyncWithProps({ children, ...props }: { children: React.Node, eggs: string, spam: string, }) { return ( itemKey in props ? props[itemKey] : new DefaultValue() }> {children} ); } const atomA = atom({ key: 'recoil-sync from props spam', default: 'DEFAULT', effects: [syncEffect({itemKey: 'spam', refine: string()})], }); const atomB = atom({ key: 'recoil-sync from props eggs', default: 'DEFAULT', effects: [syncEffect({itemKey: 'eggs', refine: string()})], }); const atomC = atom({ key: 'recoil-sync from props default', default: 'DEFAULT', effects: [syncEffect({itemKey: 'default', refine: string()})], }); const container = renderElements( , ); expect(container.textContent).toBe('"SPAM""EGGS""DEFAULT"'); }); test('Sync Atom Family', async () => { const atoms = atomFamily({ key: 'recoil-sync atom family', default: 'DEFAULT', // $FlowFixMe[missing-local-annot] effects: param => [syncEffect({itemKey: param, refine: string()})], }); const storage = new Map([ ['a', 'A'], ['b', 'B'], ]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('"A""B""DEFAULT"'); }); describe('Complex Mappings', () => { test('write to multiple items', async () => { const atomA = atom({ key: 'recoil-sync write multiple A', default: 'A', effects: [ syncEffect({ itemKey: 'a', // UNUSED refine: string(), write: ({write}, newValue) => { write( 'a1', newValue instanceof DefaultValue ? newValue : newValue + '1', ); write( 'a2', newValue instanceof DefaultValue ? newValue : newValue + '2', ); }, syncDefault: true, }), ], }); const atomB = atom({ key: 'recoil-sync write multiple B', default: 'DEFAULT', effects: [ syncEffect({ itemKey: 'b', // UNUSED refine: string(), write: ({write, reset}, newValue) => { if (newValue instanceof DefaultValue) { reset('b1'); reset('b2'); } else { write('b1', newValue + '1'); write('b2', newValue + '2'); } }, }), ], }); const [AtomB, setB, resetB] = componentThatReadsAndWritesAtom(atomB); const storage = new Map(); const allItemsRef = {current: new Map()}; const container = renderElements( , ); expect(container.textContent).toBe('"A""DEFAULT"'); await flushPromisesAndTimers(); // Test mapping when syncing default value expect(storage.size).toEqual(2); expect(storage.has('a')).toEqual(false); expect(storage.get('a1')).toEqual('A1'); expect(storage.get('a2')).toEqual('A2'); // Test mapping with allItems expect(allItemsRef.current.size).toEqual(4); expect(allItemsRef.current.get('a1')).toEqual('A1'); expect(allItemsRef.current.get('a2')).toEqual('A2'); expect(allItemsRef.current.get('b1')).toEqual(new DefaultValue()); expect(allItemsRef.current.get('b2')).toEqual(new DefaultValue()); // Test mapping when writing state changes act(() => setB('B')); expect(container.textContent).toBe('"A""B"'); expect(storage.size).toEqual(4); expect(storage.has('b')).toEqual(false); expect(storage.get('b1')).toEqual('B1'); expect(storage.get('b2')).toEqual('B2'); expect(allItemsRef.current.size).toEqual(4); expect(allItemsRef.current.get('b1')).toEqual('B1'); expect(allItemsRef.current.get('b2')).toEqual('B2'); // Test mapping when reseting state act(resetB); expect(container.textContent).toBe('"A""DEFAULT"'); expect(storage.size).toEqual(2); expect(storage.has('b')).toEqual(false); expect(storage.has('b1')).toEqual(false); expect(storage.has('b2')).toEqual(false); expect(allItemsRef.current.size).toEqual(4); expect(allItemsRef.current.get('b1')).toEqual(new DefaultValue()); expect(allItemsRef.current.get('b2')).toEqual(new DefaultValue()); }); test('read while writing', async () => { const myAtom = atom({ key: 'recoil-sync read while writing', default: 'SELF', effects: [ syncEffect({ refine: string(), write: ({write, read}, newValue) => { if (newValue instanceof DefaultValue) { write('self', newValue); return; } write('self', 'TMP'); expect(read('self')).toEqual('TMP'); write('self', `${String(read('other'))}_${newValue}`); }, syncDefault: true, }), ], }); const storage = new Map([['other', 'OTHER']]); const container = renderElements( // $FlowFixMe[incompatible-type-arg] , ); expect(container.textContent).toBe('"SELF"'); await flushPromisesAndTimers(); expect(storage.size).toEqual(2); expect(storage.get('self')).toEqual('OTHER_SELF'); }); test('read from multiple items', () => { const myAtom = atom({ key: 'recoil-sync read from multiple', default: 'DEFAULT', effects: [ syncEffect({ // $FlowFixMe[incompatible-call] refine: dict(number()), read: ({read}) => ({a: read('a'), b: read('b')}), }), ], }); const storage = new Map([ ['a', 1], ['b', 2], ]); let updateItem; const container = renderElements( { updateItem = listenInterface.updateItem; }}> , ); // Test mapping while initializing values expect(container.textContent).toBe('{"a":1,"b":2}'); // Test subscribing to multiple items act(() => updateItem('a', 10)); expect(container.textContent).toBe('{"a":10,"b":2}'); // Avoid feedback loops expect(storage.get('a')).toEqual(1); storage.set('a', 10); // Keep storage in sync act(() => updateItem('b', 20)); expect(container.textContent).toBe('{"a":10,"b":20}'); }); }); // Currently useRecoilSync() must be called in React component tree // before the first use of atoms to be initialized. // This is why we only expose and not useRecoilSync(). test('Reading before sync hook', async () => { const atoms = atomFamily({ key: 'recoil-sync order', default: 'DEFAULT', // $FlowFixMe[missing-local-annot] effects: param => [syncEffect({itemKey: param, refine: string()})], }); function SyncOrder() { const b = useRecoilValue(atoms('b')); useRecoilSync({read: itemKey => itemKey.toUpperCase()}); const c = useRecoilValue(atoms('c')); return (
{String(b)} {String(c)}
); } function MyRoot() { return (
); } const container = renderElements(); expect(container.textContent).toBe('"DEFAULT"DEFAULTC"D""E"'); }); test('Sibling ', async () => { const atomA = atom({ key: 'recoil-sync sibling root A', default: 'DEFAULT', effects: [syncEffect({itemKey: 'a', refine: string(), syncDefault: true})], }); const atomB = atom({ key: 'recoil-sync sibling root B', default: 'DEFAULT', effects: [syncEffect({itemKey: 'b', refine: string(), syncDefault: true})], }); const atomShared = atom({ key: 'recoil-sync sibling root shared', default: 'DEFAULT', effects: [ syncEffect({itemKey: 'shared', refine: string(), syncDefault: true}), ], }); const storageA = new Map([['a', 'A']]); const storageB = new Map([['shared', 'SHARED']]); const [AtomA, setA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); const [SharedInA, setSharedInA] = componentThatReadsAndWritesAtom(atomShared); const [SharedInB, setSharedInB] = componentThatReadsAndWritesAtom(atomShared); const container = renderElements( <> {/* $FlowFixMe[incompatible-type-arg] */} {/* $FlowFixMe[incompatible-type-arg] */} , ); expect(container.textContent).toEqual('"A""DEFAULT""DEFAULT""SHARED"'); await flushPromisesAndTimers(); expect(storageA.size).toBe(2); expect(storageB.size).toBe(1); expect(storageA.get('a')).toBe('A'); expect(storageA.get('shared')).toBe('DEFAULT'); expect(storageB.get('shared')).toBe('SHARED'); act(() => setA('SET_A')); expect(container.textContent).toEqual('"SET_A""DEFAULT""DEFAULT""SHARED"'); expect(storageA.size).toBe(2); expect(storageB.size).toBe(1); expect(storageA.get('a')).toBe('SET_A'); expect(storageA.get('shared')).toBe('DEFAULT'); expect(storageB.get('shared')).toBe('SHARED'); act(() => setB('SET_B')); expect(container.textContent).toEqual('"SET_A""DEFAULT""SET_B""SHARED"'); expect(storageA.size).toBe(2); expect(storageB.size).toBe(2); expect(storageA.get('a')).toBe('SET_A'); expect(storageA.get('shared')).toBe('DEFAULT'); expect(storageB.get('b')).toBe('SET_B'); expect(storageB.get('shared')).toBe('SHARED'); act(() => setSharedInA('SHARED_A')); expect(container.textContent).toEqual('"SET_A""SHARED_A""SET_B""SHARED"'); expect(storageA.size).toBe(2); expect(storageB.size).toBe(2); expect(storageA.get('a')).toBe('SET_A'); expect(storageA.get('shared')).toBe('SHARED_A'); expect(storageB.get('b')).toBe('SET_B'); expect(storageB.get('shared')).toBe('SHARED'); act(() => setSharedInB('SHARED_B')); expect(container.textContent).toEqual('"SET_A""SHARED_A""SET_B""SHARED_B"'); expect(storageA.size).toBe(2); expect(storageB.size).toBe(2); expect(storageA.get('a')).toBe('SET_A'); expect(storageA.get('shared')).toBe('SHARED_A'); expect(storageB.get('b')).toBe('SET_B'); expect(storageB.get('shared')).toBe('SHARED_B'); }); test('Unregister store and atoms', () => { const key = 'recoil-sync unregister'; const atomCleanups = []; const myAtom = atom({ key, default: 'DEFAULT', effects: [ ({storeID}) => { expect(registries_FOR_TESTING.getAtomRegistry(storeID).has(key)).toBe( false, ); }, syncEffect({refine: string()}), ({storeID}) => { expect(registries_FOR_TESTING.getAtomRegistry(storeID).has(key)).toBe( true, ); return () => { expect( registries_FOR_TESTING.getAtomRegistry(storeID).get(key)?.effects .size, ).toBe(0); atomCleanups.push(true); }; }, ], }); const subscriberRefCounts: Array = []; const unregister = jest.fn(idx => { subscriberRefCounts[idx]--; }); const register = jest.fn(idx => { subscriberRefCounts[idx] = (subscriberRefCounts[idx] ?? 0) + 1; return () => unregister(idx); }); function TestSyncUnregister({ children, idx, }: { children: React.Node, idx: number, }) { const listen = useCallback(() => register(idx), [idx]); return {children}; } let setNumRoots; function MyRoots() { const [roots, setRoots] = useState(0); setNumRoots = setRoots; return Array.from(Array(roots).keys()).map(i => ( {i} )); } const container = renderElements(); expect(container.textContent).toEqual(''); expect(register).toHaveBeenCalledTimes(0); expect(unregister).toHaveBeenCalledTimes(0); expect(subscriberRefCounts[0]).toEqual(undefined); expect(subscriberRefCounts[1]).toEqual(undefined); expect(atomCleanups.length).toEqual(0); act(() => setNumRoots(1)); expect(container.textContent).toEqual('0"DEFAULT"'); expect(register).toHaveBeenCalledTimes(1); expect(unregister).toHaveBeenCalledTimes(0); expect(subscriberRefCounts[0]).toEqual(1); expect(subscriberRefCounts[1]).toEqual(undefined); expect(atomCleanups.length).toEqual(0); act(() => setNumRoots(2)); expect(container.textContent).toEqual('0"DEFAULT"1"DEFAULT"'); expect(register).toHaveBeenCalledTimes(2); expect(unregister).toHaveBeenCalledTimes(0); expect(subscriberRefCounts[0]).toEqual(1); expect(subscriberRefCounts[1]).toEqual(1); expect(atomCleanups.length).toEqual(0); act(() => setNumRoots(1)); expect(container.textContent).toEqual('0"DEFAULT"'); expect(register).toHaveBeenCalledTimes(2); expect(unregister).toHaveBeenCalledTimes(1); expect(subscriberRefCounts[0]).toEqual(1); expect(subscriberRefCounts[1]).toEqual(0); expect(atomCleanups.length).toEqual(1); act(() => setNumRoots(0)); expect(container.textContent).toEqual(''); expect(register).toHaveBeenCalledTimes(2); expect(unregister).toHaveBeenCalledTimes(2); expect(subscriberRefCounts[0]).toEqual(0); expect(subscriberRefCounts[1]).toEqual(0); expect(atomCleanups.length).toEqual(2); }); ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync_URL-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {LocationOption} from '../RecoilSync_URL'; const {act} = require('ReactTestUtils'); const {atom} = require('Recoil'); const { TestURLSync, encodeURL, expectURL, } = require('../__test_utils__/RecoilSync_MockURLSerialization'); const {syncEffect} = require('../RecoilSync'); const {urlSyncEffect} = require('../RecoilSync_URL'); const React = require('react'); const { ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const {asType, match, number, string} = require('refine'); let atomIndex = 0; const nextKey = () => `recoil-url-sync/${atomIndex++}`; describe('Test URL Persistence', () => { beforeEach(() => { history.replaceState(null, '', '/path/page.html?foo=bar#anchor'); }); function testWriteToURL(loc: LocationOption, remainder: () => void) { const atomA = atom({ key: nextKey(), default: 'DEFAULT', effects: [urlSyncEffect({itemKey: 'a', refine: string()})], }); const atomB = atom({ key: nextKey(), default: 'DEFAULT', effects: [urlSyncEffect({itemKey: 'b', refine: string()})], }); const ignoreAtom = atom({ key: nextKey(), default: 'DEFAULT', }); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); const [IgnoreAtom, setIgnore] = componentThatReadsAndWritesAtom(ignoreAtom); const container = renderElements( , ); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); act(() => setA('A')); act(() => setB('B')); act(() => setIgnore('IGNORE')); expect(container.textContent).toBe('"A""B""IGNORE"'); expectURL([[loc, {a: 'A', b: 'B'}]]); act(() => resetA()); act(() => setB('BB')); expect(container.textContent).toBe('"DEFAULT""BB""IGNORE"'); expectURL([[loc, {b: 'BB'}]]); remainder(); } test('Write to URL', () => testWriteToURL({part: 'href'}, () => { expect(location.search).toBe(''); expect(location.pathname).toBe('/TEST'); })); test('Write to URL - Anchor Hash', () => testWriteToURL({part: 'hash'}, () => { expect(location.search).toBe('?foo=bar'); })); test('Write to URL - Query Search', () => testWriteToURL({part: 'search'}, () => { expect(location.hash).toBe('#anchor'); expect(new URL(location.href).searchParams.get('foo')).toBe(null); expect(new URL(location.href).searchParams.get('bar')).toBe(null); })); test('Write to URL - Query Params', () => testWriteToURL({part: 'queryParams'}, () => { expect(location.hash).toBe('#anchor'); expect(new URL(location.href).searchParams.get('foo')).toBe('bar'); })); test('Write to URL - Query Param', () => testWriteToURL({part: 'queryParams', param: 'bar'}, () => { expect(location.hash).toBe('#anchor'); expect(new URL(location.href).searchParams.get('foo')).toBe('bar'); })); test('Write to multiple params', async () => { const locA = {part: 'queryParams', param: 'paramA'}; const locB = {part: 'queryParams', param: 'paramB'}; const atomA = atom({ key: 'recoil-url-sync multiple param A', default: 'DEFAULT', effects: [syncEffect({storeKey: 'A', itemKey: 'x', refine: string()})], }); const atomB = atom({ key: 'recoil-url-sync multiple param B', default: 'DEFAULT', effects: [syncEffect({storeKey: 'B', itemKey: 'x', refine: string()})], }); const [AtomA, setA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); renderElements( <> , ); act(() => setA('A')); act(() => setB('B')); expectURL([ [locA, {x: 'A'}], [locB, {x: 'B'}], ]); }); function testReadFromURL( loc: | $TEMPORARY$object<{param?: string, part: 'queryParams'}> | $TEMPORARY$object<{part: 'hash'}> | $TEMPORARY$object<{part: 'href'}> | $TEMPORARY$object<{part: 'search'}>, ) { const atomA = atom({ key: nextKey(), default: 'DEFAULT', effects: [syncEffect({itemKey: 'a', refine: string()})], }); const atomB = atom({ key: nextKey(), default: 'DEFAULT', effects: [syncEffect({itemKey: 'b', refine: string()})], }); const atomC = atom({ key: nextKey(), default: 'DEFAULT', effects: [syncEffect({itemKey: 'c', refine: string()})], }); history.replaceState( null, '', encodeURL([ [ loc, { a: 'A', b: 'B', }, ], ]), ); const container = renderElements( , ); expect(container.textContent).toBe('"A""B""DEFAULT"'); } test('Read from URL', () => testReadFromURL({part: 'href'})); test('Read from URL - Anchor Hash', () => testReadFromURL({part: 'hash'})); test('Read from URL - Search Query', () => testReadFromURL({part: 'search'})); test('Read from URL - Query Params', () => testReadFromURL({part: 'queryParams'})); test('Read from URL - Query Param', () => testReadFromURL({part: 'queryParams', param: 'param'})); test('Read from URL - Query Param with other param', () => testReadFromURL({part: 'queryParams', param: 'other'})); test('Read from URL upgrade', async () => { const loc = {part: 'hash'}; // Fail validation const atomA = atom({ key: 'recoil-url-sync fail validation', default: 'DEFAULT', effects: [ // No matching sync effect syncEffect({ refine: string(), actionOnFailure_UNSTABLE: 'defaultValue', }), ], }); // Upgrade from number const atomB = atom({ key: 'recoil-url-sync upgrade number', default: 'DEFAULT', effects: [ syncEffect({ refine: match( string(), asType(number(), num => `${num}`), asType(string(), () => 'IGNORE'), // This rule is ignored ), }), ], }); // Upgrade from string const atomC = atom({ key: 'recoil-url-sync upgrade string', default: 0, effects: [ syncEffect({ refine: match( number(), asType(string(), Number), asType(number(), () => 999), // This rule is ignored ), }), ], }); history.replaceState( null, '', encodeURL([ [ loc, { 'recoil-url-sync fail validation': 123, 'recoil-url-sync upgrade number': 123, 'recoil-url-sync upgrade string': '123', }, ], ]), ); const container = renderElements( , ); expect(container.textContent).toBe('"DEFAULT""123"123'); }); test('Read/Write from URL with upgrade', async () => { const loc1 = {part: 'queryParams', param: 'param1'}; const loc2 = {part: 'queryParams', param: 'param2'}; const atomA = atom({ key: 'recoil-url-sync read/write upgrade type', default: 'DEFAULT', effects: [ syncEffect({ refine: match( string(), asType(number(), num => `${num}`), ), }), ], }); const atomB = atom({ key: 'recoil-url-sync read/write upgrade key', default: 'DEFAULT', effects: [ syncEffect({itemKey: 'OLD KEY', refine: string()}), syncEffect({itemKey: 'NEW KEY', refine: string()}), ], }); const atomC = atom({ key: 'recoil-url-sync read/write upgrade storage', default: 'DEFAULT', effects: [ syncEffect({refine: string()}), syncEffect({storeKey: 'SYNC_2', refine: string()}), ], }); history.replaceState( null, '', encodeURL([ [ loc1, { 'recoil-url-sync read/write upgrade type': 123, 'OLD KEY': 'OLD', 'recoil-url-sync read/write upgrade storage': 'STR1', }, ], [ loc2, { 'recoil-url-sync read/write upgrade storage': 'STR2', }, ], ]), ); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB, resetB] = componentThatReadsAndWritesAtom(atomB); const [AtomC, setC, resetC] = componentThatReadsAndWritesAtom(atomC); const container = renderElements( <> , ); expect(container.textContent).toBe('"123""OLD""STR2"'); act(() => setA('A')); act(() => setB('B')); act(() => setC('C')); expect(container.textContent).toBe('"A""B""C"'); expectURL([ [ loc1, { 'recoil-url-sync read/write upgrade type': 'A', 'OLD KEY': 'B', 'NEW KEY': 'B', 'recoil-url-sync read/write upgrade storage': 'C', }, ], [ loc2, { 'recoil-url-sync read/write upgrade storage': 'C', }, ], ]); act(() => resetA()); act(() => resetB()); act(() => resetC()); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); expectURL([ [loc1, {}], [loc2, {}], ]); }); }); test('Remove parameter', async () => { const loc = {part: 'queryParams', param: 'param'}; const atomA = atom({ key: 'recoil-url-sync remove param', default: 'DEFAULT', effects: [syncEffect({itemKey: 'item', refine: string()})], }); const container = renderElements( , ); expect(container.textContent).toBe('"DEFAULT"'); // Updating URL will cause atom to be set (Note manual triggering of popstate) history.replaceState(null, '', encodeURL([[loc, {item: 'SET'}]])); history.pushState(null, 'void'); history.back(); await flushPromisesAndTimers(); expect(container.textContent).toBe('"SET"'); // clear all query params from the URL to confirm it resets the atoms. history.replaceState(null, '', location.origin); history.pushState(null, 'void'); history.back(); await flushPromisesAndTimers(); expect(container.textContent).toBe('"DEFAULT"'); }); test('Persist default on read', async () => { const loc = {part: 'hash'}; const atomA = atom({ key: 'recoil-url-sync persist on read default', default: 'DEFAULT', effects: [syncEffect({refine: string(), syncDefault: true})], }); const atomB = atom({ key: 'recoil-url-sync persist on read init', default: 'DEFAULT', effects: [ ({setSelf}) => setSelf('INIT_BEFORE'), syncEffect({refine: string(), syncDefault: true}), ({setSelf}) => setSelf('INIT_AFTER'), ], }); const container = renderElements( , ); await flushPromisesAndTimers(); expect(container.textContent).toBe('"DEFAULT""INIT_AFTER"'); expectURL([ [ loc, { 'recoil-url-sync persist on read default': 'DEFAULT', 'recoil-url-sync persist on read init': 'INIT_AFTER', }, ], ]); }); ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync_URLCompound-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const {assertion, dict, nullable, number, string} = require('refine'); let React, DefaultValue, atom, atomFamily, act, componentThatReadsAndWritesAtom, renderElements, encodeURL, expectURL, gotoURL, syncEffect, RecoilURLSyncJSON; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({DefaultValue, atom, atomFamily} = require('Recoil')); ({act} = require('ReactTestUtils')); ({ componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({ encodeURL, expectURL, gotoURL, } = require('../__test_utils__/RecoilSync_MockURLSerialization')); ({syncEffect} = require('../RecoilSync')); ({RecoilURLSyncJSON} = require('../RecoilSync_URLJSON')); }); testRecoil('Upgrade item ID', async () => { const loc = {part: 'queryParams'}; const myAtom = atom({ key: 'recoil-url-sync upgrade itemID', default: 'DEFAULT', effects: [ syncEffect({ refine: string(), itemKey: 'new_key', read: ({read}) => read('old_key') ?? read('new_key'), }), ], }); history.replaceState(null, '', encodeURL([[loc, {old_key: 'OLD'}]])); const [Atom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); // Test that we can load based on old key expect(container.textContent).toEqual('"OLD"'); // Test that we can save to the new key act(() => setAtom('NEW')); expect(container.textContent).toEqual('"NEW"'); expectURL([[loc, {new_key: 'NEW'}]]); // Test that we can reset the atom and get the default instead of the old key's value act(resetAtom); expect(container.textContent).toEqual('"DEFAULT"'); expectURL([[loc, {}]]); }); testRecoil('Many items to one atom', async () => { const loc = {part: 'queryParams'}; const manyToOneSyncEffct = () => syncEffect({ refine: dict(nullable(number())), read: ({read}) => { const foo = read('foo'); const bar = read('bar'); return { foo: foo instanceof DefaultValue ? undefined : foo, bar: bar instanceof DefaultValue ? undefined : bar, }; }, write: ({write, reset}, newValue) => { if (newValue instanceof DefaultValue) { reset('foo'); reset('bar'); return; } for (const key of Object.keys(newValue)) { write(key, newValue[key]); } }, }); const myAtom = atom({ key: 'recoil-url-sync many-to-one', default: {}, effects: [manyToOneSyncEffct()], }); history.replaceState(null, '', encodeURL([[loc, {foo: 1}]])); const [Atom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); // Test initialize value from URL expect(container.textContent).toBe('{"foo":1}'); // Test subscribe to URL updates await gotoURL([[loc, {foo: 1, bar: 2}]]); expect(container.textContent).toBe('{"bar":2,"foo":1}'); // Test mutating atoms will update URL act(() => setAtom({foo: 3, bar: 4})); expectURL([[loc, {foo: 3, bar: 4}]]); // Test reseting atoms will update URL act(resetAtom); expectURL([[loc, {}]]); }); testRecoil('One item to multiple atoms', async () => { const loc = {part: 'queryParams'}; const input = assertion(dict(nullable(number()))); const oneToManySyncEffect = (prop: string) => syncEffect({ refine: nullable(number()), read: ({read}) => { const compound = input(read('compound')); return prop in compound ? compound[prop] : new DefaultValue(); }, write: ({write, read}, newValue) => { const compound = {...input(read('compound'))}; if (newValue instanceof DefaultValue) { delete compound[prop]; return write('compound', compound); } return write('compound', {...compound, [prop]: newValue}); }, }); const fooAtom = atom({ key: 'recoil-url-sync one-to-many foo', default: 0, effects: [oneToManySyncEffect('foo')], }); const barAtom = atom({ key: 'recoil-url-sync one-to-many bar', default: null, effects: [oneToManySyncEffect('bar')], }); history.replaceState(null, '', encodeURL([[loc, {compound: {foo: 1}}]])); const [Foo, setFoo, resetFoo] = componentThatReadsAndWritesAtom(fooAtom); const [Bar, setBar, resetBar] = componentThatReadsAndWritesAtom(barAtom); const container = renderElements( , ); // Test initialize value from URL expect(container.textContent).toBe('1null'); // Test subscribe to URL updates await gotoURL([[loc, {compound: {foo: 1, bar: 2}}]]); expect(container.textContent).toBe('12'); // Test mutating atoms will update URL act(() => setFoo(3)); expect(container.textContent).toBe('32'); expectURL([[loc, {compound: {foo: 3, bar: 2}}]]); act(() => setBar(4)); expect(container.textContent).toBe('34'); expectURL([[loc, {compound: {foo: 3, bar: 4}}]]); // Test reseting atoms will update URL act(resetFoo); expect(container.textContent).toBe('04'); expectURL([[loc, {compound: {bar: 4}}]]); act(resetBar); expect(container.textContent).toBe('0null'); expectURL([ [ loc, { compound: {}, }, ], ]); }); testRecoil('One item to atom family', async () => { const loc = {part: 'queryParams'}; const input = assertion(dict(nullable(number()))); const oneToFamilyEffect = (prop: string) => syncEffect({ refine: nullable(number()), read: ({read}) => { const compound = input(read('compound')); return prop in compound ? compound[prop] : new DefaultValue(); }, write: ({write, read}, newValue) => { const compound = {...input(read('compound'))}; if (newValue instanceof DefaultValue) { delete compound[prop]; return write('compound', compound); } return write('compound', {...compound, [prop]: newValue}); }, }); const myAtoms = atomFamily({ key: 'recoil-rul-sync one-to-family', default: null, // $FlowFixMe[missing-local-annot] effects: prop => [oneToFamilyEffect(prop)], }); history.replaceState(null, '', encodeURL([[loc, {compound: {foo: 1}}]])); const [Foo, setFoo, resetFoo] = componentThatReadsAndWritesAtom( myAtoms('foo'), ); const [Bar, setBar, resetBar] = componentThatReadsAndWritesAtom( myAtoms('bar'), ); const container = renderElements( , ); // Test initialize value from URL expect(container.textContent).toBe('1null'); // Test subscribe to URL updates await gotoURL([[loc, {compound: {foo: 1, bar: 2}}]]); expect(container.textContent).toBe('12'); // Test mutating atoms will update URL act(() => setFoo(3)); expect(container.textContent).toBe('32'); expectURL([[loc, {compound: {foo: 3, bar: 2}}]]); act(() => setBar(4)); expect(container.textContent).toBe('34'); expectURL([[loc, {compound: {foo: 3, bar: 4}}]]); // Test reseting atoms will update URL act(resetFoo); expect(container.textContent).toBe('null4'); expectURL([[loc, {compound: {bar: 4}}]]); act(resetBar); expect(container.textContent).toBe('nullnull'); expectURL([ [ loc, { compound: {}, }, ], ]); }); ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync_URLInterface-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {act} = require('ReactTestUtils'); const {atom} = require('Recoil'); const { TestURLSync, expectURL: testExpectURL, } = require('../__test_utils__/RecoilSync_MockURLSerialization'); const {urlSyncEffect} = require('../RecoilSync_URL'); const React = require('react'); const { componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const {string} = require('refine'); const urls: Array = []; const subscriptions: Set<() => void> = new Set(); const currentURL = () => urls[urls.length - 1]; const link = window.document.createElement('a'); const absoluteURL = (url: string) => { link.href = url; return link.href; }; /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ const expectURL = parts => testExpectURL(parts, currentURL()); const mockBrowserURL = { replaceURL: (url: string) => { urls[urls.length - 1] = absoluteURL(url); }, pushURL: (url: string) => void urls.push(absoluteURL(url)), getURL: () => absoluteURL(currentURL()), listenChangeURL: (handler: () => void) => { subscriptions.add(handler); return () => void subscriptions.delete(handler); }, }; const goBack = () => { urls.pop(); subscriptions.forEach(handler => handler()); }; test('Push URLs in mock history', async () => { const loc = {part: 'queryParams'}; const atomA = atom({ key: 'recoil-url-sync replace', default: 'DEFAULT', effects: [urlSyncEffect({refine: string(), history: 'replace'})], }); const atomB = atom({ key: 'recoil-url-sync push', default: 'DEFAULT', effects: [urlSyncEffect({refine: string(), history: 'push'})], }); const atomC = atom({ key: 'recoil-url-sync push 2', default: 'DEFAULT', effects: [urlSyncEffect({refine: string(), history: 'push'})], }); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB, resetB] = componentThatReadsAndWritesAtom(atomB); const [AtomC, setC] = componentThatReadsAndWritesAtom(atomC); const container = renderElements( , ); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); const baseHistory = history.length; // Replace A // 1: A__ act(() => setA('A')); expect(container.textContent).toBe('"A""DEFAULT""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', }, ], ]); expect(history.length).toBe(baseHistory); // Push B // 1: A__ // 2: AB_ act(() => setB('B')); expect(container.textContent).toBe('"A""B""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', 'recoil-url-sync push': 'B', }, ], ]); // Push C // 1: A__ // 2: AB_ // 3: ABC act(() => setC('C')); expect(container.textContent).toBe('"A""B""C"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', 'recoil-url-sync push': 'B', 'recoil-url-sync push 2': 'C', }, ], ]); // Pop and confirm C is reset // 1: A__ // 2: AB_ await act(goBack); expect(container.textContent).toBe('"A""B""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', 'recoil-url-sync push': 'B', }, ], ]); // Replace Reset A // 1: A__ // 2: _B_ act(resetA); expect(container.textContent).toBe('"DEFAULT""B""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync push': 'B', }, ], ]); // Push a Reset // 1: A__ // 2: _B_ // 3: ___ act(resetB); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); expectURL([[loc, {}]]); // Push BB // 1: A__ // 2: _B_ // 3: ___ // 4: _BB_ act(() => setB('BB')); expect(container.textContent).toBe('"DEFAULT""BB""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync push': 'BB', }, ], ]); // Replace AA // 1: A__ // 2: _B_ // 3: ___ // 4: AABB_ act(() => setA('AA')); expect(container.textContent).toBe('"AA""BB""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'AA', 'recoil-url-sync push': 'BB', }, ], ]); // Replace AAA // 1: A__ // 2: _B_ // 3: ___ // 4: AAABB_ act(() => setA('AAA')); expect(container.textContent).toBe('"AAA""BB""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'AAA', 'recoil-url-sync push': 'BB', }, ], ]); // Pop // 1: A__ // 2: _B_ // 3: ___ await act(goBack); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); expectURL([[loc, {}]]); // Pop // 1: A__ // 2: _B_ await act(goBack); expect(container.textContent).toBe('"DEFAULT""B""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync push': 'B', }, ], ]); // Pop // 1: A__ await act(goBack); expect(container.textContent).toBe('"A""DEFAULT""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', }, ], ]); }); ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync_URLJSON-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {LocationOption} from '../RecoilSync_URL'; const {atom} = require('Recoil'); const {syncEffect} = require('../RecoilSync'); const {RecoilURLSyncJSON} = require('../RecoilSync_URLJSON'); const React = require('react'); const { ReadsAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const { array, bool, jsonDate, literal, number, object, string, tuple, } = require('refine'); const atomUndefined = atom({ key: 'void', default: undefined, effects: [syncEffect({refine: literal(undefined), syncDefault: true})], }); const atomNull = atom({ key: 'null', default: null, effects: [syncEffect({refine: literal(null), syncDefault: true})], }); const atomBoolean = atom({ key: 'boolean', default: true, effects: [syncEffect({refine: bool(), syncDefault: true})], }); const atomNumber = atom({ key: 'number', default: 123, effects: [syncEffect({refine: number(), syncDefault: true})], }); const atomString = atom({ key: 'string', default: 'STRING', effects: [syncEffect({refine: string(), syncDefault: true})], }); const atomArray = atom({ key: 'array', default: [1, 'a'], effects: [syncEffect({refine: tuple(number(), string()), syncDefault: true})], }); const atomObject = atom({ key: 'object', default: {foo: [1, 2]}, effects: [ syncEffect({refine: object({foo: array(number())}), syncDefault: true}), ], }); const atomDate = atom({ key: 'date', default: new Date('7:00 GMT October 26, 1985'), effects: [syncEffect({refine: jsonDate(), syncDefault: true})], }); async function testJSON( loc: LocationOption, contents: string, beforeURL: string, afterURL: string, ) { history.replaceState(null, '', beforeURL); const container = renderElements( , ); expect(container.textContent).toBe(contents); await flushPromisesAndTimers(); expect(window.location.href).toBe(window.location.origin + afterURL); } describe('URL JSON Encode', () => { test('Anchor', async () => testJSON( {part: 'hash'}, 'nulltrue123"STRING"[1,"a"]{"foo":[1,2]}"1985-10-26T07:00:00.000Z"', '/path/page.html?foo=bar', '/path/page.html?foo=bar#%7B%22null%22%3Anull%2C%22boolean%22%3Atrue%2C%22number%22%3A123%2C%22string%22%3A%22STRING%22%2C%22array%22%3A%5B1%2C%22a%22%5D%2C%22object%22%3A%7B%22foo%22%3A%5B1%2C2%5D%7D%2C%22date%22%3A%221985-10-26T07%3A00%3A00.000Z%22%7D', )); test('Search', async () => testJSON( {part: 'search'}, 'nulltrue123"STRING"[1,"a"]{"foo":[1,2]}"1985-10-26T07:00:00.000Z"', '/path/page.html#anchor', '/path/page.html?%7B%22null%22%3Anull%2C%22boolean%22%3Atrue%2C%22number%22%3A123%2C%22string%22%3A%22STRING%22%2C%22array%22%3A%5B1%2C%22a%22%5D%2C%22object%22%3A%7B%22foo%22%3A%5B1%2C2%5D%7D%2C%22date%22%3A%221985-10-26T07%3A00%3A00.000Z%22%7D#anchor', )); test('Query Param', async () => testJSON( {part: 'queryParams', param: 'param'}, 'nulltrue123"STRING"[1,"a"]{"foo":[1,2]}"1985-10-26T07:00:00.000Z"', '/path/page.html?foo=bar#anchor', '/path/page.html?foo=bar¶m=%7B%22null%22%3Anull%2C%22boolean%22%3Atrue%2C%22number%22%3A123%2C%22string%22%3A%22STRING%22%2C%22array%22%3A%5B1%2C%22a%22%5D%2C%22object%22%3A%7B%22foo%22%3A%5B1%2C2%5D%7D%2C%22date%22%3A%221985-10-26T07%3A00%3A00.000Z%22%7D#anchor', )); test('Query Params', async () => testJSON( {part: 'queryParams'}, 'nulltrue123"STRING"[1,"a"]{"foo":[1,2]}"1985-10-26T07:00:00.000Z"', '/path/page.html?foo=bar#anchor', '/path/page.html?foo=bar&void=&null=null&boolean=true&number=123&string=%22STRING%22&array=%5B1%2C%22a%22%5D&object=%7B%22foo%22%3A%5B1%2C2%5D%7D&date=%221985-10-26T07%3A00%3A00.000Z%22#anchor', )); }); describe('URL JSON Parse', () => { test('Anchor', async () => testJSON( {part: 'hash'}, 'nullfalse456"SET"[2,"b"]{"foo":[]}"1955-11-05T07:00:00.000Z"', '/#{"null":null,"boolean":false,"number":456,"string":"SET","array":[2,"b"],"object":{"foo":[]},"date":"1955-11-05T07:00:00.000Z"}', '/#%7B%22null%22%3Anull%2C%22boolean%22%3Afalse%2C%22number%22%3A456%2C%22string%22%3A%22SET%22%2C%22array%22%3A%5B2%2C%22b%22%5D%2C%22object%22%3A%7B%22foo%22%3A%5B%5D%7D%2C%22date%22%3A%221955-11-05T07%3A00%3A00.000Z%22%7D', )); test('Search', async () => testJSON( {part: 'search'}, 'nullfalse456"SET"[2,"b"]{"foo":[]}"1955-11-05T07:00:00.000Z"', '/?{"null":null,"boolean":false,"number":456,"string":"SET","array":[2,"b"],"object":{"foo":[]},"date":"1955-11-05T07:00:00.000Z"}', '/?%7B%22null%22%3Anull%2C%22boolean%22%3Afalse%2C%22number%22%3A456%2C%22string%22%3A%22SET%22%2C%22array%22%3A%5B2%2C%22b%22%5D%2C%22object%22%3A%7B%22foo%22%3A%5B%5D%7D%2C%22date%22%3A%221955-11-05T07%3A00%3A00.000Z%22%7D', )); test('Query Param', async () => testJSON( {part: 'queryParams', param: 'param'}, 'nullfalse456"SET"[2,"b"]{"foo":[]}"1955-11-05T07:00:00.000Z"', '/?param={"null":null,"boolean":false,"number":456,"string":"SET","array":[2,"b"],"object":{"foo":[]},"date":"1955-11-05T07:00:00.000Z"}', '/?param=%7B%22null%22%3Anull%2C%22boolean%22%3Afalse%2C%22number%22%3A456%2C%22string%22%3A%22SET%22%2C%22array%22%3A%5B2%2C%22b%22%5D%2C%22object%22%3A%7B%22foo%22%3A%5B%5D%7D%2C%22date%22%3A%221955-11-05T07%3A00%3A00.000Z%22%7D', )); test('Query Params', async () => testJSON( {part: 'queryParams'}, 'nullfalse456"SET"[2,"b"]{"foo":[]}"1955-11-05T07:00:00.000Z"', '/?foo=bar&null=null&boolean=false&number=456&string="SET"&array=[2,"b"]&object={"foo":[]}&date="1955-11-05T07:00:00.000Z"', '/?foo=bar&null=null&boolean=false&number=456&string=%22SET%22&array=%5B2%2C%22b%22%5D&object=%7B%22foo%22%3A%5B%5D%7D&date=%221955-11-05T07%3A00%3A00.000Z%22&void=', )); }); ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync_URLListen-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {act} = require('ReactTestUtils'); const {atom} = require('Recoil'); const { TestURLSync, encodeURL, expectURL, gotoURL, } = require('../__test_utils__/RecoilSync_MockURLSerialization'); const {syncEffect} = require('../RecoilSync'); const React = require('react'); const { ReadsAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const {string} = require('refine'); test('Listen to URL changes', async () => { const locFoo = {part: 'queryParams', param: 'foo'}; const locBar = {part: 'queryParams', param: 'bar'}; const atomA = atom({ key: 'recoil-url-sync listen', default: 'DEFAULT', effects: [syncEffect({storeKey: 'foo', refine: string()})], }); const atomB = atom({ key: 'recoil-url-sync listen to multiple keys', default: 'DEFAULT', effects: [ syncEffect({storeKey: 'foo', itemKey: 'KEY A', refine: string()}), syncEffect({storeKey: 'foo', itemKey: 'KEY B', refine: string()}), ], }); const atomC = atom({ key: 'recoil-url-sync listen to multiple storage', default: 'DEFAULT', effects: [ syncEffect({storeKey: 'foo', refine: string()}), syncEffect({storeKey: 'bar', refine: string()}), ], }); history.replaceState( null, '', encodeURL([ [ locFoo, { 'recoil-url-sync listen': 'A', 'KEY A': 'B', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]), ); const container = renderElements( <> , ); expect(container.textContent).toBe('"A""B""C"'); expectURL([ [ locFoo, { 'recoil-url-sync listen': 'A', 'KEY A': 'B', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]); // Subscribe to new value await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen': 'AA', 'KEY A': 'B', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]), ); expect(container.textContent).toBe('"AA""B""C"'); expectURL([ [ locFoo, { 'recoil-url-sync listen': 'AA', 'KEY A': 'B', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]); // Subscribe to new value from different key await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen': 'AA', 'KEY A': 'BB', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]), ); expectURL([ [ locFoo, { 'recoil-url-sync listen': 'AA', 'KEY A': 'BB', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]); expect(container.textContent).toBe('"AA""BB""C"'); await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen': 'AA', 'KEY A': 'BB', 'KEY B': 'BBB', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]), ); expect(container.textContent).toBe('"AA""BBB""C"'); await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen': 'AA', 'KEY A': 'IGNORE', 'KEY B': 'BBB', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]), ); expect(container.textContent).toBe('"AA""BBB""C"'); await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen': 'AA', 'KEY A': 'BBBB', 'recoil-url-sync listen to multiple storage': 'C', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'C', }, ], ]), ); expect(container.textContent).toBe('"AA""BBBB""C"'); // Subscribe to reset await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen to multiple storage': 'C', }, ], [locBar, {}], ]), ); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); // Subscribe to new value from different storage await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen': 'AA', 'KEY A': 'B', 'recoil-url-sync listen to multiple storage': 'C1', }, ], [locBar, {}], ]), ); expect(container.textContent).toBe('"AA""B""DEFAULT"'); await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen to multiple storage': 'C1', }, ], [ locBar, { 'recoil-url-sync listen to multiple storage': 'CC', }, ], ]), ); expect(container.textContent).toBe('"DEFAULT""DEFAULT""CC"'); await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen to multiple storage': 'C1', }, ], [locBar, {}], ]), ); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); await act(() => gotoURL([ [ locFoo, { 'recoil-url-sync listen to multiple storage': 'CC1', }, ], [locBar, {}], ]), ); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); }); ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync_URLPush-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {act} = require('ReactTestUtils'); const {atom} = require('Recoil'); const { TestURLSync, expectURL, goBack, } = require('../__test_utils__/RecoilSync_MockURLSerialization'); const {urlSyncEffect} = require('../RecoilSync_URL'); const React = require('react'); const { componentThatReadsAndWritesAtom, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const {string} = require('refine'); test('Push URLs in browser history', async () => { const loc = {part: 'queryParams'}; const atomA = atom({ key: 'recoil-url-sync replace', default: 'DEFAULT', effects: [urlSyncEffect({refine: string(), history: 'replace'})], }); const atomB = atom({ key: 'recoil-url-sync push', default: 'DEFAULT', effects: [urlSyncEffect({refine: string(), history: 'push'})], }); const atomC = atom({ key: 'recoil-url-sync push 2', default: 'DEFAULT', effects: [urlSyncEffect({refine: string(), history: 'push'})], }); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB, resetB] = componentThatReadsAndWritesAtom(atomB); const [AtomC, setC] = componentThatReadsAndWritesAtom(atomC); const container = renderElements( , ); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); const baseHistory = history.length; // Replace A // 1: A__ act(() => setA('A')); expect(container.textContent).toBe('"A""DEFAULT""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', }, ], ]); expect(history.length).toBe(baseHistory); // Push B // 1: A__ // 2: AB_ act(() => setB('B')); expect(container.textContent).toBe('"A""B""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', 'recoil-url-sync push': 'B', }, ], ]); // Push C // 1: A__ // 2: AB_ // 3: ABC act(() => setC('C')); expect(container.textContent).toBe('"A""B""C"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', 'recoil-url-sync push': 'B', 'recoil-url-sync push 2': 'C', }, ], ]); // Pop and confirm C is reset // 1: A__ // 2: AB_ await act(goBack); expect(container.textContent).toBe('"A""B""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', 'recoil-url-sync push': 'B', }, ], ]); // Replace Reset A // 1: A__ // 2: _B_ act(resetA); expect(container.textContent).toBe('"DEFAULT""B""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync push': 'B', }, ], ]); // Push a Reset // 1: A__ // 2: _B_ // 3: ___ act(resetB); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); expectURL([[loc, {}]]); // Push BB // 1: A__ // 2: _B_ // 3: ___ // 4: _BB_ act(() => setB('BB')); expect(container.textContent).toBe('"DEFAULT""BB""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync push': 'BB', }, ], ]); // Replace AA // 1: A__ // 2: _B_ // 3: ___ // 4: AABB_ act(() => setA('AA')); expect(container.textContent).toBe('"AA""BB""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'AA', 'recoil-url-sync push': 'BB', }, ], ]); // Replace AAA // 1: A__ // 2: _B_ // 3: ___ // 4: AAABB_ act(() => setA('AAA')); expect(container.textContent).toBe('"AAA""BB""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'AAA', 'recoil-url-sync push': 'BB', }, ], ]); // Pop // 1: A__ // 2: _B_ // 3: ___ await act(goBack); expect(container.textContent).toBe('"DEFAULT""DEFAULT""DEFAULT"'); expectURL([[loc, {}]]); // Pop // 1: A__ // 2: _B_ await act(goBack); expect(container.textContent).toBe('"DEFAULT""B""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync push': 'B', }, ], ]); // Pop // 1: A__ await act(goBack); expect(container.textContent).toBe('"A""DEFAULT""DEFAULT"'); expectURL([ [ loc, { 'recoil-url-sync replace': 'A', }, ], ]); }); ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync_URLTransit-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {LocationOption} from '../RecoilSync_URL'; import type {TransitHandler} from '../RecoilSync_URLTransit'; const {atom, selector} = require('Recoil'); const {syncEffect} = require('../RecoilSync'); const {RecoilURLSyncTransit} = require('../RecoilSync_URLTransit'); const React = require('react'); const { ReadsAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const { array, bool, custom, date, literal, map, number, object, set, string, tuple, } = require('refine'); class MyClass { prop: string; constructor(msg: string) { this.prop = msg; } } const atomNull = atom({ key: 'null', default: null, effects: [syncEffect({refine: literal(null), syncDefault: true})], }); const atomBoolean = atom({ key: 'boolean', default: true, effects: [syncEffect({refine: bool(), syncDefault: true})], }); const atomNumber = atom({ key: 'number', default: 123, effects: [syncEffect({refine: number(), syncDefault: true})], }); const atomString = atom({ key: 'string', default: 'STRING', effects: [syncEffect({refine: string(), syncDefault: true})], }); const atomArray = atom({ key: 'array', default: [1, 'a'], effects: [syncEffect({refine: tuple(number(), string()), syncDefault: true})], }); const atomObject = atom({ key: 'object', default: {foo: [1, 2]}, effects: [ syncEffect({refine: object({foo: array(number())}), syncDefault: true}), ], }); const atomSet = atom({ key: 'set', default: new Set([1, 2]), effects: [syncEffect({refine: set(number()), syncDefault: true})], }); const atomMap = atom({ key: 'map', default: new Map([[1, 'a']]), effects: [syncEffect({refine: map(number(), string()), syncDefault: true})], }); const atomDate = atom({ key: 'date', default: new Date('7:00 GMT October 26, 1985'), effects: [syncEffect({refine: date(), syncDefault: true})], }); const atomUser = atom({ key: 'user', default: new MyClass('CUSTOM'), effects: [ syncEffect({ refine: custom(x => (x instanceof MyClass ? x : null)), syncDefault: true, }), ], }); const atomWithFallback = atom({ key: 'withFallback', default: selector({key: 'fallback selector', get: () => 'FALLBACK'}), effects: [syncEffect({refine: string(), syncDefault: true})], }); const HANDLERS = [ { tag: 'USER', class: MyClass, write: (x: $FlowFixMe) => [x.prop], read: ([x]: $FlowFixMe) => new MyClass(x), }, ]; async function testTransit( loc: LocationOption, /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ atoms, contents: string, beforeURL: string, afterURL: string, ) { history.replaceState(null, '', beforeURL); const container = renderElements( {atoms.map(testAtom => ( ))} , ); expect(container.textContent).toBe(contents); await flushPromisesAndTimers(); expect(window.location.href).toBe(window.location.origin + afterURL); } describe('URL Transit Encode', () => { test('Anchor - primitives', async () => testTransit( {part: 'hash'}, [atomNull, atomBoolean, atomNumber, atomString], 'nulltrue123"STRING"', '/path/page.html?foo=bar', '/path/page.html?foo=bar#%5B%22%5E%20%22%2C%22null%22%2Cnull%2C%22boolean%22%2Ctrue%2C%22number%22%2C123%2C%22string%22%2C%22STRING%22%5D', )); test('Search - primitives', async () => testTransit( {part: 'search'}, [atomNull, atomBoolean, atomNumber, atomString], 'nulltrue123"STRING"', '/path/page.html#anchor', '/path/page.html?%5B%22%5E%20%22%2C%22null%22%2Cnull%2C%22boolean%22%2Ctrue%2C%22number%22%2C123%2C%22string%22%2C%22STRING%22%5D#anchor', )); test('Query Param - primitives', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomNull, atomBoolean, atomNumber, atomString], 'nulltrue123"STRING"', '/path/page.html?foo=bar#anchor', '/path/page.html?foo=bar¶m=%5B%22%5E+%22%2C%22null%22%2Cnull%2C%22boolean%22%2Ctrue%2C%22number%22%2C123%2C%22string%22%2C%22STRING%22%5D#anchor', )); test('Query Params - primitives', async () => testTransit( {part: 'queryParams'}, [atomNull, atomBoolean, atomNumber, atomString], 'nulltrue123"STRING"', '/path/page.html#anchor', '/path/page.html?null=%5B%22%7E%23%27%22%2Cnull%5D&boolean=%5B%22%7E%23%27%22%2Ctrue%5D&number=%5B%22%7E%23%27%22%2C123%5D&string=%5B%22%7E%23%27%22%2C%22STRING%22%5D#anchor', )); test('Query Param - objects', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomArray, atomObject], '[1,"a"]{"foo":[1,2]}', '/path/page.html?foo=bar#anchor', '/path/page.html?foo=bar¶m=%5B%22%5E+%22%2C%22array%22%2C%5B1%2C%22a%22%5D%2C%22object%22%2C%5B%22%5E+%22%2C%22foo%22%2C%5B1%2C2%5D%5D%5D#anchor', )); test('Query Params - objects', async () => testTransit( {part: 'queryParams'}, [atomArray, atomObject], '[1,"a"]{"foo":[1,2]}', '/path/page.html#anchor', '/path/page.html?array=%5B1%2C%22a%22%5D&object=%5B%22%5E+%22%2C%22foo%22%2C%5B1%2C2%5D%5D#anchor', )); test('Query Param - containers', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomSet, atomMap], '[1,2]{"1":"a"}', '/path/page.html?foo=bar#anchor', '/path/page.html?foo=bar¶m=%5B%22%5E+%22%2C%22set%22%2C%5B%22%7E%23Set%22%2C%5B1%2C2%5D%5D%2C%22map%22%2C%5B%22%7E%23Map%22%2C%5B%5B1%2C%22a%22%5D%5D%5D%5D#anchor', )); test('Query Params - containers', async () => testTransit( {part: 'queryParams'}, [atomSet, atomMap], '[1,2]{"1":"a"}', '/path/page.html#anchor', '/path/page.html?set=%5B%22%7E%23Set%22%2C%5B1%2C2%5D%5D&map=%5B%22%7E%23Map%22%2C%5B%5B1%2C%22a%22%5D%5D%5D#anchor', )); test('Query Param - classes', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomDate, atomUser], '"1985-10-26T07:00:00.000Z"{"prop":"CUSTOM"}', '/path/page.html?foo=bar#anchor', '/path/page.html?foo=bar¶m=%5B%22%5E+%22%2C%22date%22%2C%5B%22%7E%23Date%22%2C%221985-10-26T07%3A00%3A00.000Z%22%5D%2C%22user%22%2C%5B%22%7E%23USER%22%2C%5B%22CUSTOM%22%5D%5D%5D#anchor', )); test('Query Params - classes', async () => testTransit( {part: 'queryParams'}, [atomDate, atomUser], '"1985-10-26T07:00:00.000Z"{"prop":"CUSTOM"}', '/path/page.html#anchor', '/path/page.html?date=%5B%22%7E%23Date%22%2C%221985-10-26T07%3A00%3A00.000Z%22%5D&user=%5B%22%7E%23USER%22%2C%5B%22CUSTOM%22%5D%5D#anchor', )); test('Query Param - fallback', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomWithFallback], '"FALLBACK"', '/path/page.html?foo=bar#anchor', '/path/page.html?foo=bar¶m=%5B%22%5E+%22%5D#anchor', )); test('Query Params - fallback', async () => testTransit( {part: 'queryParams'}, [atomWithFallback], '"FALLBACK"', '/path/page.html#anchor', '/path/page.html#anchor', )); }); describe('URL Transit Parse', () => { test('Anchor - primitives', async () => testTransit( {part: 'hash'}, [atomNull, atomBoolean, atomNumber, atomString], 'nullfalse456"SET"', '/#["^ ","null",null,"boolean",false,"number",456,"string","SET"]', '/#%5B%22%5E%20%22%2C%22null%22%2Cnull%2C%22boolean%22%2Cfalse%2C%22number%22%2C456%2C%22string%22%2C%22SET%22%5D', )); test('Search - primitives', async () => testTransit( {part: 'search'}, [atomNull, atomBoolean, atomNumber, atomString], 'nullfalse456"SET"', '/?["^ ","null",null,"boolean",false,"number",456,"string","SET"]', '/?%5B%22%5E%20%22%2C%22null%22%2Cnull%2C%22boolean%22%2Cfalse%2C%22number%22%2C456%2C%22string%22%2C%22SET%22%5D', )); test('Query Param - primitives', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomNull, atomBoolean, atomNumber, atomString], 'nullfalse456"SET"', '/?param=["^ ","null",null,"boolean",false,"number",456,"string","SET"]', '/?param=%5B%22%5E+%22%2C%22null%22%2Cnull%2C%22boolean%22%2Cfalse%2C%22number%22%2C456%2C%22string%22%2C%22SET%22%5D', )); test('Query Params - primitives', async () => testTransit( {part: 'queryParams'}, [atomNull, atomBoolean, atomNumber, atomString], 'nullfalse456"SET"', '/?null=["~%23\'",null]&boolean=["~%23\'",false]&number=["~%23\'",456]&string=["~%23\'","SET"]', '/?null=%5B%22%7E%23%27%22%2Cnull%5D&boolean=%5B%22%7E%23%27%22%2Cfalse%5D&number=%5B%22%7E%23%27%22%2C456%5D&string=%5B%22%7E%23%27%22%2C%22SET%22%5D', )); test('Query Param - objects', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomArray, atomObject], '[2,"b"]{"foo":[]}', '/?param=["^ ","array",[2,"b"],"object",["^ ","foo",[]]]', '/?param=%5B%22%5E+%22%2C%22array%22%2C%5B2%2C%22b%22%5D%2C%22object%22%2C%5B%22%5E+%22%2C%22foo%22%2C%5B%5D%5D%5D', )); test('Query Params - objects', async () => testTransit( {part: 'queryParams'}, [atomArray, atomObject], '[2,"b"]{"foo":[]}', '/?array=[2,"b"]&object=["^+","foo",[]]', '/?array=%5B2%2C%22b%22%5D&object=%5B%22%5E+%22%2C%22foo%22%2C%5B%5D%5D', )); test('Query Param - containers', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomSet, atomMap], '[3,4]{"2":"b"}', '/?param=["^+","set",["~%23Set",[3,4]],"map",["~%23Map",[[2,"b"]]]]', '/?param=%5B%22%5E+%22%2C%22set%22%2C%5B%22%7E%23Set%22%2C%5B3%2C4%5D%5D%2C%22map%22%2C%5B%22%7E%23Map%22%2C%5B%5B2%2C%22b%22%5D%5D%5D%5D', )); test('Query Params - containers', async () => testTransit( {part: 'queryParams'}, [atomSet, atomMap], '[3,4]{"2":"b"}', '/?set=["~%23Set",[3,4]]&map=["~%23Map",[[2,"b"]]]', '/?set=%5B%22%7E%23Set%22%2C%5B3%2C4%5D%5D&map=%5B%22%7E%23Map%22%2C%5B%5B2%2C%22b%22%5D%5D%5D', )); test('Query Param - classes', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomDate, atomUser], '"1955-11-05T07:00:00.000Z"{"prop":"PROP"}', '/?param=["^ ","date",["~%23Date","1955-11-05T07:00:00.000Z"],"user",["~%23USER",["PROP"]]]', '/?param=%5B%22%5E+%22%2C%22date%22%2C%5B%22%7E%23Date%22%2C%221955-11-05T07%3A00%3A00.000Z%22%5D%2C%22user%22%2C%5B%22%7E%23USER%22%2C%5B%22PROP%22%5D%5D%5D', )); test('Query Params - classes', async () => testTransit( {part: 'queryParams'}, [atomDate, atomUser], '"1955-11-05T07:00:00.000Z"{"prop":"PROP"}', '/?date=["~%23Date","1955-11-05T07:00:00.000Z"]&user=["~%23USER",["PROP"]]', '/?date=%5B%22%7E%23Date%22%2C%221955-11-05T07%3A00%3A00.000Z%22%5D&user=%5B%22%7E%23USER%22%2C%5B%22PROP%22%5D%5D', )); test('Query Param - fallback', async () => testTransit( {part: 'queryParams', param: 'param'}, [atomWithFallback], '"SET"', '/?param=["^ ","withFallback","SET"]', '/?param=%5B%22%5E+%22%2C%22withFallback%22%2C%22SET%22%5D', )); test('Query Params - fallback', async () => testTransit( {part: 'queryParams'}, [atomWithFallback], '"SET"', '/?withFallback="SET"', '/?withFallback=%5B%22%7E%23%27%22%2C%22SET%22%5D', )); }); describe('URL Transit - handlers prop', () => { let consoleErrorSpy; const originalDEV = window.__DEV__; beforeEach(() => { window.__DEV__ = true; consoleErrorSpy = jest.spyOn(console, 'error'); }); afterEach(() => { window.__DEV__ = originalDEV; consoleErrorSpy.mockRestore(); }); function wasExpectationViolationCalled(): boolean { const expectationViolation = consoleErrorSpy.mock.calls.find(call => call[0]?.message?.match(/RecoilURLSyncTransit.*unstable/), ); return !!expectationViolation; } test('detect unstable handlers', async () => { const container = document.createElement('div'); function renderWithTransitHandlers( // $FlowFixMe[unclear-type] handlers: Array>, ) { renderElements( , container, ); } // $FlowFixMe[unclear-type] const handlersA: Array> = []; // $FlowFixMe[unclear-type] const handlersB: Array> = []; renderWithTransitHandlers(handlersA); renderWithTransitHandlers(handlersA); expect(wasExpectationViolationCalled()).toBe(false); renderWithTransitHandlers(handlersB); expect(wasExpectationViolationCalled()).toBe(true); }); }); ================================================ FILE: packages/recoil-sync/__tests__/RecoilSync_URLTransitJSON-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {atom} = require('Recoil'); const {syncEffect} = require('../RecoilSync'); const {RecoilURLSyncJSON} = require('../RecoilSync_URLJSON'); const {RecoilURLSyncTransit} = require('../RecoilSync_URLTransit'); const React = require('react'); const { ReadsAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const {array, bool, number, object, string, tuple} = require('refine'); const atomBoolean = atom({ key: 'boolean', default: true, effects: [syncEffect({storeKey: 'json', refine: bool(), syncDefault: true})], }); const atomNumber = atom({ key: 'number', default: 123, effects: [ syncEffect({storeKey: 'json', refine: number(), syncDefault: true}), ], }); const atomString = atom({ key: 'string', default: 'STRING', effects: [ syncEffect({storeKey: 'json', refine: string(), syncDefault: true}), ], }); const atomArray = atom({ key: 'array', default: [1, 'a'], effects: [ syncEffect({ storeKey: 'transit', refine: tuple(number(), string()), syncDefault: true, }), ], }); const atomObject = atom({ key: 'object', default: {foo: [1, 2]}, effects: [ syncEffect({ storeKey: 'transit', refine: object({foo: array(number())}), syncDefault: true, }), ], }); async function testURL(contents: string, beforeURL: string, afterURL: string) { history.replaceState(null, '', beforeURL); const container = renderElements( , ); expect(container.textContent).toBe(contents); await flushPromisesAndTimers(); expect(window.location.href).toBe(window.location.origin + afterURL); } test('URL Encode JSON & Transit', async () => testURL( 'true123"STRING"[1,"a"]{"foo":[1,2]}', '/path/page.html?foo=bar', '/path/page.html?foo=bar&boolean=true&number=123&string=%22STRING%22&transit=%5B%22%5E+%22%2C%22array%22%2C%5B1%2C%22a%22%5D%2C%22object%22%2C%5B%22%5E+%22%2C%22foo%22%2C%5B1%2C2%5D%5D%5D', )); test('URL Parse JSON & Transit', async () => testURL( 'false456"SET"[2,"b"]{"foo":[]}', '/?foo=bar&boolean=false&number=456&string="SET"&transit=["^ ","array",[2,"b"],"object",["^ ","foo",[]],"user",["~%23USER",["PROP"]]]', '/?foo=bar&boolean=false&number=456&string=%22SET%22&transit=%5B%22%5E+%22%2C%22array%22%2C%5B2%2C%22b%22%5D%2C%22object%22%2C%5B%22%5E+%22%2C%22foo%22%2C%5B%5D%5D%5D', )); ================================================ FILE: packages/recoil-sync/package-for-release.json ================================================ { "name": "recoil-sync", "version": "0.2.0", "description": "recoil-sync provides an add-on library to help synchronize Recoil state with external systems", "main": "cjs/index.js", "module": "es/index.js", "unpkg": "umd/index.js", "types": "index.d.ts", "files": ["umd", "es", "cjs", "index.d.ts"], "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT", "dependencies": { "transit-js": "^0.8.874", "@recoiljs/refine": "^0.1.1" }, "peerDependencies": { "recoil": ">=0.7.3" } } ================================================ FILE: packages/recoil-sync/package.json ================================================ { "name": "recoil-sync", "description": "This is the internal package.json enabling CommonJS module", "main": "RecoilSync_index.js", "haste_commonjs": true, "files": [ "RecoilSync_index.js" ], "directories": { "": "./" }, "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT" } ================================================ FILE: packages/refine/Refine_API.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * refine: type-refinement combinator library for checking mixed values * see wiki for more info: https://fburl.com/wiki/14q16qqy * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; import type {Checker, CheckFailure, CheckResult} from './Refine_Checkers'; const err = require('recoil-shared/util/Recoil_err'); /** * function to assert that a given value matches a checker */ export type AssertionFunction = (value: mixed) => V; /** * function to coerce a given value to a checker type, returning null if invalid */ export type CoercionFunction = (value: mixed) => ?V; /** * helper for raising an error based on a failure */ function raiseError(suffix: string, resultFailure: ?CheckFailure): empty { if (resultFailure != null) { const path = resultFailure.path.toString(); const message = resultFailure.message; throw err(`[refine.js (path=${path}, message=${message})]: ${suffix}`); } throw err(`[refine.js (null result)]: ${suffix}`); } /** * create a function to assert a value matches a checker, throwing otherwise * * For example, * * ``` * const assert = assertion(array(number())); * const value: Array = assert([1,2]); * * try { * // should throw with `Refine.js assertion failed: ...` * const invalid = assert('test'); * } catch { * } * ``` */ function assertion( checker: Checker, errorMessage: string = 'assertion error', ): AssertionFunction { return value => { const result = checker(value); return result.type === 'success' ? result.value : raiseError(errorMessage, result); }; } /** * create a CoercionFunction given a checker. * * Allows for null-coercing a value to a given type using a checker. Optionally * provide a callback which receives the full check * result object (e.g. for logging). * * Example: * * ```javascript * import {coercion, record, string} from 'refine'; * import MyLogger from './MyLogger'; * * const Person = record({ * name: string(), * hobby: string(), * }); * * const coerce = coercion(Person, result => MyLogger.log(result)); * * declare value: mixed; * * // ?Person * const person = coerce(value); * ``` */ function coercion( checker: Checker, onResult?: (CheckResult) => void, ): CoercionFunction { return value => { const result = checker(value); if (onResult != null) { onResult(result); } return result.type === 'success' ? result.value : null; }; } module.exports = { assertion, coercion, }; ================================================ FILE: packages/refine/Refine_Checkers.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * refine: type-refinement combinator library for checking mixed values * see wiki for more info: https://fburl.com/wiki/14q16qqy * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; /** * the result of failing to match a value to its expected type */ export type CheckFailure = $ReadOnly<{ type: 'failure', message: string, path: Path, }>; /** * the result of successfully matching a value to its expected type */ export type CheckSuccess<+V> = $ReadOnly<{ type: 'success', value: V, // if using `nullable` with the `nullWithWarningWhenInvalid` option, // failures will be appended here warnings: $ReadOnlyArray, }>; /** * the result of checking whether a type matches an expected value */ export type CheckResult<+V> = CheckSuccess | CheckFailure; /** * a function which checks if a given mixed value matches a type V, * returning the value if it does, otherwise a failure message. */ export type Checker<+V> = (value: mixed, path?: Path) => CheckResult; /** * utility type to extract flowtype matching checker structure * * ``` * const check = array(record({a: number()})); * * // equal to: type MyArray = $ReadOnlyArray<{a: number}>; * type MyArray = CheckerReturnType; * ``` */ export type CheckerReturnType = $Call< (checker: Checker) => T, CheckerFunction, >; /** * Path during checker traversal */ class Path { parent: ?Path; field: string; constructor(parent?: Path | null = null, field?: string = '') { this.parent = parent; this.field = field; } // Method to extend path by a field while traversing a container extend(field: string): Path { return new Path(this, field); } toString(): string { const pieces = []; let current: ?Path = this; while (current != null) { const {field, parent} = current; pieces.push(field); current = parent; } return pieces.reverse().join(''); } } /** * wrap value in an object signifying successful checking */ function success( value: V, warnings: $ReadOnlyArray, ): CheckSuccess { return {type: 'success', value, warnings}; } /** * indicate typecheck failed */ function failure(message: string, path: Path): CheckFailure { return {type: 'failure', message, path}; } /** * utility function for composing checkers */ function compose( checker: Checker, next: (success: CheckSuccess, path: Path) => CheckResult, ): Checker { return (value, path = new Path()) => { const result = checker(value, path); return result.type === 'failure' ? result : next(result, path); }; } module.exports = { Path, success, failure, compose, }; ================================================ FILE: packages/refine/Refine_ContainerCheckers.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * refine: type-refinement combinator library for checking mixed values * see wiki for more info: https://fburl.com/wiki/14q16qqy * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; import type {Checker, CheckFailure} from './Refine_Checkers'; const {Path, compose, failure, success} = require('./Refine_Checkers'); // Check that the provided value is a plain object and not an instance of some // other container type, built-in, or user class. // $FlowFixMe[missing-local-annot] function isPlainObject(value: T) { // $FlowIssue[method-unbinding] if (Object.prototype.toString.call(value) !== '[object Object]') { return false; } const prototype = Object.getPrototypeOf(value); return prototype === null || prototype === Object.prototype; } /** * checker to assert if a mixed value is an array of * values determined by a provided checker */ function array(valueChecker: Checker): Checker<$ReadOnlyArray> { return (value, path = new Path()) => { if (!Array.isArray(value)) { return failure('value is not an array', path); } const len = value.length; const out = new Array(len); const warnings: Array = []; for (let i = 0; i < len; i++) { const element = value[i]; const result = valueChecker(element, path.extend(`[${i}]`)); if (result.type === 'failure') { return failure(result.message, result.path); } out[i] = result.value; if (result.warnings.length !== 0) { warnings.push(...result.warnings); } } return success(out, warnings); }; } /** * checker to assert if a mixed value is a tuple of values * determined by provided checkers. Extra entries are ignored. * * Example: * ```jsx * const checker = tuple( number(), string() ); * ``` * * Example with optional trailing entry: * ```jsx * const checker = tuple( number(), voidable(string())); * ``` */ function tuple>>( ...checkers: Checkers ): Checker<$TupleMap(Checker) => T>> { return (value, path = new Path()) => { if (!Array.isArray(value)) { return failure('value is not an array', path); } const out = new Array(checkers.length); const warnings: Array = []; for (const [i, checker] of checkers.entries()) { const result = checker(value[i], path.extend(`[${i}]`)); if (result.type === 'failure') { return failure(result.message, result.path); } out[i] = result.value; if (result.warnings.length !== 0) { warnings.push(...result.warnings); } } return success(out, warnings); }; } /** * checker to assert if a mixed value is a string-keyed dict of * values determined by a provided checker */ function dict( valueChecker: Checker, ): Checker<$ReadOnly<{[key: string]: V}>> { return (value, path = new Path()) => { if (typeof value !== 'object' || value === null || !isPlainObject(value)) { return failure('value is not an object', path); } const out: {[key: string]: V} = {}; const warnings: Array = []; for (const [key, element] of Object.entries(value)) { const result = valueChecker(element, path.extend(`.${key}`)); if (result.type === 'failure') { return failure(result.message, result.path); } out[key] = result.value; if (result.warnings.length !== 0) { warnings.push(...result.warnings); } } return success(out, warnings); }; } // expose opaque version of optional property as public api, // forcing consistent usage of built-in `optional` to define optional properties export opaque type OptionalPropertyChecker<+T> = OptionalProperty; // not a public api, don't export at root class OptionalProperty<+T> { +checker: Checker; constructor(checker: Checker) { this.checker = checker; } } /** * checker which can only be used with `object` or `writablObject`. Marks a * field as optional, skipping the key in the result if it doesn't * exist in the input. * * @example * ```jsx * import {object, string, optional} from 'refine'; * * const checker = object({a: string(), b: optional(string())}); * assert(checker({a: 1}).type === 'success'); * ``` */ function optional<+T>(checker: Checker): OptionalPropertyChecker { return new OptionalProperty((value, path = new Path()) => { const result = checker(value, path); if (result.type === 'failure') { return { ...result, message: '(optional property) ' + result.message, }; } else { return result; } }); } /** * checker to assert if a mixed value is a fixed-property object, * with key-value pairs determined by a provided object of checkers. * Any extra properties in the input object values are ignored. * Class instances are not supported, use the custom() checker for those. * * Example: * ```jsx * const myObject = object({ * name: string(), * job: object({ * years: number(), * title: string(), * }), * }); * ``` * * Properties can be optional using `voidable()` or have default values * using `withDefault()`: * ```jsx * const customer = object({ * name: string(), * reference: voidable(string()), * method: withDefault(string(), 'email'), * }); * ``` */ function object< Checkers: $ReadOnly<{ [key: string]: Checker | OptionalPropertyChecker, }>, >( checkers: Checkers, ): Checker< $ReadOnly< $ObjMap(c: Checker | OptionalPropertyChecker) => T>, >, > { const checkerProperties: $ReadOnlyArray = Object.keys(checkers); return (value, path = new Path()) => { if (typeof value !== 'object' || value === null || !isPlainObject(value)) { return failure('value is not an object', path); } const out: {[string]: mixed} = {}; const warnings: Array = []; for (const key of checkerProperties) { const provided: Checker | OptionalProperty = checkers[key]; let check: Checker; let element: mixed; if (provided instanceof OptionalProperty) { check = provided.checker; if (!value.hasOwnProperty(key)) { continue; } element = value[key]; } else { check = provided; element = value.hasOwnProperty(key) ? value[key] : undefined; } const result = check(element, path.extend(`.${key}`)); if (result.type === 'failure') { return failure(result.message, result.path); } out[key] = result.value; if (result.warnings.length !== 0) { warnings.push(...result.warnings); } } return success(out, warnings); }; } /** * checker to assert if a mixed value is a Set type */ function set(checker: Checker): Checker<$ReadOnlySet> { return (value, path = new Path()) => { if (!(value instanceof Set)) { return failure('value is not a Set', path); } const out = new Set(); const warnings: Array = []; for (const item of value) { const result = checker(item, path.extend('[]')); if (result.type === 'failure') { return failure(result.message, result.path); } out.add(result.value); if (result.warnings.length) { warnings.push(...result.warnings); } } return success(out, warnings); }; } /** * checker to assert if a mixed value is a Map. */ function map( keyChecker: Checker, valueChecker: Checker, ): Checker<$ReadOnlyMap> { return (value, path = new Path()) => { if (!(value instanceof Map)) { return failure('value is not a Map', path); } const out = new Map(); const warnings: Array = []; for (const [k, v] of value.entries()) { const keyResult = keyChecker(k, path.extend(`[${k}] key`)); if (keyResult.type === 'failure') { return failure(keyResult.message, keyResult.path); } const valueResult = valueChecker(v, path.extend(`[${k}]`)); if (valueResult.type === 'failure') { return failure(valueResult.message, valueResult.path); } out.set(k, v); warnings.push(...keyResult.warnings, ...valueResult.warnings); } return success(out, warnings); }; } /** * identical to `array()` except the resulting value is a writable flow type. */ function writableArray(valueChecker: Checker): Checker> { return compose(array(valueChecker), ({value, warnings}) => success([...value], warnings), ); } /** * identical to `dict()` except the resulting value is a writable flow type. */ function writableDict( valueChecker: Checker, ): Checker<{[key: string]: V}> { return compose(dict(valueChecker), ({value, warnings}) => success({...value}, warnings), ); } /** * identical to `object()` except the resulting value is a writable flow type. */ function writableObject< Checkers: $ReadOnly<{ [key: string]: Checker | OptionalPropertyChecker, }>, >( checkers: Checkers, ): Checker< $ObjMap(c: Checker | OptionalPropertyChecker) => T>, > { return compose(object(checkers), ({value, warnings}) => success({...value}, warnings), ); } module.exports = { array, tuple, object, optional, dict, set, map, writableArray, writableDict, writableObject, }; ================================================ FILE: packages/refine/Refine_JSON.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Helper module for using refine on JSON objects. * see wiki for more info: https://fburl.com/wiki/14q16qqy * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; import type {Checker} from './Refine_Checkers'; const {assertion} = require('./Refine_API'); /** * function which takes a json string, parses it, * and matches it with a checker (returning null if no match) */ export type JSONParser = (?string) => T; /** * @param text A valid JSON string or null. * @param reviver A function that transforms the results. This function is called for each member of the object. * If a member contains nested objects, the nested objects are transformed before the parent object is. */ function tryParseJSONMixed( text: ?string, reviver?: (key: mixed, value: mixed) => mixed, ): mixed { if (text == null) { return null; } try { return (JSON.parse(text, reviver): mixed); } catch { return null; } } /** * creates a JSON parser which will error if the resulting value is invalid */ function jsonParserEnforced( checker: Checker, suffix?: string, ): JSONParser { const assertedChecker = assertion(checker, suffix ?? 'value is invalid'); return (rawJSON: ?string) => { return assertedChecker(tryParseJSONMixed(rawJSON ?? '')); }; } /** * convienience function to wrap a checker in a function * for easy JSON string parsing. */ function jsonParser(checker: Checker): JSONParser { return (rawJSON: ?string) => { const result = checker(tryParseJSONMixed(rawJSON)); return result.type === 'success' ? result.value : null; }; } module.exports = { jsonParserEnforced, jsonParser, }; ================================================ FILE: packages/refine/Refine_PrimitiveCheckers.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * refine: type-refinement combinator library for checking mixed values * see wiki for more info: https://fburl.com/wiki/14q16qqy * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; import type {Checker} from './Refine_Checkers'; const {Path, compose, failure, success} = require('./Refine_Checkers'); /** * a mixed (i.e. untyped) value */ function mixed(): Checker { return MIXED_CHECKER; } const MIXED_CHECKER: Checker = value => success(value, []); /** * checker to assert if a mixed value matches a literal value */ function literal( literalValue: T, ): Checker { const str = (value: T) => JSON.stringify(value); return (value, path = new Path()) => { return value === literalValue ? success(literalValue, []) : failure(`value is not literal ${str(literalValue) ?? 'void'}`, path); }; } /** * boolean value checker */ function bool(): Checker { // NOTE boolean is a reserved word so boolean() will not export properly in OSS return (value, path = new Path()) => typeof value === 'boolean' ? success(value, []) : failure('value is not a boolean', path); } /** * checker to assert if a mixed value is a number */ function number(): Checker { return (value, path = new Path()) => typeof value === 'number' ? success(value, []) : failure('value is not a number', path); } /** * Checker to assert if a mixed value is a string. * * Provide an optional RegExp template to match string against. */ function string(regex?: RegExp): Checker { return (value, path = new Path()) => { if (typeof value !== 'string') { return failure('value is not a string', path); } if (regex != null && !regex.test(value)) { return failure(`value does not match regex: ${regex.toString()}`, path); } return success(value, []); }; } /** * Checker to assert if a mixed value matches a union of string literals. * Legal values are provided as key/values in an object and may be translated by * providing different values in the object. * * For example: * ```jsx * const suitChecker = stringLiterals({ * heart: 'heart', * spade: 'spade', * club: 'club', * diamond: 'diamond', * }); * * const suit: 'heart' | 'spade' | 'club' | 'diamond' = assertion(suitChecker())(x); * ``` * * Strings can also be mapped to new values: * ```jsx * const placeholderChecker = stringLiterals({ * foo: 'spam', * bar: 'eggs', * }); * ``` * * It can be useful to have a single source of truth for your literals. To * only specify them once and use it for both the Flow union type and the * runtime checker you can use the following pattern: * ```jsx * const suits = { * heart: 'heart', * spade: 'spade', * club: 'club', * diamond: 'diamond', * }; * type Suit = $Values; * const suitChecker = stringLiterls(suits); * ``` */ function stringLiterals( enumValues: T, ): Checker<$Values> { return (value, path = new Path()) => { if (typeof value !== 'string') { return failure('value must be a string', path); } const out = enumValues[value]; if (out == null) { return failure( `value is not one of ${Object.keys(enumValues).join(', ')}`, path, ); } return success(out, []); }; } /* * Checker to assert if a mixed value matches a string | number value of an * object. This is useful for non Flow enums, in the form of {[string]: string} * or {[string]: number}. * * For example: * ```jsx * const MyEnum = {foo: 'bar', baz: 'bat'}; * const enumObjectChecker = enumObject(MyEnum); * const value: 'bar' | 'bat' = assertion(enumObjectChecker())(x); * ``` */ function enumObject( enumObj: T, ): Checker<$Values> { const enumValues = Object.keys(enumObj).reduce( // $FlowFixMe[invalid-computed-prop] (res, key) => Object.assign(res, {[enumObj[key]]: enumObj[key]}), {}, ); const stringLiteralsChecker = stringLiterals(enumValues); return (rawValue, path = new Path()) => { const value = typeof rawValue === 'number' ? rawValue.toString() : rawValue; const result = stringLiteralsChecker(value, path); if (result.type === 'success' && typeof result.value !== typeof rawValue) { return failure('input must be the same type as the enum values', path); } return result; }; } /** * checker to assert if a mixed value is a Date object * * For example: * ```jsx * const dateChecker = date(); * * assertion(dateChecker())(new Date()); * ``` */ function date(): Checker { return (value, path = new Path()) => { if (!(value instanceof Date)) { return failure('value is not a date', path); } if (isNaN(value)) { return failure('invalid date', path); } return success(value, []); }; } /** * checker to coerce a date string to a Date object. This is useful for input * that was from a JSON encoded `Date` object. * * For example: * ```jsx * const jsonDateChecker = coerce(jsonDate({encoding: 'string'})); * * jsonDateChecker('October 26, 1985'); * jsonDateChecker('1955-11-05T07:00:00.000Z'); * jsonDateChecker(JSON.stringify(new Date())); * ``` */ function jsonDate(): Checker { return compose(string(), ({value, warnings}, path) => { const parsedDate = new Date(value); return Number.isNaN(parsedDate) ? failure('value is not valid date string', path) : success(parsedDate, warnings); }); } module.exports = { mixed, literal, bool, number, string, stringLiterals, date, jsonDate, enumObject, }; ================================================ FILE: packages/refine/Refine_UtilityCheckers.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * refine: type-refinement combinator library for checking mixed values * see wiki for more info: https://fburl.com/wiki/14q16qqy * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; import type {Checker, CheckFailure, CheckResult} from './Refine_Checkers'; const {Path, compose, failure, success} = require('./Refine_Checkers'); /** * Cast the type of a value after passing a given checker * * For example: * * ```javascript * import {string, asType} from 'refine'; * * opaque type ID = string; * * const IDChecker: Checker = asType(string(), s => (s: ID)); * ``` */ function asType(checker: Checker
, cast: A => B): Checker { return compose(checker, ({value, warnings}) => success(cast(value), warnings), ); } function unionFailure( message: string, path: Path, failures: $ReadOnlyArray, ): CheckFailure { return failure( `${message}: ${failures.map(f => f.message).join(', ')}`, path, ); } /** * checker which asserts the value matches * at least one of the two provided checkers */ function or(aChecker: Checker, bChecker: Checker): Checker { return (value, path = new Path()) => { const a = aChecker(value, path); if (a.type === 'success') { return success(a.value, a.warnings); } const b = bChecker(value, path); if (b.type === 'success') { return success(b.value, b.warnings); } return unionFailure('value did not match any types in or()', path, [a, b]); }; } /** * checker which asserts the value matches * at least one of the provided checkers * * NOTE: the reason `union` and `or` both exist is that there is a bug * within flow that prevents extracting the type from `union` without * annotation -- see https://fburl.com/gz7u6401 */ function union(...checkers: $ReadOnlyArray>): Checker { return (value, path = new Path()) => { const failures = []; for (const checker of checkers) { const result = checker(value, path); if (result.type === 'success') { return success(result.value, result.warnings); } failures.push(result); } return unionFailure( 'value did not match any types in union', path, failures, ); }; } /** * Provide a set of checkers to check in sequence to use the first match. * This is similar to union(), but all checkers must have the same type. * * This can be helpful for supporting backward compatibility. For example the * following loads a string type, but can also convert from a number as the * previous version or pull from an object as an even older version: * * ```jsx * const backwardCompatibilityChecker: Checker = match( * string(), * asType(number(), num => `${num}`), * asType(object({num: number()}), obj => `${obj.num}`), * ); * ``` */ function match(...checkers: $ReadOnlyArray>): Checker { return union(...checkers); } /** * wraps a given checker, making the valid value nullable * * By default, a value passed to nullable must match the checker spec exactly * when it is not null, or it will fail. * * passing the `nullWithWarningWhenInvalid` enables gracefully handling invalid * values that are less important -- if the provided checker is invalid, * the new checker will return null. * * For example: * * ```javascript * import {nullable, record, string} from 'refine'; * * const Options = object({ * // this must be a non-null string, * // or Options is not valid * filename: string(), * * // if this field is not a string, * // it will be null and Options will pass the checker * description: nullable(string(), { * nullWithWarningWhenInvalid: true, * }) * }) * * const result = Options({filename: 'test', description: 1}); * * invariant(result.type === 'success'); * invariant(result.value.description === null); * invariant(result.warnings.length === 1); // there will be a warning * ``` */ function nullable( checker: Checker, options?: $ReadOnly<{ // if this is true, the checker will not fail // validation if an invalid value is provided, instead // returning null and including a warning as to the invalid type. nullWithWarningWhenInvalid?: boolean, }>, ): Checker { const {nullWithWarningWhenInvalid = false} = options ?? {}; return (value, parentPath = new Path()): CheckResult => { if (value == null) { return success(value, []); } const result = checker(value, parentPath); if (result.type === 'success') { return success(result.value, result.warnings); } // if this is enabled, "succeed" the checker with a warning // if the non-null value does not match expectation if (nullWithWarningWhenInvalid) { return success(null, [result]); } const {message, path} = result; return failure(message, path); }; } /** * wraps a given checker, making the valid value voidable * * By default, a value passed to voidable must match the checker spec exactly * when it is not undefined, or it will fail. * * passing the `undefinedWithWarningWhenInvalid` enables gracefully handling invalid * values that are less important -- if the provided checker is invalid, * the new checker will return undefined. * * For example: * * ```javascript * import {voidable, record, string} from 'refine'; * * const Options = object({ * // this must be a string, or Options is not valid * filename: string(), * * // this must be a string or undefined, * // or Options is not valid * displayName: voidable(string()), * * // if this field is not a string, * // it will be undefined and Options will pass the checker * description: voidable(string(), { * undefinedWithWarningWhenInvalid: true, * }) * }) * * const result = Options({filename: 'test', description: 1}); * * invariant(result.type === 'success'); * invariant(result.value.description === undefined); * invariant(result.warnings.length === 1); // there will be a warning * ``` */ function voidable( checker: Checker, options?: $ReadOnly<{ // if this is true, the checker will not fail // validation if an invalid value is provided, instead // returning undefined and including a warning as to the invalid type. undefinedWithWarningWhenInvalid?: boolean, }>, ): Checker { const {undefinedWithWarningWhenInvalid = false} = options ?? {}; return (value, parentPath = new Path()): CheckResult => { if (value === undefined) { return success(undefined, []); } const result = checker(value, parentPath); if (result.type === 'success') { return success(result.value, result.warnings); } // if this is enabled, "succeed" the checker with a warning // if the non-void value does not match expectation if (undefinedWithWarningWhenInvalid) { return success(undefined, [result]); } const {message, path} = result; return failure(message, path); }; } /** * a checker that provides a withDefault value if the provided value is nullable. * * For example: * ```jsx * const objPropertyWithDefault = object({ * foo: withDefault(number(), 123), * }); * ``` * Both `{}` and `{num: 123}` will refine to `{num: 123}` */ function withDefault(checker: Checker, fallback: T): Checker { return (value, path = new Path()) => { if (value == null) { return success(fallback, []); } const result = checker(value, path); return result.type === 'failure' || result.value != null ? result : success(fallback, []); }; } /** * wraps a checker with a logical constraint. * * Predicate function can return either a boolean result or * a tuple with a result and message * * For example: * * ```javascript * import {number, constraint} from 'refine'; * * const evenNumber = constraint( * number(), * n => n % 2 === 0 * ); * * const passes = evenNumber(2); * // passes.type === 'success'; * * const fails = evenNumber(1); * // fails.type === 'failure'; * ``` */ function constraint( checker: Checker, predicate: T => boolean | [boolean, string], ): Checker { return compose(checker, ({value, warnings}, path) => { const result = predicate(value); const [passed, message] = typeof result === 'boolean' ? [result, 'value failed constraint check'] : result; return passed ? success(value, warnings) : failure(message, path); }); } /** * wrapper to allow for passing a lazy checker value. This enables * recursive types by allowing for passing in the returned value of * another checker. For example: * * ```javascript * const user = object({ * id: number(), * name: string(), * friends: array(lazy(() => user)) * }); * ``` * * Example of array with arbitrary nesting depth: * ```jsx * const entry = or(number(), array(lazy(() => entry))); * const nestedArray = array(entry); * ``` */ function lazy(getChecker: () => Checker): Checker { return (value, path = new Path()) => { const checker = getChecker(); return checker(value, path); }; } /** * helper to create a custom checker from a provided function. * If the function returns a non-nullable value, the checker succeeds. * * ```jsx * const myClassChecker = custom(x => x instanceof MyClass ? x : null); * ``` * * Nullable custom types can be created by composing with `nullable()` or * `voidable()` checkers: * * ```jsx * const maybeMyClassChecker = * nullable(custom(x => x instanceof MyClass ? x : null)); * ``` */ function custom( checkValue: (value: mixed) => ?T, failureMessage: string = `failed to return non-null from custom checker.`, ): Checker { return (value, path = new Path()) => { try { const checked = checkValue(value); return checked != null ? success(checked, []) : failure(failureMessage, path); } catch (error) { return failure(error.message, path); } }; } module.exports = { or, union, match, nullable, voidable, withDefault, constraint, asType, lazy, custom, }; ================================================ FILE: packages/refine/Refine_index.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; export type {AssertionFunction, CoercionFunction} from './Refine_API'; export type {JSONParser} from './Refine_JSON'; export type { Checker, CheckFailure, CheckResult, CheckSuccess, CheckerReturnType, } from './Refine_Checkers'; export type {OptionalPropertyChecker} from './Refine_ContainerCheckers'; const {assertion, coercion} = require('./Refine_API'); const {Path} = require('./Refine_Checkers'); const { array, dict, map, object, optional, set, tuple, writableArray, writableDict, writableObject, } = require('./Refine_ContainerCheckers'); const {jsonParser, jsonParserEnforced} = require('./Refine_JSON'); const { bool, date, enumObject, jsonDate, literal, mixed, number, string, stringLiterals, } = require('./Refine_PrimitiveCheckers'); const { asType, constraint, custom, lazy, match, nullable, or, union, voidable, withDefault, } = require('./Refine_UtilityCheckers'); module.exports = { // API assertion, coercion, jsonParser, jsonParserEnforced, Path, // Checkers - Primitives mixed, literal, bool, number, string, stringLiterals, enumObject, date, jsonDate, // Checkers - Utility asType, or, union, match, nullable, voidable, withDefault, constraint, lazy, custom, // Checkers - Containers array, tuple, dict, object, optional, set, map, writableArray, writableDict, writableObject, }; ================================================ FILE: packages/refine/__tests__/Refine-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; import type {CheckResult} from '../Refine_Checkers'; const {assertion, coercion} = require('../Refine_API'); const {array} = require('../Refine_ContainerCheckers'); const {date, number, string} = require('../Refine_PrimitiveCheckers'); const {or} = require('../Refine_UtilityCheckers'); const invariant = require('recoil-shared/util/Recoil_invariant'); describe('assertion', () => { it('should not throw if value is valid', () => { const assert = assertion(array(or(number(), string()))); const value = assert([1, '2', 3, 4]); expect(value).toEqual([1, '2', 3, 4]); }); it('should throw if value is invalid', () => { const assert = assertion(array(or(number(), string()))); expect(() => assert([1, '2', true, 4])).toThrow(); }); }); describe('coercion', () => { it('should return a value when valid', () => { const coerce = coercion(date()); const d = new Date(); expect(coerce(d)).toBe(d); }); it('should return null when invalid', () => { const coerce = coercion(number()); const d = new Date(); expect(coerce(d)).toBe(null); }); it('should correctly call calback with result', () => { let callbackResult: ?CheckResult = null; const coerce = coercion(date(), result => { callbackResult = result; }); const d = new Date(); expect(coerce(d)).toBe(d); invariant(callbackResult != null, 'should be set'); invariant(callbackResult.type == 'success', 'should succeed'); }); }); ================================================ FILE: packages/refine/__tests__/Refine_Containers-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; const {coercion} = require('../Refine_API'); const { array, dict, map, object, optional, set, tuple, writableArray, writableDict, writableObject, } = require('../Refine_ContainerCheckers'); const {number, string} = require('../Refine_PrimitiveCheckers'); const {lazy, nullable, or, voidable} = require('../Refine_UtilityCheckers'); const invariant = require('recoil-shared/util/Recoil_invariant'); describe('array', () => { it('should succeed in coercing correct array', () => { const coerce = array(number()); const result = coerce([1, 2, 3]); invariant(result.type === 'success', 'should succeed'); expect(result.value).toEqual([1, 2, 3]); }); it('should succeed in coercing correct array with null', () => { const coerce = array(nullable(number())); const result = coerce([1, 2, null]); invariant(result.type === 'success', 'should succeed'); expect(result.value).toEqual([1, 2, null]); }); it('nested array', () => { const coerce = coercion(array(array(number()))); expect(coerce([])).toEqual([]); expect(coerce([1, 2])).toEqual(null); expect(coerce([[1, 2]])).toEqual([[1, 2]]); expect( coerce([ [1, 2], [3, 4], ]), ).toEqual([ [1, 2], [3, 4], ]); expect( coerce([ [1, 2], ['str', 4], ]), ).toEqual(null); expect(coerce([[[3]]])).toEqual(null); }); it('arbitrary depth nested array', () => { // $FlowFixMe[recursive-definition] // $FlowFixMe[underconstrained-implicit-instantiation] const check = or(number(), array(lazy(() => check))); // $FlowFixMe[underconstrained-implicit-instantiation] const coerce = coercion(array(check)); expect(coerce(0)).toEqual(null); expect(coerce([])).toEqual([]); expect(coerce([1, 1])).toEqual([1, 1]); expect(coerce([1, 'str'])).toEqual(null); expect(coerce([1, [2, 2]])).toEqual([1, [2, 2]]); expect(coerce([1, [[3], [3, [4]]]])).toEqual([1, [[3], [3, [4]]]]); }); it('should succeed in not coercing correct array with nullable element', () => { const coerce = array(number()); const result = coerce([1, null, 3]); invariant(result.type === 'failure', 'should fail'); expect(result.path.toString()).toEqual('[1]'); }); it('should succeed in not coercing correct array with invalid element', () => { const coerce = array(number()); const result = coerce([1, 2, '3']); invariant(result.type === 'failure', 'should fail'); expect(result.path.toString()).toEqual('[2]'); }); it('should succeed when using writable version, with correct type', () => { const coerce = writableArray(number()); const result = coerce([1, 2, 3]); invariant(result.type === 'success', 'should succeed'); result.value[0] = 3; }); }); describe('tuple', () => { it('mixed type tuples', () => { const coerce = coercion(tuple(number(), string())); expect(coerce(false)).toEqual(null); expect(coerce([])).toEqual(null); expect(coerce([1])).toEqual(null); expect(coerce([1, 'str'])).toEqual([1, 'str']); }); it('nested tuples', () => { const coerce = coercion(tuple(number(), tuple(number(), string()))); expect(coerce([])).toEqual(null); expect(coerce([1, 'str'])).toEqual(null); expect(coerce([1, [1, 'str']])).toEqual([1, [1, 'str']]); }); it('optional trailing entries', () => { const coerce = coercion(tuple(number(), voidable(string()))); expect(coerce([])).toEqual(null); expect(coerce([1])).toEqual([1, undefined]); expect(coerce([1, 'str'])).toEqual([1, 'str']); expect(coerce([1, 2])).toEqual(null); expect(coerce([1, 'str', 3])).toEqual([1, 'str']); }); }); describe('dict', () => { it('should successfully parse a dictionary', () => { const coerce = dict(object({a: number(), b: number()})); const result = coerce({test: {a: 1, b: 2}, other: {a: 1, b: 2}}); invariant(result.type === 'success', 'should succeed'); }); it("should fail if the values don't match", () => { const coerce = dict(object({a: number(), b: number()})); const result = coerce({test: {a: 1, b: 2}, other: {c: 1, d: 2}}); invariant(result.type === 'failure', 'should fail'); expect(result.path.toString()).toEqual('.other.a'); }); it('should succeed when using writable version, with correct type', () => { const coerce = writableDict(number()); const result = coerce({a: 1, b: 2}); invariant(result.type === 'success', 'should succeed'); // should flow check as writable result.value.a = 3; }); it('only accept plain objects', () => { class MyClass {} const coerce = coercion(dict(number())); expect(coerce({})).toEqual({}); expect(coerce(new Date())).toEqual(null); expect(coerce(new Map())).toEqual(null); expect(coerce(new Set())).toEqual(null); expect(coerce(new MyClass())).toEqual(null); }); }); describe('object', () => { it('should succeed in parsing basic object', () => { const coerce = object({ a: number(), b: string(), }); const result = coerce({a: 1, b: 'test'}); invariant(result.type === 'success', 'should succeed'); // typecheck assertion const a: number = result.value.a; const b: string = result.value.b; expect(a).toEqual(1); expect(b).toEqual('test'); }); it('should allow optional props', () => { const coerce = object({ a: number(), b: string(), c: optional(number()), }); const result = coerce({a: 1, b: 'test'}); invariant(result.type === 'success', 'should succeed'); // eslint-disable-next-line no-unused-vars const n: ?number = result.value.c; // typecheck assertion const a: number = result.value.a; const b: string = result.value.b; expect(a).toEqual(1); expect(b).toEqual('test'); const result2 = coerce({a: 1, b: 'test', c: 2}); invariant(result2.type === 'success', 'should succeed'); const result3 = coerce({a: 1, b: 'test', c: undefined}); invariant(result3.type === 'failure', 'should fail'); }); it('should succeed in parsing nested objects', () => { const coerce = object({ name: string(), job: object({ years: number(), title: string(), }), }); const result = coerce({name: 'Elsa', job: {title: 'Engineer', years: 3}}); invariant(result.type === 'success', 'should succeed'); expect(result.value.job.title).toEqual('Engineer'); }); it('extra properties are ignored', () => { const coerce = coercion( object({ name: string(), }), ); expect(coerce({})).toEqual(null); expect(coerce({name: 'Elsa'})).toEqual({name: 'Elsa'}); expect(coerce({name: 'Elsa', sister: 'Anna'})).toEqual({name: 'Elsa'}); }); it('optional properties', () => { const coerce = coercion( object({ name: string(), ref: voidable(string()), }), ); expect(coerce({})).toEqual(null); expect(coerce({name: 'Elsa'})).toEqual({name: 'Elsa'}); expect(coerce({name: 'Elsa', ref: 'Anna'})).toEqual({ name: 'Elsa', ref: 'Anna', }); expect(coerce({name: 'Elsa', extra: 'foo'})).toEqual({name: 'Elsa'}); }); it('should fail in parsing nested objects with invalid property', () => { const coerce = object({ name: string(), job: object({ years: number(), title: string(), }), }); const result = coerce({ name: 'Elsa', job: {title: 'Engineer', years: 'woops'}, }); invariant(result.type === 'failure', 'should succeed'); expect(result.path.toString()).toEqual('.job.years'); }); it('should succeed when using writable version, with correct type', () => { const coerce = writableObject({ name: string(), job: object({ years: number(), title: string(), }), }); const result = coerce({name: 'Elsa', job: {title: 'Engineer', years: 3}}); invariant(result.type === 'success', 'should succeed'); // should flow check as writable result.value.name = 'MechaElsa'; }); it('only accept plain objects', () => { class MyClass {} const coerce = coercion(object({})); expect(coerce({})).toEqual({}); expect(coerce(new Date())).toEqual(null); expect(coerce(new Map())).toEqual(null); expect(coerce(new Set())).toEqual(null); expect(coerce(new MyClass())).toEqual(null); }); }); describe('set', () => { it('coerce sets', () => { const coerce = coercion(set(number())); expect(coerce(false)).toEqual(null); expect(coerce([1, 2])).toEqual(null); // $FlowFixMe[missing-empty-array-annot] expect(coerce(new Set([]))).toEqual(new Set([])); expect(coerce(new Set([1, 2]))).toEqual(new Set([1, 2])); expect(coerce(new Set([1, 2, 2]))).toEqual(new Set([1, 2])); expect(coerce(new Set([1, 2, 'str']))).toEqual(null); }); it('nested sets', () => { const coerce = coercion(set(set(number()))); expect(coerce(null)).toEqual(null); expect(coerce(new Set([1]))).toEqual(null); expect(coerce(new Set([new Set()]))).toEqual(new Set([new Set()])); expect(coerce(new Set([new Set([1])]))).toEqual(new Set([new Set([1])])); }); }); describe('map', () => { it('coerce maps', () => { const coerce = coercion(map(string(), number())); expect(coerce(false)).toEqual(null); expect(coerce(new Map())).toEqual(new Map()); expect(coerce(new Map([['foo', 'bar']]))).toEqual(null); expect(coerce(new Map([['foo', 123]]))).toEqual(new Map([['foo', 123]])); expect( coerce( new Map([ ['foo', 123], ['bar', 456], ]), ), ).toEqual( new Map([ ['foo', 123], ['bar', 456], ]), ); }); it('nested maps', () => { const coerce = coercion(map(string(), map(string(), number()))); expect(coerce(new Map())).toEqual(new Map()); expect(coerce(new Map([['foo', new Map()]]))).toEqual( new Map([['foo', new Map()]]), ); expect(coerce(new Map([['foo', new Map([['bar', 123]])]]))).toEqual( new Map([['foo', new Map([['bar', 123]])]]), ); }); it('map with non-string keys', () => { const numberKey = coercion(map(number(), string())); expect(numberKey(new Map([['foo', 'bar']]))).toEqual(null); expect(numberKey(new Map([[123, 'bar']]))).toEqual(new Map([[123, 'bar']])); const objKey = coercion(map(object({str: string()}), number())); expect(objKey(new Map([[{str: 'foo'}, 123]]))).toEqual( new Map([[{str: 'foo'}, 123]]), ); }); }); ================================================ FILE: packages/refine/__tests__/Refine_JSON-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; const {object} = require('../Refine_ContainerCheckers'); const {jsonParser, jsonParserEnforced} = require('../Refine_JSON'); const {bool, number, string} = require('../Refine_PrimitiveCheckers'); const {nullable} = require('../Refine_UtilityCheckers'); const invariant = require('recoil-shared/util/Recoil_invariant'); describe('json', () => { it('should correctly parse valid json', () => { const parse = jsonParser( object({a: string(), b: nullable(number()), c: bool()}), ); const result = parse('{"a": "test", "c": true}'); expect(result).toEqual({a: 'test', b: undefined, c: true}); invariant(result != null, 'should not be null'); expect(result.a).toEqual('test'); }); it('should error on null_or_invalid if desired', () => { const MESSAGE = 'IS_NULL_OR_INVALID'; const parse = jsonParserEnforced( object({a: string(), b: nullable(number()), c: bool()}), MESSAGE, ); expect(parse('{"a": "a", "c": true}')).toEqual({ a: 'a', b: undefined, c: true, }); expect(() => parse('{"a": "a", "d": true}')).toThrow(new RegExp(MESSAGE)); expect(() => parse(null)).toThrow(new RegExp(MESSAGE)); }); }); ================================================ FILE: packages/refine/__tests__/Refine_Primitives-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; import type {CheckerReturnType} from '../Refine_Checkers'; import type {CoercionFunction} from 'Refine_API'; import type {Checker} from 'Refine_Checkers'; const {assertion, coercion} = require('../Refine_API'); const { bool, date, enumObject, jsonDate, literal, number, string, stringLiterals, } = require('../Refine_PrimitiveCheckers'); const {asType} = require('../Refine_UtilityCheckers'); const invariant = require('recoil-shared/util/Recoil_invariant'); describe('literal', () => { it('should correctly parse exact string', () => { const coerce = literal('test'); const result = coerce('test'); invariant(result.type === 'success', 'should succeed'); invariant(result.value === 'test', 'should succeed'); }); it('should fail parse different string', () => { const coerce = literal('test'); const result = coerce('other'); invariant(result.type === 'failure', 'should fail'); }); it('should correctly parse exact number', () => { const coerce = literal(1); const result = coerce(1); invariant(result.type === 'success', 'should succeed'); invariant(result.value === 1, 'should succeed'); }); it('should correctly parse exact boolean', () => { const coerce = literal(true); const result = coerce(true); invariant(result.type === 'success', 'should succeed'); invariant(result.value === true, 'should succeed'); }); it('parse null', () => { const coerce = coercion(asType(literal(null), () => true)); expect(coerce(false)).toEqual(null); expect(coerce(null)).toEqual(true); expect(coerce(undefined)).toEqual(null); }); it('parse undefined', () => { const coerce = coercion(literal(undefined)); expect(coerce(false)).toEqual(null); expect(coerce(null)).toEqual(null); expect(coerce(undefined)).toEqual(undefined); }); }); describe('bool', () => { it('should correctly parse true', () => { const coerce = bool(); const result = coerce(true); invariant(result.type === 'success', 'should succeed'); // test type extraction type Result = CheckerReturnType; const test: Result = true; invariant(result.value === test, 'value should be true'); }); it('should correctly parse false', () => { const coerce = bool(); const result = coerce(false); invariant(result.type === 'success', 'should succeed'); invariant(result.value === false, 'value should be false'); }); it('should correctly parse invalid', () => { const coerce = bool(); const result = coerce(1); invariant(result.type === 'failure', 'should fail'); }); }); describe('number', () => { it('should correctly parse number', () => { const coerce = number(); const result = coerce(1); invariant(result.type === 'success', 'should succeed'); invariant(result.value === 1, 'value should be true'); }); it('should correctly parse invalid', () => { const coerce = number(); const result = coerce(true); invariant(result.type === 'failure', 'should fail'); }); }); describe('string', () => { it('should correctly parse number', () => { const coerce = string(); const result = coerce('test'); invariant(result.type === 'success', 'should succeed'); invariant(result.value === 'test', 'value should be true'); }); it('should correctly parse invalid', () => { const coerce = string(); const result = coerce(null); invariant(result.type === 'failure', 'should fail'); }); it('match regex', () => { const coerce = string(/^users?$/); expect(coerce('user').type).toBe('success'); expect(coerce('users').type).toBe('success'); expect(coerce('busers').type).toBe('failure'); }); }); describe('stringLiterals', () => { it('parse string literals', () => { const coerce = coercion(stringLiterals({foo: 'foo', bar: 'bar'})); expect(coerce(false)).toEqual(null); expect(coerce('fail')).toEqual(null); expect(coerce('foo')).toEqual('foo'); // Confirm it can be typed as a union of string literals const _x: null | void | 'foo' | 'bar' = coerce('foo'); }); it('parse string literals with transformation', () => { const coerce = coercion(stringLiterals({foo: 'eggs', bar: 'spam'})); expect(coerce(false)).toEqual(null); expect(coerce('fail')).toEqual(null); expect(coerce('foo')).toEqual('eggs'); // Confirm it can be typed as a union of string literals const _x: null | void | 'eggs' | 'spam' = coerce('foo'); }); it('will cast value into string literal union type', () => { const food = Object.freeze({foo: 'eggs', bar: 'spam'}); const assert = assertion(stringLiterals(food)); const value = assert('foo'); // $FlowExpectedError - it is expected to fail ('invalid': typeof value); }); }); type ExampleEnumType = 'baz' | 'bat'; const ExampleEnum = Object.freeze({foo: 'baz', bar: 'bat'}); describe('enumObject', () => { it('parse strings', () => { const checker: Checker = enumObject(ExampleEnum); const coerce: CoercionFunction = coercion(checker); expect(coerce(false)).toEqual(null); expect(coerce('fail')).toEqual(null); expect(coerce('foo')).toEqual(null); expect(coerce(1)).toEqual(null); expect(coerce('baz')).toEqual('baz'); // Confirm it can be typed as a union of string literals const _x: null | void | 'baz' | 'bat' = coerce('baz'); type ExampleEnumCheckerReturnType = CheckerReturnType; ('baz': ExampleEnumCheckerReturnType); // $FlowExpectedError ('bad string': ExampleEnumCheckerReturnType); }); it('parse numbers', () => { const coerce = coercion(enumObject({'1': 3, '2': 4})); expect(coerce(false)).toEqual(null); expect(coerce('fail')).toEqual(null); expect(coerce('2')).toEqual(null); expect(coerce('3')).toEqual(null); expect(coerce(2)).toEqual(null); expect(coerce(3)).toEqual(3); // Confirm it can be typed as a union of string literals const _x: null | void | 3 | 4 = coerce(4); }); }); describe('date', () => { it('should correctly parse date', () => { const coerce = date(); const d = new Date(); const result = coerce(d); invariant(result.type === 'success', 'should succeed'); invariant(result.value === d, 'value should be true'); }); it('should correctly parse invalid', () => { const coerce = date(); const result = coerce(true); invariant(result.type === 'failure', 'should fail'); }); it('should fail an invalid date', () => { const coerce = coercion(date()); const myDate = new Date(); expect(coerce(myDate)).toEqual(myDate); expect(coerce(new Date('invalid'))).toEqual(null); }); }); describe('jsonDate', () => { it('should parse date strings', () => { const coerce = coercion(jsonDate()); expect(coerce('Oct 26, 1985')).toEqual(new Date('Oct 26, 1985')); expect(coerce('1955-11-05T07:00:00.000Z')).toEqual( new Date('1955-11-05T07:00:00.000Z'), ); }); }); ================================================ FILE: packages/refine/__tests__/Refine_Utilities-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall monitoring_interfaces */ 'use strict'; import type {Checker} from '../Refine_Checkers'; const {coercion} = require('../Refine_API'); const {array, object} = require('../Refine_ContainerCheckers'); const {bool, number, string} = require('../Refine_PrimitiveCheckers'); const { asType, constraint, custom, lazy, match, nullable, or, union, voidable, withDefault, } = require('../Refine_UtilityCheckers'); const invariant = require('recoil-shared/util/Recoil_invariant'); // opaque flow test opaque type ID = string; (asType(string(), id => (id: ID)): Checker); describe('asType', () => { it('upgrade number to string', () => { const coerce = coercion(asType(number(), num => `${num}`)); expect(coerce(false)).toBe(null); expect(coerce('str')).toBe(null); expect(coerce(123)).toBe('123'); }); }); describe('or', () => { it('should match value when correct', () => { const parser = or(string(), number()); const result = parser('test'); invariant(result.type === 'success', 'should succeed'); expect(result.value).toEqual('test'); const second = parser(1); invariant(second.type === 'success', 'should succeed'); expect(second.value).toEqual(1); }); it('should not match if value is not correct', () => { const parser = or(string(), number()); const result = parser(true); invariant(result.type === 'failure', 'should fail'); expect(result.message).toEqual( 'value did not match any types in or(): value is not a string, value is not a number', ); }); }); describe('union', () => { it('should match value when correct', () => { const parser = union(string(), number(), bool()); const result = parser('test'); invariant(result.type === 'success', 'should succeed'); expect(result.value).toEqual('test'); const second = parser(1); invariant(second.type === 'success', 'should succeed'); expect(second.value).toEqual(1); const third = parser(true); invariant(third.type === 'success', 'should succeed'); expect(third.value).toEqual(true); }); it('should not match if value is not correct', () => { const parser = union(string(), number()); const result = parser(true); invariant(result.type === 'failure', 'should fail'); expect(result.message).toEqual( 'value did not match any types in union: value is not a string, value is not a number', ); }); }); describe('match', () => { it('upgrade to string from various types', () => { const coerce = coercion( match( string(), asType(number(), num => `${num}`), asType(object({str: string()}), obj => obj.str), asType(object({num: number()}), obj => `${obj.num}`), ), ); expect(coerce(false)).toBe(null); expect(coerce('str')).toBe('str'); expect(coerce(123)).toBe('123'); expect(coerce({num: 123})).toBe('123'); expect(coerce({str: 'str'})).toBe('str'); expect(coerce({num: 123, str: 'str'})).toBe('str'); expect(coerce({foo: 'bar'})).toBe(null); }); }); describe('nullable', () => { it('should correctly parse nullable value when null', () => { const coerce = nullable(string()); const result = coerce(null); invariant(result.type === 'success', 'should succeed'); invariant(result.value === null, 'value should be true'); }); it('should correctly parse value when undefined is provided', () => { const coerce = nullable(string()); const result = coerce(undefined); invariant(result.type === 'success', 'should succeed'); invariant(result.value === undefined, 'value should be true'); }); it('should correctly parse nullable value when not null', () => { const coerce = nullable(string()); const result = coerce('test'); invariant(result.type === 'success', 'should succeed'); invariant(result.value === 'test', 'value should be true'); }); it('should correctly parse invalid', () => { const coerce = nullable(string()); const result = coerce(1); invariant(result.type === 'failure', 'should fail'); }); it('should validate the value, but return null if invalid', () => { const coerce = nullable(string(), { nullWithWarningWhenInvalid: true, }); const result = coerce(1); invariant(result.type === 'success', 'should succeed'); invariant(result.value === null, 'value should be true'); expect(result.warnings?.length).toBe(1); }); it('should pass along warnings in child result', () => { const coerce = object({ field: nullable( object({ child: nullable(string(), { nullWithWarningWhenInvalid: true, }), }), ), }); const result = coerce({field: {child: 1}}); invariant(result.type === 'success', 'should succeed'); expect(result.warnings?.length).toBe(1); const warning = result.warnings?.[0]; invariant(warning != null, 'should have warning'); expect(warning.path.toString()).toEqual('.field.child'); }); it('should propogate warnings correctly when using `nullWithWarningWhenInvalid`', () => { const nullConfig = { nullWithWarningWhenInvalid: true, }; const check = object({ a: string(), b: object({ c: nullable(number(), nullConfig), d: object({ e: bool(), f: nullable(bool(), nullConfig), }), }), }); const result = check({ a: 'test', b: { c: 'invalid', d: { e: true, f: 'invalid', }, }, }); invariant(result.type === 'success', 'should succeed to validate'); expect(result.warnings?.[0]?.path.toString()).toEqual('.b.c'); expect(result.warnings?.[1]?.path.toString()).toEqual('.b.d.f'); }); }); describe('voidable', () => { it('should correctly parse value when undefined is provided', () => { const coerce = voidable(string()); const result = coerce(undefined); invariant(result.type === 'success', 'should succeed'); invariant(result.value === undefined, 'value should be true'); }); it('should correctly parse value when non-void value is provided', () => { const coerce = voidable(string()); const result = coerce('test'); invariant(result.type === 'success', 'should succeed'); invariant(result.value === 'test', 'value should be true'); }); it('should correctly parse invalid', () => { const coerce = voidable(string()); const result = coerce(1); invariant(result.type === 'failure', 'should fail'); }); it('should validate the value, but return undefined if invalid', () => { const coerce = voidable(string(), { undefinedWithWarningWhenInvalid: true, }); const result = coerce(1); invariant(result.type === 'success', 'should succeed'); invariant(result.value === undefined, 'value should be true'); expect(result.warnings?.length).toBe(1); }); it('should pass along warnings in child result', () => { const coerce = object({ field: voidable( object({ child: voidable(string(), { undefinedWithWarningWhenInvalid: true, }), }), ), }); const result = coerce({field: {child: 1}}); invariant(result.type === 'success', 'should succeed'); expect(result.warnings?.length).toBe(1); const warning = result.warnings?.[0]; invariant(warning != null, 'should have warning'); expect(warning.path.toString()).toEqual('.field.child'); }); it('should correctly parse omitted voidable keys in object', () => { const coerce = object({ description: voidable(string()), title: string(), }); const result = coerce({title: 'test'}); invariant(result.type === 'success', 'should succeed'); invariant(result.value.title === 'test', 'value should be true'); invariant(result.value.description === undefined, 'value should be true'); }); }); describe('withDefault', () => { it('provide fallback value', () => { const coerce = coercion(withDefault(number(), 456)); expect(coerce(false)).toEqual(null); expect(coerce(123)).toEqual(123); expect(coerce('str')).toEqual(null); expect(coerce(null)).toEqual(456); expect(coerce(undefined)).toEqual(456); }); it('values refined to null also fallback', () => { const coerce = coercion( withDefault( asType(number(), () => null), 456, ), ); expect(coerce(false)).toEqual(null); expect(coerce(123)).toEqual(456); expect(coerce('str')).toEqual(null); expect(coerce(null)).toEqual(456); expect(coerce(undefined)).toEqual(456); }); it('object with optional property with default', () => { const coerce = coercion(object({num: withDefault(number(), 456)})); expect(coerce({num: 123})).toEqual({num: 123}); expect(coerce({})).toEqual({num: 456}); }); }); describe('constraint', () => { it('should correctly fail values which do not pass predicate', () => { const evenNumber = constraint(number(), n => n % 2 === 0); expect(evenNumber(2).type).toBe('success'); expect(evenNumber(1).type).toBe('failure'); }); it('should fail if underlying checker fails', () => { const evenNumber = constraint(number(), n => n % 2 === 0); expect(evenNumber(true).type).toBe('failure'); }); it('should correctly provide warning when checker passes but constraint does not', () => { const message = 'number is not even'; const evenNumber = constraint(number(), n => [n % 2 === 0, message]); const result = evenNumber(1); invariant(result.type === 'failure', 'should fail'); expect(result.message).toBe(message); }); }); describe('lazy', () => { it('should successfully parse basic values', () => { const coerce: Checker = lazy(() => string()); const result = coerce('test'); invariant(result.type === 'success', 'should succeed'); expect(result.value).toBe('test'); }); it('should allow for recursive types', () => { // $FlowFixMe[recursive-definition] const user = object({ id: number(), name: string(), // $FlowFixMe[underconstrained-implicit-instantiation] friends: nullable(array(lazy(() => user))), }); const result = user({ id: 1, name: 'a', friends: [ {id: 2, name: 'b'}, {id: 3, name: 'c'}, ], }); invariant(result.type === 'success', 'should succeed'); // example for typechecking const friendsNames = result.value.friends?.map(f => f.name); expect(friendsNames).toEqual(['b', 'c']); }); }); describe('custom', () => { it('should properly check using a custom function', () => { const isOneOrTwo = (v: mixed): ?(1 | 2) => (v === 1 || v === 2 ? v : null); const checkOneOrTwo = custom(isOneOrTwo); const oneResult = checkOneOrTwo(1); invariant(oneResult.type === 'success', 'should succeed'); const threeResult = checkOneOrTwo(3); invariant(threeResult.type === 'failure', 'should fail'); }); it('catch errors as failures', () => { function userValidator() { throw new Error('MY ERROR'); } const result = custom(userValidator)(); invariant(result.type === 'failure', 'should fail'); expect(result.message).toEqual('MY ERROR'); }); }); ================================================ FILE: packages/refine/package-for-release.json ================================================ { "name": "@recoiljs/refine", "publishConfig": { "access": "public" }, "version": "0.1.1", "description": "A type-refinement / validator combinator library for mixed / unknown values in Flow or TypeScript", "main": "cjs/index.js", "module": "es/index.js", "unpkg": "umd/index.js", "types": "index.d.ts", "files": ["umd", "es", "cjs", "index.d.ts"], "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT" } ================================================ FILE: packages/refine/package.json ================================================ { "name": "refine", "description": "This is the internal package.json enabling CommonJS module", "main": "Refine_index.js", "haste_commonjs": true, "files": [ "Refine_index.js" ], "directories": { "": "./" }, "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT" } ================================================ FILE: packages/shared/__test_utils__/Recoil_ReactRenderModes.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; let strictMode: boolean = false; const isStrictModeEnabled = (): boolean => strictMode; const setStrictMode = (enableStrictMode: boolean): void => { strictMode = enableStrictMode; }; let concurrentMode: boolean = false; const isConcurrentModeEnabled = (): boolean => concurrentMode; const setConcurrentMode = (enableConcurrentMode: boolean): void => { concurrentMode = enableConcurrentMode; }; module.exports = { isStrictModeEnabled, setStrictMode, isConcurrentModeEnabled, setConcurrentMode, }; ================================================ FILE: packages/shared/__test_utils__/Recoil_TestingUtils.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Store} from '../../recoil/core/Recoil_State'; import type {RecoilState, RecoilValue, RecoilValueReadOnly} from 'Recoil'; // @fb-only: const ReactDOMComet = require('ReactDOMComet'); // @fb-only: const ReactDOM = require('ReactDOMLegacy_DEPRECATED'); const {act} = require('ReactTestUtils'); const { RecoilRoot, atom, selector, useRecoilValue, useResetRecoilState, useSetRecoilState, } = require('Recoil'); // @fb-only: const StrictMode = require('StrictMode'); const {graph} = require('../../recoil/core/Recoil_Graph'); const {getNextStoreID} = require('../../recoil/core/Recoil_Keys'); const { notifyComponents_FOR_TESTING, sendEndOfBatchNotifications_FOR_TESTING, } = require('../../recoil/core/Recoil_RecoilRoot'); const { invalidateDownstreams, } = require('../../recoil/core/Recoil_RecoilValueInterface'); const {makeEmptyStoreState} = require('../../recoil/core/Recoil_State'); const invariant = require('../util/Recoil_invariant'); const nullthrows = require('../util/Recoil_nullthrows'); const stableStringify = require('../util/Recoil_stableStringify'); const { isConcurrentModeEnabled, isStrictModeEnabled, } = require('./Recoil_ReactRenderModes'); const React = require('react'); const {useEffect} = require('react'); const err = require('recoil-shared/util/Recoil_err'); const ReactDOM = require('react-dom'); // @oss-only const StrictMode = React.StrictMode; // @oss-only const QUICK_TEST = false; // @fb-only: const IS_INTERNAL = true; const IS_INTERNAL = false; // @oss-only // TODO Use Snapshots for testing instead of this thunk? function makeStore(): Store { const storeState = makeEmptyStoreState(); const store: Store = { storeID: getNextStoreID(), getState: () => storeState, replaceState: replacer => { const currentStoreState = store.getState(); // FIXME: does not increment state version number currentStoreState.currentTree = replacer(currentStoreState.currentTree); // no batching so nextTree is never active invalidateDownstreams(store, currentStoreState.currentTree); const {reactMode} = require('../../recoil/core/Recoil_ReactMode'); if (reactMode().early) { notifyComponents_FOR_TESTING( store, currentStoreState, currentStoreState.currentTree, ); } sendEndOfBatchNotifications_FOR_TESTING(store); }, getGraph: version => { const graphs = storeState.graphsByVersion; if (graphs.has(version)) { return nullthrows(graphs.get(version)); } const newGraph = graph(); graphs.set(version, newGraph); return newGraph; }, subscribeToTransactions: () => { throw new Error( 'This functionality, should not tested at this level. Use a component to test this functionality: e.g. componentThatReadsAndWritesAtom', ); }, addTransactionMetadata: () => { throw new Error('not implemented'); }, }; return store; } class ErrorBoundary extends React.Component< {children: React.Node | null, fallback?: Error => React.Node}, {hasError: boolean, error?: ?Error}, > { state: {hasError: boolean, error?: ?Error} = {hasError: false}; static getDerivedStateFromError(error: Error): { hasError: boolean, error?: ?Error, } { return {hasError: true, error}; } render(): React.Node { return this.state.hasError ? this.props.fallback != null && this.state.error != null ? this.props.fallback(this.state.error) : 'error' : this.props.children; } } type ReactAbstractElement = React.Element< React.AbstractComponent, >; function renderLegacyReactRoot( container: HTMLElement, contents: ReactAbstractElement, ) { ReactDOM.render(contents, container); // @oss-only // @fb-only: ReactDOM.render(contents, container, 'Recoil_TestingUtils.js'); } // @fb-only: const createRoot = ReactDOMComet.createRoot; // $FlowFixMe[prop-missing] unstable_createRoot is not part of react-dom typing // @oss-only const createRoot = ReactDOM.createRoot ?? ReactDOM.unstable_createRoot; // @oss-only function isConcurrentModeAvailable(): boolean { return createRoot != null; } function renderConcurrentReactRoot( container: HTMLElement, contents: ReactAbstractElement, // $FlowFixMe[missing-local-annot] ) { if (!isConcurrentModeAvailable()) { throw err( 'Concurrent rendering is not available with the current version of React.', ); } // $FlowFixMe[not-a-function] unstable_createRoot is not part of react-dom typing // @oss-only createRoot(container).render(contents); } function renderUnwrappedElements( elements: React.Node, container?: ?HTMLDivElement, ): HTMLDivElement { const div = container ?? document.createElement('div'); const renderReactRoot = isConcurrentModeEnabled() ? renderConcurrentReactRoot : renderLegacyReactRoot; act(() => { renderReactRoot( div, isStrictModeEnabled() ? ( // $FlowFixMe[incompatible-call] // @oss-only {elements} ) : ( // $FlowFixMe[incompatible-call] <>{elements} ), ); }); return div; } function renderElements( elements: ?React.Node, container?: ?HTMLDivElement, ): HTMLDivElement { return renderUnwrappedElements( {/* eslint-disable-next-line fb-www/no-null-fallback-for-error-boundary */} {elements} , container, ); } function renderElementsWithSuspenseCount( elements: React.Node, ): [HTMLDivElement, JestMockFn<[], void>] { const suspenseCommit = jest.fn(() => {}); function Fallback() { useEffect(suspenseCommit); return 'loading'; } const container = renderUnwrappedElements( {/* eslint-disable-next-line fb-www/no-null-fallback-for-error-boundary */} }>{elements} , ); return [container, suspenseCommit]; } //////////////////////////////////////// // Useful RecoilValue nodes for testing //////////////////////////////////////// let id = 0; function stringAtom(): RecoilState { return atom({key: `StringAtom-${id++}`, default: 'DEFAULT'}); } const errorThrowingAsyncSelector: ( string, ?RecoilValue, ) => RecoilValue = ( msg, dep: ?RecoilValue, ): RecoilValueReadOnly => selector({ key: `AsyncErrorThrowingSelector${id++}`, get: ({get}) => { if (dep != null) { get(dep); } return Promise.reject(new Error(msg)); }, }); const resolvingAsyncSelector: (T) => RecoilValue = ( value: T, ): RecoilValueReadOnly => selector({ key: `ResolvingSelector${id++}`, get: () => Promise.resolve(value), }); const loadingAsyncSelector: () => RecoilValueReadOnly = () => selector({ key: `LoadingSelector${id++}`, get: () => new Promise(() => {}), }); function asyncSelector( dep?: RecoilValue, ): [RecoilValue, (T) => void, (Error) => void] { let resolve: (result: Promise | T) => void = () => invariant(false, 'bug in test code'); // make flow happy with initialization let reject: (error: mixed) => void = () => invariant(false, 'bug in test code'); const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); const sel = selector({ key: `AsyncSelector${id++}`, // $FlowFixMe[missing-local-annot] get: ({get}) => { if (dep != null) { get(dep); } return promise; }, }); return [sel, resolve, reject]; } ////////////////////////////////// // Useful Components for testing ////////////////////////////////// function ReadsAtom({ atom, // eslint-disable-line no-shadow }: { atom: RecoilValue, }): React.Node { return stableStringify(useRecoilValue(atom)); } // Returns a tuple: [ // Component, // setValue(T), // resetValue() // ] function componentThatReadsAndWritesAtom( recoilState: RecoilState, ): [() => React.Node, (T) => void, () => void] { let setValue; let resetValue; const ReadsAndWritesAtom = (): React.Node => { setValue = useSetRecoilState(recoilState); resetValue = useResetRecoilState(recoilState); return stableStringify(useRecoilValue(recoilState)); }; return [ ReadsAndWritesAtom, (value: T) => setValue(value), () => resetValue(), ]; } function flushPromisesAndTimers(): Promise { // Wrap flush with act() to avoid warning that only shows up in OSS environment return act( () => new Promise(resolve => { window.setTimeout(resolve, 100); jest.runAllTimers(); }), ); } type ReloadImports = () => void | (() => void); type AssertionsFn = ({ gks: Array, strictMode: boolean, concurrentMode: boolean, }) => ?Promise; type TestOptions = { gks?: Array>, }; type TestFn = (string, AssertionsFn, TestOptions | void) => void; const testGKs = (reloadImports: ReloadImports, gks: Array>): TestFn => ( testDescription: string, assertionsFn: AssertionsFn, {gks: additionalGKs = ([]: Array>)}: TestOptions = {gks: []}, ) => { function runTests({ strictMode, concurrentMode, }: { strictMode: boolean, concurrentMode: boolean, }) { test.each([ // $FlowFixMe[incompatible-call] ...[...gks, ...additionalGKs].map(gksToTest => [ (!gksToTest.length ? testDescription : `${testDescription} [${gksToTest.join(', ')}]`) + (strictMode || concurrentMode ? ` [${[ strictMode ? 'StrictMode' : null, concurrentMode ? 'ConcurrentMode' : null, ] .filter(x => x != null) .join(', ')}]` : ''), gksToTest, ]), ])('%s', async (_title, gksToTest) => { jest.resetModules(); const gkx = require('recoil-shared/util/Recoil_gkx'); gkx.clear(); // @oss-only const { setStrictMode, setConcurrentMode, } = require('./Recoil_ReactRenderModes'); // Setup test environment setStrictMode(strictMode); setConcurrentMode(concurrentMode); // See: https://github.com/reactwg/react-18/discussions/102 const prevReactActEnvironment = global.IS_REACT_ACT_ENVIRONMENT; global.IS_REACT_ACT_ENVIRONMENT = true; gksToTest.forEach(gkx.setPass); const after = reloadImports(); try { await assertionsFn({gks: gksToTest, strictMode, concurrentMode}); } finally { global.IS_REACT_ACT_ENVIRONMENT = prevReactActEnvironment; gksToTest.forEach(gkx.setFail); after?.(); setStrictMode(false); setConcurrentMode(false); } }); } if (QUICK_TEST) { runTests({strictMode: false, concurrentMode: true}); } else { runTests({strictMode: false, concurrentMode: false}); runTests({strictMode: true, concurrentMode: false}); if (isConcurrentModeAvailable()) { runTests({strictMode: false, concurrentMode: true}); // 2020-12-20: The internal isn't yet enabled to run effects // multiple times. So, rely on GitHub CI actions to test this for now. if (!IS_INTERNAL) { runTests({strictMode: true, concurrentMode: true}); } } } }; const WWW_GKS_TO_TEST = QUICK_TEST ? [ [ 'recoil_hamt_2020', 'recoil_sync_external_store', 'recoil_memory_managament_2020', ], ] : [ // OSS for React <18: ['recoil_hamt_2020', 'recoil_suppress_rerender_in_callback'], // Also enables early rendering // OSS for React 18, test internally: [ 'recoil_hamt_2020', 'recoil_sync_external_store', 'recoil_suppress_rerender_in_callback', // Only used for fallback if no useSyncExternalStore() ], // Latest with GC: [ 'recoil_hamt_2020', 'recoil_sync_external_store', 'recoil_suppress_rerender_in_callback', 'recoil_memory_managament_2020', 'recoil_release_on_cascading_update_killswitch_2021', ], // Experimental mode for useTransition() support: ['recoil_hamt_2020', 'recoil_transition_support'], ]; /** * GK combinations to exclude in OSS, presumably because these combinations pass * in FB internally but not in OSS. Ideally this array would be empty. */ const OSS_GK_COMBINATION_EXCLUSIONS: $ReadOnlyArray<$ReadOnlyArray> = []; // eslint-disable-next-line no-unused-vars const OSS_GKS_TO_TEST = WWW_GKS_TO_TEST.filter( gkCombination => !OSS_GK_COMBINATION_EXCLUSIONS.some( exclusion => exclusion.every(gk => gkCombination.includes(gk)) && gkCombination.every(gk => exclusion.includes(gk)), ), ); const getRecoilTestFn = (reloadImports: ReloadImports): TestFn => testGKs( reloadImports, // @fb-only: WWW_GKS_TO_TEST, OSS_GKS_TO_TEST, // @oss-only ); module.exports = { makeStore, renderUnwrappedElements, renderElements, renderElementsWithSuspenseCount, ErrorBoundary, ReadsAtom, componentThatReadsAndWritesAtom, stringAtom, errorThrowingAsyncSelector, resolvingAsyncSelector, loadingAsyncSelector, asyncSelector, flushPromisesAndTimers, getRecoilTestFn, IS_INTERNAL, }; ================================================ FILE: packages/shared/package.json ================================================ { "name": "recoil-shared", "description": "This is the internal package.json enabling CommonJS module", "haste_commonjs": true, "directories": { "": "./" }, "repository": "https://github.com/facebookexperimental/Recoil.git", "license": "MIT" } ================================================ FILE: packages/shared/polyfill/ReactBatchedUpdates.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * This is to export esstiential functions from react-dom * for our web build * * @flow strict * @format * @oncall recoil */ const {unstable_batchedUpdates} = require('ReactDOMLegacy_DEPRECATED'); module.exports = {unstable_batchedUpdates}; ================================================ FILE: packages/shared/polyfill/ReactBatchedUpdates.native.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * This is to export esstiential functions from react-dom * for our react-native build * * @flow strict * @format * @oncall recoil */ // $FlowExpectedError[cannot-resolve-module] const {unstable_batchedUpdates} = require('ReactNative'); module.exports = {unstable_batchedUpdates}; ================================================ FILE: packages/shared/polyfill/err.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; function err(message: string): Error { const error = new Error(message); // In V8, Error objects keep the closure scope chain alive until the // err.stack property is accessed. if (error.stack === undefined) { // IE sets the stack only if error is thrown try { throw error; } catch (_) {} // eslint-disable-line fb-www/no-unused-catch-bindings, no-empty } return error; } module.exports = err; ================================================ FILE: packages/shared/polyfill/expectationViolation.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const sprintf = require('./sprintf'); function expectationViolation(format: string, ...args: $ReadOnlyArray) { if (__DEV__) { const message = sprintf.call(null, format, ...args); const error = new Error(message); error.name = 'Expectation Violation'; console.error(error); } } module.exports = expectationViolation; ================================================ FILE: packages/shared/polyfill/invariant.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; function invariant(condition: boolean, message: string) { if (!condition) { throw new Error(message); } } module.exports = invariant; ================================================ FILE: packages/shared/polyfill/recoverableViolation.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; function recoverableViolation( message: string, _projectName: 'recoil', {error}: {error?: Error} = {}, ): null { if (__DEV__) { console.error(message, error); } return null; } module.exports = recoverableViolation; ================================================ FILE: packages/shared/polyfill/sprintf.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; function sprintf(format: string, ...args: Array): string { let index = 0; return format.replace(/%s/g, () => String(args[index++])); } module.exports = sprintf; ================================================ FILE: packages/shared/util/Recoil_CopyOnWrite.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Utilities for working with built-in Maps and Sets without mutating them. * * @flow strict * @format * @oncall recoil */ 'use strict'; function setByAddingToSet(set: $ReadOnlySet, v: V): Set { const next = new Set(set); next.add(v); return next; } function setByDeletingFromSet(set: $ReadOnlySet, v: V): Set { const next = new Set(set); next.delete(v); return next; } function mapBySettingInMap( map: $ReadOnlyMap, k: K, v: V, ): Map { const next = new Map(map); next.set(k, v); return next; } function mapByUpdatingInMap( map: $ReadOnlyMap, k: K, updater: (V | void) => V, ): Map { const next = new Map(map); next.set(k, updater(next.get(k))); return next; } function mapByDeletingFromMap(map: $ReadOnlyMap, k: K): Map { const next = new Map(map); next.delete(k); return next; } function mapByDeletingMultipleFromMap( map: $ReadOnlyMap, ks: Set, ): Map { const next = new Map(map); ks.forEach(k => next.delete(k)); return next; } module.exports = { setByAddingToSet, setByDeletingFromSet, mapBySettingInMap, mapByUpdatingInMap, mapByDeletingFromMap, mapByDeletingMultipleFromMap, }; ================================================ FILE: packages/shared/util/Recoil_Environment.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; /* eslint-disable fb-www/typeof-undefined */ const isSSR: boolean = // $FlowFixMe(site=recoil) Window does not have a FlowType definition https://github.com/facebook/flow/issues/6709 typeof Window === 'undefined' || typeof window === 'undefined'; /* eslint-enable fb-www/typeof-undefined */ const isWindow = (value: mixed): boolean => !isSSR && // $FlowFixMe(site=recoil) Window does not have a FlowType definition https://github.com/facebook/flow/issues/6709 (value === window || value instanceof Window); const isReactNative: boolean = typeof navigator !== 'undefined' && navigator.product === 'ReactNative'; // eslint-disable-line fb-www/typeof-undefined module.exports = { isSSR, isReactNative, isWindow, }; ================================================ FILE: packages/shared/util/Recoil_Memoize.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; /** * Caches a function's results based on the key returned by the passed * hashFunction. */ function memoizeWithArgsHash, TReturn>( fn: (...TArgs) => TReturn, hashFunction: (...TArgs) => string, ): (...TArgs) => TReturn { let cache; return ((...args: TArgs): TReturn => { if (!cache) { cache = ({}: {[string]: TReturn}); } const key = hashFunction(...args); if (!Object.hasOwnProperty.call(cache, key)) { cache[key] = fn(...args); } return cache[key]; }: (...TArgs) => TReturn); } /** * Caches a function's results based on a comparison of the arguments. * Only caches the last return of the function. * Defaults to reference equality */ function memoizeOneWithArgsHash, TReturn>( fn: (...TArgs) => TReturn, hashFunction: (...TArgs) => string, ): (...TArgs) => TReturn { let lastKey: ?string; let lastResult: TReturn; // breaking cache when arguments change return ((...args: TArgs): TReturn => { const key = hashFunction(...args); if (lastKey === key) { return lastResult; } lastKey = key; lastResult = fn(...args); return lastResult; }: (...TArgs) => TReturn); } /** * Caches a function's results based on a comparison of the arguments. * Only caches the last return of the function. * Defaults to reference equality */ function memoizeOneWithArgsHashAndInvalidation< TArgs: $ReadOnlyArray, TReturn, >( fn: (...TArgs) => TReturn, hashFunction: (...TArgs) => string, ): [(...TArgs) => TReturn, () => void] { let lastKey: ?string; let lastResult: TReturn; // breaking cache when arguments change const memoizedFn: (...TArgs) => TReturn = (...args: TArgs): TReturn => { const key = hashFunction(...args); if (lastKey === key) { return lastResult; } lastKey = key; lastResult = fn(...args); return lastResult; }; const invalidate = () => { lastKey = null; }; return [memoizedFn, invalidate]; } module.exports = { memoizeWithArgsHash, memoizeOneWithArgsHash, memoizeOneWithArgsHashAndInvalidation, }; ================================================ FILE: packages/shared/util/Recoil_PerformanceTimings.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * This is a stub for some integration into FB internal stuff * * @flow strict * @format * @oncall recoil */ function startPerfBlock(_id: mixed): () => null { return () => null; } module.exports = {startPerfBlock}; ================================================ FILE: packages/shared/util/Recoil_ReactBatchedUpdates.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * This is to export esstiential functions from react-dom * for our web build * * @flow strict * @format * @oncall recoil */ // @fb-only: const {unstable_batchedUpdates} = require('ReactDOMComet'); // prettier-ignore const {unstable_batchedUpdates} = require('recoil-shared/polyfill/ReactBatchedUpdates'); // @oss-only module.exports = {unstable_batchedUpdates}; ================================================ FILE: packages/shared/util/Recoil_RecoilEnv.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; const err = require('./Recoil_err'); export type RecoilEnv = { RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED: boolean, RECOIL_GKS_ENABLED: Set, }; const env: RecoilEnv = { RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED: true, // Note: RECOIL_GKS_ENABLED settings will only be honored in OSS builds of Recoil RECOIL_GKS_ENABLED: new Set([ 'recoil_hamt_2020', 'recoil_sync_external_store', 'recoil_suppress_rerender_in_callback', 'recoil_memory_managament_2020', ]), }; function readProcessEnvBooleanFlag(name: string, set: boolean => void) { const sanitizedValue = process.env[name]?.toLowerCase()?.trim(); if (sanitizedValue == null || sanitizedValue === '') { return; } const allowedValues = ['true', 'false']; if (!allowedValues.includes(sanitizedValue)) { throw err( `process.env.${name} value must be 'true', 'false', or empty: ${sanitizedValue}`, ); } set(sanitizedValue === 'true'); } function readProcessEnvStringArrayFlag( name: string, set: (Array) => void, ) { const sanitizedValue = process.env[name]?.trim(); if (sanitizedValue == null || sanitizedValue === '') { return; } set(sanitizedValue.split(/\s*,\s*|\s+/)); } /** * Allow NodeJS/NextJS/etc to set the initial state through process.env variable * Note: we don't assume 'process' is available in all runtime environments * * @see https://github.com/facebookexperimental/Recoil/issues/733 */ function applyProcessEnvFlagOverrides() { // note: this check is needed in addition to the check below, runtime error will occur without it! // eslint-disable-next-line fb-www/typeof-undefined if (typeof process === 'undefined') { return; } if (process?.env == null) { return; } readProcessEnvBooleanFlag( 'RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED', value => { env.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = value; }, ); readProcessEnvStringArrayFlag('RECOIL_GKS_ENABLED', value => { value.forEach(gk => { env.RECOIL_GKS_ENABLED.add(gk); }); }); } applyProcessEnvFlagOverrides(); module.exports = env; ================================================ FILE: packages/shared/util/Recoil_concatIterables.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; /** * Combines multiple Iterables into a single Iterable. * Traverses the input Iterables in the order provided and maintains the order * of their elements. * * Example: * ``` * const r = Array.from(concatIterables(['a', 'b'], ['c'], ['d', 'e', 'f'])); * r == ['a', 'b', 'c', 'd', 'e', 'f']; * ``` */ function* concatIterables( iters: Iterable>, ): Iterable { for (const iter of iters) { for (const val of iter) { yield val; } } } module.exports = concatIterables; ================================================ FILE: packages/shared/util/Recoil_deepFreezeValue.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Deep freeze values. Do not descend into React elements, Immutable structures, * or in general things that respond poorly to being frozen. Follows the * implementation of deepFreezeValue. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {isReactNative, isWindow} = require('./Recoil_Environment'); const isNode = require('./Recoil_isNode'); const isPromise = require('./Recoil_isPromise'); function shouldNotBeFrozen(value: mixed): boolean { // Primitives and functions: if (value === null || typeof value !== 'object') { return true; } // React elements: switch (typeof value.$$typeof) { case 'symbol': return true; case 'number': return true; } // Immutable structures: if ( value['@@__IMMUTABLE_ITERABLE__@@'] != null || value['@@__IMMUTABLE_KEYED__@@'] != null || value['@@__IMMUTABLE_INDEXED__@@'] != null || value['@@__IMMUTABLE_ORDERED__@@'] != null || value['@@__IMMUTABLE_RECORD__@@'] != null ) { return true; } // DOM nodes: if (isNode(value)) { return true; } if (isPromise(value)) { return true; } if (value instanceof Error) { return true; } if (ArrayBuffer.isView(value)) { return true; } // Some environments, just as Jest, don't work with the instanceof check if (!isReactNative && isWindow(value)) { return true; } return false; } // Recursively freeze a value to enforce it is read-only. // This may also have minimal performance improvements for enumerating // objects (based on browser implementations, of course) function deepFreezeValue(value: mixed) { if (typeof value !== 'object' || shouldNotBeFrozen(value)) { return; } Object.freeze(value); // Make all properties read-only for (const key in value) { // $FlowIssue[method-unbinding] added when improving typing for this parameters if (Object.prototype.hasOwnProperty.call(value, key)) { const prop = value[key]; // Prevent infinite recurssion for circular references. if (typeof prop === 'object' && prop != null && !Object.isFrozen(prop)) { deepFreezeValue(prop); } } } Object.seal(value); // This also makes existing properties non-configurable. } module.exports = deepFreezeValue; ================================================ FILE: packages/shared/util/Recoil_differenceSets.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; /** * Returns a set containing all of the values from the first set that are not * present in any of the subsequent sets. * * Note: this is written procedurally (i.e., without filterSet) for performant * use in tight loops. */ function differenceSets( set: $ReadOnlySet, ...setsWithValuesToRemove: $ReadOnlyArray<$ReadOnlySet> ): $ReadOnlySet { const ret = new Set(); FIRST: for (const value of set) { for (const otherSet of setsWithValuesToRemove) { if (otherSet.has(value)) { continue FIRST; } } ret.add(value); } return ret; } module.exports = differenceSets; ================================================ FILE: packages/shared/util/Recoil_err.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; // @fb-only: const {err} = require('fb-error'); const err = require('recoil-shared/polyfill/err.js'); // @oss-only module.exports = err; ================================================ FILE: packages/shared/util/Recoil_expectationViolation.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; // @fb-only: const expectationViolation = require('expectationViolation'); const expectationViolation = require('recoil-shared/polyfill/expectationViolation.js'); // @oss-only module.exports = expectationViolation; ================================================ FILE: packages/shared/util/Recoil_filterIterable.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; /** * Creates a new iterable whose output is generated by passing the input * iterable's values through the filter function. */ function* filterIterable( iterable: Iterable, predicate: (v: T, index: number) => boolean, ): Iterable { // Use generator to create iterable/iterator let index = 0; for (const value of iterable) { if (predicate(value, index++)) { yield value; } } } module.exports = filterIterable; ================================================ FILE: packages/shared/util/Recoil_filterMap.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; /** * Returns a map containing all of the keys + values from the original map where * the given callback returned true. */ function filterMap( map: $ReadOnlyMap, callback: (value: TValue, key: TKey) => boolean, ): Map { const result = new Map(); for (const [key, value] of map) { if (callback(value, key)) { result.set(key, value); } } return result; } module.exports = filterMap; ================================================ FILE: packages/shared/util/Recoil_filterSet.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; /** * Returns a set containing all of the values from the original set where * the given callback returned true. */ function filterSet( set: $ReadOnlySet, callback: (value: TValue) => boolean, ): $ReadOnlySet { const result = new Set(); for (const value of set) { if (callback(value)) { result.add(value); } } return result; } module.exports = filterSet; ================================================ FILE: packages/shared/util/Recoil_gkx.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; const RecoilEnv = require('./Recoil_RecoilEnv'); function Recoil_gkx_OSS(gk: string): boolean { return RecoilEnv.RECOIL_GKS_ENABLED.has(gk); } Recoil_gkx_OSS.setPass = (gk: string): void => { RecoilEnv.RECOIL_GKS_ENABLED.add(gk); }; Recoil_gkx_OSS.setFail = (gk: string): void => { RecoilEnv.RECOIL_GKS_ENABLED.delete(gk); }; Recoil_gkx_OSS.clear = (): void => { RecoilEnv.RECOIL_GKS_ENABLED.clear(); }; module.exports = Recoil_gkx_OSS; // @oss-only // @fb-only: module.exports = require('gkx'); ================================================ FILE: packages/shared/util/Recoil_invariant.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; // @fb-only: const invariant = require('invariant'); const invariant = require('recoil-shared/polyfill/invariant.js'); // @oss-only module.exports = invariant; ================================================ FILE: packages/shared/util/Recoil_isNode.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; function isNode(object: mixed): boolean { if (typeof window === 'undefined') { return false; } const doc = object != null ? (object: $FlowFixMe).ownerDocument ?? object : document; const defaultView = doc.defaultView ?? window; return !!( object != null && (typeof defaultView.Node === 'function' ? object instanceof defaultView.Node : typeof object === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string') ); } module.exports = isNode; ================================================ FILE: packages/shared/util/Recoil_isPromise.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; declare function isPromise(p: mixed): boolean %checks(p instanceof Promise); // Split declaration and implementation to allow this function to pretend to // check for actual instance of Promise instead of something with a `then` // method. // eslint-disable-next-line no-redeclare function isPromise(p: $FlowFixMe): boolean { return !!p && typeof p.then === 'function'; } module.exports = isPromise; ================================================ FILE: packages/shared/util/Recoil_lazyProxy.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; /** * Return a proxy object based on the provided base and factories objects. * The proxy will include all properties of the base object as-is. * The factories object contains callbacks to obtain the values of the properies * for its keys. * * This is useful for providing users an object where some properties may be * lazily computed only on first access. */ // $FlowIssue[unclear-type] function lazyProxy any}>( base: Base, factories: Factories, ): { ...Base, ...$ObjMap(() => F) => F>, } { const proxy: Proxy = new Proxy(base, { // Compute and cache lazy property if not already done. get: (target, prop) => { if (!(prop in target) && prop in factories) { target[prop] = factories[prop](); } return target[prop]; }, // This method allows user to iterate keys as normal ownKeys: target => { // Materialize all lazy properties. This appears to be necessary for // onKeys to work properly, the object must actually have the properties // that it reports to have. for (const lazyProp in factories) { // Call this for side-effect to materialize lazy property // $FlowExpectedError[prop-missing] proxy[lazyProp]; } return Object.keys(target); }, }); // $FlowIssue[incompatible-return] return proxy; } module.exports = lazyProxy; ================================================ FILE: packages/shared/util/Recoil_mapIterable.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; /** * Creates a new iterable whose output is generated by passing the input * iterable's values through the mapper function. */ function mapIterable( iterable: Iterable, callback: (v: T, index: number) => K, ): Iterable { // Use generator to create iterable/iterator return (function* () { let index = 0; for (const value of iterable) { yield callback(value, index++); } })(); } module.exports = mapIterable; ================================================ FILE: packages/shared/util/Recoil_mapMap.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; /** * Returns a new Map object with the same keys as the original, but with the * values replaced with the output of the given callback function. */ function mapMap( map: $ReadOnlyMap, callback: (value: TValue, key: TKey) => TValueOut, ): Map { const result = new Map(); map.forEach((value, key) => { result.set(key, callback(value, key)); }); return result; } module.exports = mapMap; ================================================ FILE: packages/shared/util/Recoil_mergeMaps.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; function mergeMaps( ...maps: $ReadOnlyArray<$ReadOnlyMap> ): Map { const result = new Map(); for (let i = 0; i < maps.length; i++) { const iterator = maps[i].keys(); let nextKey; while (!(nextKey = iterator.next()).done) { // $FlowIssue[incompatible-call] - map/iterator knows nothing about flow types result.set(nextKey.value, maps[i].get(nextKey.value)); } } return result; } module.exports = mergeMaps; ================================================ FILE: packages/shared/util/Recoil_nullthrows.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; const err = require('./Recoil_err'); function nullthrows(x: ?T, message: ?string): T { if (x != null) { return x; } throw err(message ?? 'Got unexpected null or undefined'); } module.exports = nullthrows; ================================================ FILE: packages/shared/util/Recoil_recoverableViolation.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; // @fb-only: const recoverableViolation = require('recoverableViolation'); const recoverableViolation = require('recoil-shared/polyfill/recoverableViolation.js'); // @oss-only module.exports = recoverableViolation; ================================================ FILE: packages/shared/util/Recoil_shallowArrayEqual.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; function shallowArrayEqual>( a: TArr, b: TArr, ): boolean { if (a === b) { return true; } if (a.length !== b.length) { return false; } for (let i = 0, l = a.length; i < l; i++) { if (a[i] !== b[i]) { return false; } } return true; } module.exports = shallowArrayEqual; ================================================ FILE: packages/shared/util/Recoil_someSet.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; /** * The someSet() method tests whether some elements in the given Set pass the * test implemented by the provided function. */ function someSet( set: $ReadOnlySet, callback: (value: T, key: T, set: $ReadOnlySet) => boolean, context?: mixed, ): boolean { const iterator = set.entries(); let current = iterator.next(); while (!current.done) { const entry = current.value; if (callback.call(context, entry[1], entry[0], set)) { return true; } current = iterator.next(); } return false; } module.exports = someSet; ================================================ FILE: packages/shared/util/Recoil_stableStringify.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ 'use strict'; const err = require('./Recoil_err'); const isPromise = require('./Recoil_isPromise'); const TIME_WARNING_THRESHOLD_MS = 15; type Options = $ReadOnly<{allowFunctions?: boolean}>; function stringify(x: mixed, opt: Options, key?: string): string { // A optimization to avoid the more expensive JSON.stringify() for simple strings // This may lose protection for u2028 and u2029, though. if (typeof x === 'string' && !x.includes('"') && !x.includes('\\')) { return `"${x}"`; } // Handle primitive types switch (typeof x) { case 'undefined': return ''; // JSON.stringify(undefined) returns undefined, but we always want to return a string case 'boolean': return x ? 'true' : 'false'; case 'number': case 'symbol': // case 'bigint': // BigInt is not supported in www return String(x); case 'string': // Add surrounding quotes and escape internal quotes return JSON.stringify(x); case 'function': if (opt?.allowFunctions !== true) { throw err('Attempt to serialize function in a Recoil cache key'); } return `__FUNCTION(${x.name})__`; } if (x === null) { return 'null'; } // Fallback case for unknown types if (typeof x !== 'object') { return JSON.stringify(x) ?? ''; } // Deal with all promises as equivalent for now. if (isPromise(x)) { return '__PROMISE__'; } // Arrays handle recursive stringification if (Array.isArray(x)) { // $FlowFixMe[missing-local-annot] return `[${x.map((v, i) => stringify(v, opt, i.toString()))}]`; } // If an object defines a toJSON() method, then use that to override the // serialization. This matches the behavior of JSON.stringify(). // Pass the key for compatibility. // Immutable.js collections define this method to allow us to serialize them. if (typeof x.toJSON === 'function') { // flowlint-next-line unclear-type: off return stringify((x: any).toJSON(key), opt, key); } // For built-in Maps, sort the keys in a stable order instead of the // default insertion order. Support non-string keys. if (x instanceof Map) { const obj: {[string]: $FlowFixMe} = {}; for (const [k, v] of x) { // Stringify will escape any nested quotes obj[typeof k === 'string' ? k : stringify(k, opt)] = v; } return stringify(obj, opt, key); } // For built-in Sets, sort the keys in a stable order instead of the // default insertion order. if (x instanceof Set) { return stringify( // $FlowFixMe[missing-local-annot] Array.from(x).sort((a, b) => stringify(a, opt).localeCompare(stringify(b, opt)), ), opt, key, ); } // Anything else that is iterable serialize as an Array. if ( Symbol !== undefined && x[Symbol.iterator] != null && typeof x[Symbol.iterator] === 'function' ) { // flowlint-next-line unclear-type: off return stringify(Array.from((x: any)), opt, key); } // For all other Objects, sort the keys in a stable order. return `{${Object.keys(x) .filter(k => x[k] !== undefined) .sort() // stringify the key to add quotes and escape any nested slashes or quotes. .map(k => `${stringify(k, opt)}:${stringify(x[k], opt, k)}`) .join(',')}}`; } // Utility similar to JSON.stringify() except: // * Serialize built-in Sets as an Array // * Serialize built-in Maps as an Object. Supports non-string keys. // * Serialize other iterables as arrays // * Sort the keys of Objects and Maps to have a stable order based on string conversion. // This overrides their default insertion order. // * Still uses toJSON() of any object to override serialization // * Support Symbols (though don't guarantee uniqueness) // * We could support BigInt, but Flow doesn't seem to like it. // See Recoil_stableStringify-test.js for examples function stableStringify( x: mixed, opt: Options = {allowFunctions: false}, ): string { if (__DEV__) { if (typeof window !== 'undefined') { const startTime = window.performance ? window.performance.now() : 0; const str = stringify(x, opt); const endTime = window.performance ? window.performance.now() : 0; if (endTime - startTime > TIME_WARNING_THRESHOLD_MS) { /* eslint-disable fb-www/no-console */ console.groupCollapsed( `Recoil: Spent ${endTime - startTime}ms computing a cache key`, ); console.warn(x, str); console.groupEnd(); /* eslint-enable fb-www/no-console */ } return str; } } return stringify(x, opt); } module.exports = stableStringify; ================================================ FILE: packages/shared/util/Recoil_unionSets.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; function unionSets( ...sets: $ReadOnlyArray<$ReadOnlySet> ): Set { const result = new Set(); for (const set of sets) { for (const value of set) { result.add(value); } } return result; } module.exports = unionSets; ================================================ FILE: packages/shared/util/Recoil_useComponentName.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; /** * THIS CODE HAS BEEN COMMENTED OUT INTENTIONALLY * * This technique of getting the component name is imperfect, since it both only * works in a non-minified code base, and more importantly introduces performance * problems since it relies in throwing errors which is an expensive operation. * * At some point we may want to reevaluate this technique hence why we have commented * this code out, rather than delete it all together. */ // const {useRef} = require('react'); // const gkx = require('recoil-shared/util/Recoil_gkx'); // const stackTraceParser = require('recoil-shared/util/Recoil_stackTraceParser'); function useComponentName(): string { // const nameRef = useRef(); // if (__DEV__) { // if (gkx('recoil_infer_component_names')) { // if (nameRef.current === undefined) { // // There is no blessed way to determine the calling React component from // // within a hook. This hack uses the fact that hooks must start with 'use' // // and that hooks are either called by React Components or other hooks. It // // follows therefore, that to find the calling component, you simply need // // to look down the stack and find the first function which doesn't start // // with 'use'. We are only enabling this in dev for now, since once the // // codebase is minified, the naming assumptions no longer hold true. // // eslint-disable-next-line fb-www/no-new-error // const frames = stackTraceParser(new Error().stack); // for (const {methodName} of frames) { // // I observed cases where the frame was of the form 'Object.useXXX' // // hence why I'm searching for hooks following a word boundary // if (!methodName.match(/\buse[^\b]+$/)) { // return (nameRef.current = methodName); // } // } // nameRef.current = null; // } // return nameRef.current ?? ''; // } // } // @fb-only: return ""; return ''; // @oss-only } module.exports = useComponentName; ================================================ FILE: packages/shared/util/Recoil_usePrevious.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {useEffect, useRef} = require('react'); function usePrevious(value: T): T | void { const ref = useRef(); useEffect(() => { // $FlowFixMe[incompatible-type] ref.current = value; }); return ref.current; } module.exports = usePrevious; ================================================ FILE: packages/shared/util/Recoil_useRefInitOnce.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {useRef} = require('react'); /** * The same as `useRef()` except that if a function is specified then it will * call that function to get the value to initialize the reference with. * This is similar to how `useState()` behaves when given a function. It allows * the user to avoid generating the initial value for subsequent renders. * The tradeoff is that to set the reference to a function itself you need to * nest it: useRefInitOnce(() => () => {...}); */ function useRefInitOnce(initialValue: (() => T) | T): {current: T} { // $FlowExpectedError[incompatible-call] const ref = useRef(initialValue); if (ref.current === initialValue && typeof initialValue === 'function') { // $FlowExpectedError[incompatible-use] ref.current = initialValue(); } return ref; } module.exports = useRefInitOnce; ================================================ FILE: packages/shared/util/__tests__/Recoil_Memoize-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { memoizeOneWithArgsHash, memoizeOneWithArgsHashAndInvalidation, memoizeWithArgsHash, } = require('../Recoil_Memoize'); describe('memoizeWithArgsHash', () => { it('caches functions based on the hash function', () => { let i = 0; const f = jest.fn<$ReadOnlyArray<$FlowFixMe>, _>(() => i++); const mem = memoizeWithArgsHash<$ReadOnlyArray<$FlowFixMe>, _>( f, (_a, _b, c) => String(c), ); expect(mem()).toBe(0); expect(mem(1, 2, 3)).toBe(1); expect(mem(0, 0, 3)).toBe(1); expect(f.mock.calls.length).toBe(2); }); it('handles "hasOwnProperty" as a hash key with no errors', () => { let i = 0; const f = jest.fn<$ReadOnlyArray<$FlowFixMe>, _>(() => i++); const mem = memoizeWithArgsHash<$ReadOnlyArray<$FlowFixMe>, _>( f, () => 'hasOwnProperty', ); expect(mem()).toBe(0); expect(() => mem()).not.toThrow(); expect(mem(1)).toBe(0); expect(f.mock.calls.length).toBe(1); }); }); describe('memoizeOneWithArgsHash', () => { it('caches functions based on the arguments', () => { let i = 0; const f = jest.fn<$ReadOnlyArray<$FlowFixMe>, _>(() => i++); const mem = memoizeOneWithArgsHash<$ReadOnlyArray<$FlowFixMe>, _>( f, (a, b, c) => String(a) + String(b) + String(c), ); expect(mem()).toBe(0); expect(mem(1, 2, 3)).toBe(1); expect(mem(0, 0, 3)).toBe(2); expect(mem(0, 0, 3)).toBe(2); expect(mem(1, 2, 3)).toBe(3); expect(f.mock.calls.length).toBe(4); }); it('caches functions based on partial arguments', () => { let i = 0; const f = jest.fn<$ReadOnlyArray<$FlowFixMe>, _>(() => i++); const mem = memoizeOneWithArgsHash<$ReadOnlyArray<$FlowFixMe>, _>( f, (_a, _b, c) => String(c), ); expect(mem()).toBe(0); expect(mem(1, 2, 3)).toBe(1); expect(mem(0, 0, 3)).toBe(1); expect(mem(0, 0, 3)).toBe(1); expect(mem(1, 2, 3)).toBe(1); expect(mem(1, 2, 4)).toBe(2); expect(f.mock.calls.length).toBe(3); }); }); describe('memoizeOneWithArgsHashAndInvalidation', () => { it('caches functions based on the arguments', () => { let i = 0; const f = jest.fn<$ReadOnlyArray<$FlowFixMe>, _>(() => i++); const [mem, invalidate] = memoizeOneWithArgsHashAndInvalidation< $ReadOnlyArray<$FlowFixMe>, _, >(f, (a, b, c) => String(a) + String(b) + String(c)); expect(mem()).toBe(0); expect(mem(1, 2, 3)).toBe(1); expect(mem(0, 0, 3)).toBe(2); expect(mem(0, 0, 3)).toBe(2); expect(mem(1, 2, 3)).toBe(3); expect(mem(1, 2, 3)).toBe(3); invalidate(); expect(mem(1, 2, 3)).toBe(4); expect(mem(1, 2, 3)).toBe(4); expect(f.mock.calls.length).toBe(5); }); it('caches functions based on partial arguments', () => { let i = 0; const f = jest.fn<$ReadOnlyArray<$FlowFixMe>, _>(() => i++); const [mem, invalidate] = memoizeOneWithArgsHashAndInvalidation< $ReadOnlyArray<$FlowFixMe>, _, >(f, (_a, _b, c) => String(c)); expect(mem()).toBe(0); expect(mem(1, 2, 3)).toBe(1); expect(mem(0, 0, 3)).toBe(1); expect(mem(0, 0, 3)).toBe(1); expect(mem(1, 2, 3)).toBe(1); expect(mem(1, 2, 4)).toBe(2); expect(mem(1, 2, 4)).toBe(2); invalidate(); expect(mem(1, 2, 4)).toBe(3); expect(mem(1, 2, 4)).toBe(3); expect(f.mock.calls.length).toBe(4); }); }); ================================================ FILE: packages/shared/util/__tests__/Recoil_RecoilEnv-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { IS_INTERNAL, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); const testOssOnly = IS_INTERNAL ? test.skip : test; describe('RecoilEnv', () => { testOssOnly('OSS only: environment propagates GKs', () => { const RecoilEnv = require('../Recoil_RecoilEnv'); const gkx = require('../Recoil_gkx'); expect(gkx('recoil_test_gk')).toBe(false); RecoilEnv.RECOIL_GKS_ENABLED.add('recoil_test_gk'); expect(gkx('recoil_test_gk')).toBe(true); }); describe('support for process.env.RECOIL_GKS_ENABLED', () => { const originalProcessEnv = process.env; beforeEach(() => { process.env = {...originalProcessEnv}; process.env.RECOIL_GKS_ENABLED = 'recoil_test_gk1,recoil_test_gk2 recoil_test_gk3'; jest.resetModules(); }); afterEach(() => { process.env = originalProcessEnv; }); testOssOnly('OSS only: environment propagates GKs', () => { const gkx = require('../Recoil_gkx'); expect(gkx('recoil_test_gk1')).toBe(true); expect(gkx('recoil_test_gk2')).toBe(true); expect(gkx('recoil_test_gk3')).toBe(true); }); }); }); ================================================ FILE: packages/shared/util/__tests__/Recoil_deepFreezeValue-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const deepFreezeValue = require('../Recoil_deepFreezeValue'); describe('deepFreezeValue', () => { test('Do not freeze Promises', () => { const obj = {test: new Promise(() => {})}; deepFreezeValue(obj); expect(Object.isFrozen(obj)).toBe(true); expect(Object.isFrozen(obj.test)).toBe(false); }); test('Do not freeze Errors', () => { const obj = {test: new Error()}; deepFreezeValue(obj); expect(Object.isFrozen(obj)).toBe(true); expect(Object.isFrozen(obj.test)).toBe(false); }); test('check no error: object with ArrayBufferView property', () => { expect(() => deepFreezeValue({test: new Int8Array(4)})).not.toThrow(); expect(() => deepFreezeValue({test: new Uint8Array(4)})).not.toThrow(); expect(() => deepFreezeValue({test: new Uint8ClampedArray(4)}), ).not.toThrow(); expect(() => deepFreezeValue({test: new Uint16Array(4)})).not.toThrow(); expect(() => deepFreezeValue({test: new Int32Array(4)})).not.toThrow(); expect(() => deepFreezeValue({test: new Uint32Array(4)})).not.toThrow(); expect(() => deepFreezeValue({test: new Float32Array(4)})).not.toThrow(); expect(() => deepFreezeValue({test: new Float64Array(4)})).not.toThrow(); expect(() => deepFreezeValue({test: new DataView(new ArrayBuffer(16), 0)}), ).not.toThrow(); }); test('check no error: object with Window property', () => { if (typeof window === 'undefined') { return; } expect(() => deepFreezeValue({test: window})).not.toThrow(); }); // TODO add test of other pattern }); ================================================ FILE: packages/shared/util/__tests__/Recoil_lazyProxy-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const lazyProxy = require('../Recoil_lazyProxy'); test('lazyProxy', () => { const lazyProp = jest.fn(() => 456); const proxy = lazyProxy( { foo: 123, }, { bar: lazyProp, }, ); expect(proxy.foo).toBe(123); expect(proxy.bar).toBe(456); expect(lazyProp).toHaveBeenCalledTimes(1); expect(proxy.bar).toBe(456); expect(lazyProp).toHaveBeenCalledTimes(1); }); test('lazyProxy - keys', () => { const proxy = lazyProxy( { foo: 123, }, { bar: () => 456, }, ); expect(Object.keys(proxy)).toEqual(['foo', 'bar']); expect('foo' in proxy).toBe(true); expect('bar' in proxy).toBe(true); const keys = []; for (const key in proxy) { keys.push(key); } expect(keys).toEqual(['foo', 'bar']); }); ================================================ FILE: packages/shared/util/__tests__/Recoil_stableStringify-test.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const stableStringify = require('../Recoil_stableStringify'); const immutable = require('immutable'); describe('stableStringify', () => { // undefined test('undefined', () => { expect(stableStringify()).toBe(''); expect(stableStringify(undefined)).toBe(''); }); // Primitives test('primitives', () => { expect(stableStringify(null)).toBe('null'); expect(stableStringify(true)).toBe('true'); expect(stableStringify(false)).toBe('false'); expect(stableStringify(42)).toBe('42'); expect(stableStringify('hello world')).toBe('"hello world"'); expect(stableStringify('contains \\ backslash')).toBe( '"contains \\\\ backslash"', ); expect(stableStringify('nested "quotes"')).toBe('"nested \\"quotes\\""'); expect(stableStringify('nested escaped \\" quote')).toBe( '"nested escaped \\\\\\" quote"', ); // expect(stableStringify(BigInt(42))).toBe('42'); // BigInt is not supported in www }); test('Symbol', () => { expect(stableStringify(Symbol('foo'))).toBe('Symbol(foo)'); }); // Array test('Array', () => { expect(stableStringify([1, 2])).toBe('[1,2]'); }); // Object test('Object', () => { // Object with stable key order expect(stableStringify({foo: 2, bar: 1})).toBe('{"bar":1,"foo":2}'); expect(stableStringify({bar: 1, foo: 2})).toBe('{"bar":1,"foo":2}'); // Object with quote in key expect(stableStringify({'key with "quotes"': 'value'})).toBe( '{"key with \\"quotes\\"":"value"}', ); // Object with undefined values expect(stableStringify({foo: undefined, bar: 2})).toBe('{"bar":2}'); }); // Nested objects test('Nested Objects', () => { expect(stableStringify({arr: [1, 2]})).toBe('{"arr":[1,2]}'); expect(stableStringify([{foo: 1}, {bar: 2}])).toBe('[{"foo":1},{"bar":2}]'); }); // Set test('Set', () => { // Built-in Set with stable order expect(stableStringify(new Set([1, 2]))).toBe('[1,2]'); expect(stableStringify(new Set([2, 1]))).toBe('[1,2]'); expect(stableStringify(new Set([{foo: 2, bar: 1}]))).toBe( '[{"bar":1,"foo":2}]', ); expect(stableStringify(new Set([{bar: 1, foo: 2}]))).toBe( '[{"bar":1,"foo":2}]', ); }); // Map test('Map', () => { // Built-in Map with stable key order expect( stableStringify( new Map([ ['foo', 2], ['bar', 1], ]), ), ).toBe('{"bar":1,"foo":2}'); expect( stableStringify( new Map([ ['bar', 1], ['foo', 2], ]), ), ).toBe('{"bar":1,"foo":2}'); // Built-in Map with non-string keys with stable key order expect( stableStringify( new Map([ [{bar: 1}, 1], [{foo: 2}, 2], ]), ), ).toBe('{"{\\"bar\\":1}":1,"{\\"foo\\":2}":2}'); expect( stableStringify( new Map([ [{foo: 2}, 2], [{bar: 1}, 1], ]), ), ).toBe('{"{\\"bar\\":1}":1,"{\\"foo\\":2}":2}'); }); // Nested Maps/Sets test('Nested Maps/Sets', () => { expect( stableStringify( new Set([ new Map([ ['foo', 2], ['bar', 1], ]), ]), ), ).toBe('[{"bar":1,"foo":2}]'); expect(stableStringify(new Map([['arr', new Set([2, 1])]]))).toBe( '{"arr":[1,2]}', ); }); // Iterable test('Iterable', () => { expect( stableStringify({ // eslint-disable-next-line object-shorthand [Symbol.iterator]: function* () { yield 'foo'; yield 'bar'; }, }), ).toBe('["foo","bar"]'); }); }); describe('stableStringify Immutable', () => { // List test('List', () => { expect(stableStringify(immutable.List([1, 2]))).toBe('[1,2]'); expect(stableStringify(immutable.List([2, 1]))).toBe('[2,1]'); // List with mutated internal metadata (e.g. __hash or __ownerID) expect(stableStringify(immutable.List([1, 2]).asMutable())).toBe('[1,2]'); }); // Set test('Set', () => { // Set - JSON conversion is handled by Immutable's toJSON(), which produces // an array, so the keys are not sorted expect(stableStringify(immutable.Set(['a', 'b']))).toBe('["a","b"]'); expect(stableStringify(immutable.Set(['b', 'a']))).toBe('["b","a"]'); // OrderedSet is handled the same as Set }); // Map test('Map', () => { expect(stableStringify(immutable.Map({foo: 2, bar: 1}))).toBe( '{"bar":1,"foo":2}', ); expect(stableStringify(immutable.Map({bar: 1, foo: 2}))).toBe( '{"bar":1,"foo":2}', ); // OrderedMap is handled the same as Map, so ordering is lost }); // Record test('Record', () => { const R = immutable.Record({foo: undefined}); const r = R({foo: 1}); expect(stableStringify(r)).toBe('{"foo":1}'); }); // Nested Immutable test('Nested Immutables', () => { expect(stableStringify([immutable.List([1, 2])])).toBe('[[1,2]]'); expect( stableStringify({foo: immutable.List([2]), bar: immutable.List([1])}), ).toBe('{"bar":[1],"foo":[2]}'); }); }); ================================================ FILE: packages-ext/recoil-devtools/.babelrc ================================================ { "presets": [ "@babel/react", "@babel/flow" ], "plugins": [ [ "module-resolver", { "root": [ "./src" ], "alias": { "React": "react", "ReactDOMLegacy_DEPRECATED": "react-dom", "ReactTestUtils": "react-dom/test-utils" } } ], "babel-preset-fbjs/plugins/dev-expression", "@babel/plugin-proposal-class-properties", "@babel/proposal-nullish-coalescing-operator", "@babel/proposal-optional-chaining", "@babel/transform-flow-strip-types" ] } ================================================ FILE: packages-ext/recoil-devtools/.eslintignore ================================================ node_modules build yarn.lock recoil_devtools_ext/ ================================================ FILE: packages-ext/recoil-devtools/.eslintrc ================================================ { "plugins": [ "react-hooks", "unused-imports" ], "rules": { "unused-imports/no-unused-vars": [ "warn", { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } ], "unused-imports/no-unused-imports": "warn", "react-hooks/exhaustive-deps": "error", "react-hooks/rules-of-hooks": "error" } } ================================================ FILE: packages-ext/recoil-devtools/.gitignore ================================================ # dependencies /yarn.lock /package-lock.json /node_modules #build /build /recoil_devtools_ext recoil_devtools_ext.* # testing /coverage .watchmanconfig .watchmand # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local ================================================ FILE: packages-ext/recoil-devtools/.prettierignore ================================================ recoil_devtools_ext/ ================================================ FILE: packages-ext/recoil-devtools/flow.js ================================================ declare var __DEV__: boolean; ================================================ FILE: packages-ext/recoil-devtools/package.json ================================================ { "name": "recoil-devtools", "version": "0.1.1", "description": "DevTools for the Recoil state management library", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/facebookexperimental/Recoil" }, "scripts": { "build": "NODE_ENV=production node utils/build.js", "start": "NODE_ENV=development node utils/webserver.js", "prettier": "prettier --write '**/*.{js,jsx,css,html}'", "test": "jest", "test-watch": "jest --watch", "pack": "npm run build && crx pack recoil_devtools_ext -o recoil_devtools_ext.crx -p" }, "dependencies": { "@hot-loader/react-dom": "^16.13.0", "@types/chrome": "0.0.104", "@types/react": "^16.9.26", "classnames": "^2.2.6", "crx": "^5.0.1", "d3": "^5.16.0", "d3-array": "^2.7.1", "d3-collection": "^1.0.7", "d3-interpolate": "^2.0.1", "d3-scale": "^3.2.2", "d3-selection": "^2.0.0", "d3-transition": "^2.0.0", "flow-bin": "^0.129.0", "immutable": "^4.0.0-rc.12", "jsondiffpatch-for-react": "^1.0.4", "nullthrows": "^1.1.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-hot-loader": "^4.12.20", "recoil": "0.0.13" }, "devDependencies": { "@babel/core": "^7.9.0", "@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/preset-env": "^7.9.0", "@babel/preset-flow": "^7.10.4", "@babel/preset-react": "^7.9.4", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "babel-plugin-module-resolver": "^4.0.0", "babel-preset-fbjs": "^3.3.0", "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^5.1.1", "css-loader": "^3.4.2", "eslint": "^6.8.0", "eslint-plugin-import": "^2.20.1", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-react-hooks": "^3.0.0", "eslint-plugin-unused-imports": "^2.0.0", "file-loader": "^6.0.0", "fs-extra": "^9.0.0", "html-loader": "0.5.5", "html-webpack-plugin": "^4.0.2", "jest": "^26.0.1", "prettier": "^2.0.2", "style-loader": "^1.1.3", "webpack": "^4.42.1", "webpack-cli": "^3.3.11", "webpack-dev-server": "3.2.1", "write-file-webpack-plugin": "^4.5.1" } } ================================================ FILE: packages-ext/recoil-devtools/src/constants/Constants.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * This Recoil Plugin adds support for Redux Dev Tools (https://fburl.com/sjb1mvms). * It allows you to watch Recoil state changes as they happen. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const ExtensionSource: string = __DEV__ ? 'recoil-dev-tools-DEV-MODE' : 'recoil-dev-tools'; const ExtensionSourceContentScript: string = __DEV__ ? 'recoil-dev-tools-content-script-DEV-MODE' : 'recoil-dev-tools-content-script'; const RecoilDevToolsActions = { // sent from page INIT: 'recoil_devtools_init', UPDATE: 'recoil_devtools_update', UPLOAD_CHUNK: 'recoil_devtools_chunk', // sent from background store to popup DISCONNECT: 'recoil_devtools_disconnect', CONNECT: 'recoil_devtools_connect', UPDATE_STORE: 'recoil_devtools_update_store', // sent from popup to background SUBSCRIBE_POPUP: 'recoil_devtools_subscribe_popup', // sent from background store to page GO_TO_SNAPSHOT: 'recoil_devtools_go_to_snapshot', }; export type MainTabsType = 'Diff' | 'State' | 'Graph'; const MainTabs: MainTabsType[] = ['Diff', 'State', 'Graph']; const MainTabsTitle: {[MainTabsType]: string} = Object.freeze({ Diff: 'Modified Values', State: 'Known Nodes', Graph: 'Registered Dependencies', }); const MessageChunkSize = 1024 * 1024; module.exports = { ExtensionSource, ExtensionSourceContentScript, RecoilDevToolsActions, MainTabs, MainTabsTitle, MessageChunkSize, }; ================================================ FILE: packages-ext/recoil-devtools/src/manifest.json ================================================ { "name": "Recoil DevTools", "background": { "scripts": ["background.bundle.js"], "persistent": false }, "page_action": { "default_popup": "popup.html", "default_icon": "icon-34.png" }, "icons": { "128": "icon-128.png" }, "devtools_page": "devtools.html", "content_scripts": [ { "matches": [""], "js": ["contentScript.bundle.js"], "run_at": "document_start", "all_frames": true } ], "web_accessible_resources": [ "pageScript.bundle.js", "icon-128.png", "icon-34.png" ], "manifest_version": 2, "version": "0.1.2", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "update_url": "https://www.internalfb.com/intern/browser_extensions/chrome/update.xml" } ================================================ FILE: packages-ext/recoil-devtools/src/pages/Background/Background.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {BackgroundPostMessage} from '../../types/DevtoolsTypes'; const {RecoilDevToolsActions} = require('../../constants/Constants'); const {debug, warn} = require('../../utils/Logger'); const Store = require('../../utils/Store'); const store = (window.store = new Store()); const getConnectionId = ({sender}: chrome$Port): number => { // If this is a devtool connection, there's no tab.id // But that ID is not required so we return 0 return sender?.tab?.id ?? 0; }; const getConnectionName = ({name}: chrome$Port): string => { let id = name ?? 'Recoil Connection'; return id; }; function onConnect(port: chrome$Port): void { const connectionId = getConnectionId(port); const displayName = getConnectionName(port); let isPopupConnection = false; const chunksBuffer = new Map(); const msgHandler = (msg: BackgroundPostMessage) => { // ignore invalid message formats if (msg?.action == null) { return; } if (msg.action === RecoilDevToolsActions.UPDATE) { store.processMessage(msg, connectionId); } else if (msg.action === RecoilDevToolsActions.INIT) { store.connect( connectionId, msg.data?.persistenceLimit, msg.data?.initialValues, displayName, msg.data?.devMode, port, ); debug('CONNECT', connectionId); // This is only needed if we want to display a popup banner // in addition to the devpanel. // chrome.pageAction.show(connectionId); } else if (msg.action === RecoilDevToolsActions.SUBSCRIBE_POPUP) { isPopupConnection = true; store.subscribe(port); } else if (msg.action === RecoilDevToolsActions.UPLOAD_CHUNK) { const chunkSoFar = (chunksBuffer.get(msg.txID) ?? '') + (msg.chunk ?? ''); chunksBuffer.set(msg.txID, chunkSoFar); if (Boolean(msg.isFinalChunk)) { try { const data = JSON.parse(chunkSoFar); msgHandler(data); } catch (e) { warn('Recoil DevTools: Message failed due to "`${e.message}`"'); } finally { chunksBuffer.delete(msg.txID); } } } }; port.onMessage.addListener(msgHandler); port.onDisconnect.addListener(() => { debug('DISCONNECT', connectionId); if (isPopupConnection) { store.unsubscribe(port); } else { store.disconnect(connectionId); } }); } chrome.runtime.onConnect.addListener(onConnect); module.exports = {onConnect}; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Background/__tests__/Background.test.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @oncall recoil */ global.chrome = { runtime: { onConnect: { addListener: jest.fn(), }, }, }; global.__DEV__ = true; const {onConnect} = require('../Background'); const {RecoilDevToolsActions} = require('../../../constants/Constants'); describe('Background Proccess', () => { it('onConnect listern added', () => { expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); }); const store = global.window.store; it('Store is found', () => { expect(store).toBeDefined(); }); let evtHandler = null; const port = { onMessage: { addListener: jest.fn(fn => (evtHandler = fn)), }, onDisconnect: { addListener: jest.fn(), }, }; onConnect(port); it('evtHandler is set and connection is', () => { expect(evtHandler).toBeDefined(); }); it('connection is created', () => { evtHandler({ action: RecoilDevToolsActions.INIT, data: { initialValues: { a: {t: '0', v: 2}, }, }, }); expect(store.connections.size).toBe(1); expect(store.connections.get(0).transactions.getSize()).toBe(1); }); it('transaction is stored', () => { evtHandler({ action: RecoilDevToolsActions.UPDATE, message: { modifiedValues: { b: {t: '0', v: 2}, }, }, }); expect(store.connections.get(0).transactions.getSize()).toBe(2); expect(store.connections.get(0).transactions.get(1).modifiedValues).toEqual( [{isSubscriber: false, name: 'b'}], ); }); it('transaction in chunks is stored', () => { evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: '{"action":"recoil_devtools_update","source":"sourc', isFinalChunk: false, txID: 2, }); evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: 'e","message":{"mustThrow":true,"modifiedValues":{"', isFinalChunk: false, txID: 2, }); evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: 'c":{"t":"0","v":2}}}}', isFinalChunk: true, txID: 2, }); expect(store.connections.get(0).transactions.getSize()).toBe(3); expect(store.connections.get(0).transactions.get(2).modifiedValues).toEqual( [{isSubscriber: false, name: 'c'}], ); }); it('mixed chunks are dealt with properly', () => { // mixed txID 3 evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: '{"action":"recoil_devtools_update","source":"sourc', isFinalChunk: false, txID: 3, }); // mixed txID 4 evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: '{"action":"recoil_devtools_update","source":"sourc', isFinalChunk: false, txID: 4, }); evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: 'e","message":{"mustThrow":true,"modifiedValues":{"', isFinalChunk: false, txID: 3, }); evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: 'e","message":{"mustThrow":true,"modifiedValues":{"', isFinalChunk: false, txID: 4, }); evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: 'd":{"t":"0","v":2}}}}', isFinalChunk: true, txID: 3, }); evtHandler({ action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: 'e":{"t":"0","v":2}}}}', isFinalChunk: true, txID: 4, }); expect(store.connections.get(0).transactions.getSize()).toBe(5); expect(store.connections.get(0).transactions.get(3).modifiedValues).toEqual( [{isSubscriber: false, name: 'd'}], ); expect(store.connections.get(0).transactions.get(4).modifiedValues).toEqual( [{isSubscriber: false, name: 'e'}], ); }); }); ================================================ FILE: packages-ext/recoil-devtools/src/pages/Background/index.html ================================================ ================================================ FILE: packages-ext/recoil-devtools/src/pages/Content/ContentScript.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {DevToolsOptions, PostMessageData} from '../../types/DevtoolsTypes'; const { ExtensionSource, ExtensionSourceContentScript, MessageChunkSize, RecoilDevToolsActions, } = require('../../constants/Constants'); const {warn} = require('../../utils/Logger'); const nullthrows = require('nullthrows'); // Init message listeners function initContentScriptListeners() { let connected = false; let bg = null; window.addEventListener('message', (message: MessageEvent) => { // $FlowFixMe: get message type const data = ((message.data: mixed): PostMessageData); if (data.source !== ExtensionSource) { return; } if (data.action === RecoilDevToolsActions.INIT) { connect(data.props); } else { send(data); } }); function connect(props: ?DevToolsOptions) { // Connect to the background script connected = true; bg = chrome.runtime.connect(window.devToolsExtensionID, { name: props?.name ?? '', }); send({ action: RecoilDevToolsActions.INIT, props: props ? {...props} : undefined, }); bg?.onDisconnect.addListener(() => { connected = false; bg = null; }); bg?.onMessage.addListener(msg => { window.postMessage({ source: ExtensionSourceContentScript, ...msg, }); }); } function send(data: PostMessageData) { if (bg !== null && connected) { try { bg.postMessage(data); } catch (err) { if (err.message === 'Message length exceeded maximum allowed length.') { sendInChunks(data); } else { warn(`Transaction ignored in Recoil DevTools: ${err.message}`); } } } } function sendInChunks(data: PostMessageData) { if (bg == null || !connected) { return; } const encoded = JSON.stringify(data); const len = encoded.length; for (let i = 0; i < len; i = i + MessageChunkSize) { const chunk = encoded.slice(i, i + MessageChunkSize); try { bg?.postMessage({ action: RecoilDevToolsActions.UPLOAD_CHUNK, txID: data.txID ?? -1, chunk, isFinalChunk: i + MessageChunkSize >= len, }); } catch (err) { warn(`Transaction ignored in Recoil DevTools: ${err.message}`); } } } } // - Load page script so it can access the window object function initPageScript() { const pageScript = document.createElement('script'); pageScript.type = 'text/javascript'; pageScript.src = chrome.extension.getURL('pageScript.bundle.js'); // remove the pageScript node after it has run pageScript.onload = function () { this.parentNode.removeChild(this); }; nullthrows(document.head ?? document.documentElement).appendChild(pageScript); } initContentScriptListeners(); initPageScript(); module.exports = {initContentScriptListeners}; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Content/__tests__/ContentScript.test.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @oncall recoil */ // Mock spies const handlers = []; const bg = { onDisconnect: { addListener: jest.fn(), }, onMessage: { addListener: jest.fn(), }, postMessage: jest.fn(msg => { if (msg.message?.mustThrow) { throw new Error('Message length exceeded maximum allowed length.'); } }), }; // Mock global objects global.chrome = { extension: { getURL: () => '', }, runtime: { connect: () => bg, }, }; global.window.addEventListener = (evt, handler) => { handlers.push(handler); }; global.__DEV__ = true; const { ExtensionSource, RecoilDevToolsActions, } = require('../../../constants/Constants'); // Mock constants jest.mock('../../../constants/Constants', () => ({ ExtensionSource: 'source', ExtensionSourceContentScript: 'script', RecoilDevToolsActions: { INIT: 'recoil_devtools_init', UPDATE: 'recoil_devtools_update', UPLOAD_CHUNK: 'recoil_devtools_chunk', }, MessageChunkSize: 50, })); // Side-effect: initializes handlers variable require('../ContentScript'); describe('initializing Content Script listeners', () => { it('sets events handler', () => { expect(handlers.length).toEqual(1); }); const EvtHandler = handlers[0]; it('ignore message with wrong source', () => { EvtHandler({ data: { action: RecoilDevToolsActions.INIT, source: 'other', }, }); expect(bg.postMessage).not.toHaveBeenCalled(); }); it('ignore message with missing action', () => { EvtHandler({ data: { source: ExtensionSource, }, }); expect(bg.postMessage).not.toHaveBeenCalled(); }); it('inits connection', () => { EvtHandler({ data: { action: RecoilDevToolsActions.INIT, source: ExtensionSource, }, }); expect(bg.postMessage).toHaveBeenCalledWith({ action: RecoilDevToolsActions.INIT, props: undefined, }); }); it('Sends updates', () => { EvtHandler({ data: { action: RecoilDevToolsActions.UPDATE, source: ExtensionSource, message: {modifiedValues: {a: {t: '0', v: 2}}}, }, }); expect(bg.postMessage).toHaveBeenLastCalledWith({ action: RecoilDevToolsActions.UPDATE, source: ExtensionSource, message: { modifiedValues: { a: {t: '0', v: 2}, }, }, }); }); it('Sends updates in chunks after size error', () => { bg.postMessage.mockClear(); EvtHandler({ data: { action: RecoilDevToolsActions.UPDATE, source: ExtensionSource, message: {mustThrow: true, modifiedValues: {a: {t: '0', v: 2}}}, }, }); expect(bg.postMessage).toHaveBeenCalledTimes(4); expect(bg.postMessage).toHaveBeenNthCalledWith(2, { action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: '{"action":"recoil_devtools_update","source":"sourc', isFinalChunk: false, txID: -1, }); expect(bg.postMessage).toHaveBeenNthCalledWith(3, { action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: 'e","message":{"mustThrow":true,"modifiedValues":{"', isFinalChunk: false, txID: -1, }); expect(bg.postMessage).toHaveBeenNthCalledWith(4, { action: RecoilDevToolsActions.UPLOAD_CHUNK, chunk: 'a":{"t":"0","v":2}}}}', isFinalChunk: true, txID: -1, }); }); }); ================================================ FILE: packages-ext/recoil-devtools/src/pages/Devtools/DevtoolsScript.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; chrome.devtools.panels.create( __DEV__ ? 'Recoil (DEV)' : 'Recoil', '', 'devpanel.html', () => {}, ); ================================================ FILE: packages-ext/recoil-devtools/src/pages/Devtools/index.html ================================================
================================================ FILE: packages-ext/recoil-devtools/src/pages/Page/PageScript.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { DevToolsConnnectProps, RecoilDevToolsActionsType, RecoilSnapshot, } from '../../types/DevtoolsTypes'; const { ExtensionSource, ExtensionSourceContentScript, RecoilDevToolsActions, } = require('../../constants/Constants'); const EvictableList = require('../../utils/EvictableList'); const {debug} = require('../../utils/Logger'); const {serialize} = require('../../utils/Serialization'); const DefaultCustomSerialize = (item: mixed, _: string): mixed => item; async function normalizeSnapshot( snapshot: ?RecoilSnapshot, onlyDirty: boolean, props: DevToolsConnnectProps, ): Promise<{modifiedValues?: {[string]: mixed}}> { if (snapshot == null) { return {modifiedValues: undefined}; } const modifiedValues = {}; const release = snapshot.retain(); try { const dirtyNodes = snapshot.getNodes_UNSTABLE({isModified: onlyDirty}); const customSerialize = props.serializeFn ?? DefaultCustomSerialize; const subscribers = new Set(); // We wrap this loop into a promise to defer the execution // of the second (subscribers) loop, so selector // can settle before we check their values await new Promise(resolve => { for (const node of dirtyNodes) { const info = snapshot.getInfo_UNSTABLE(node); // We only accumulate subscribers if we are looking at only dirty nodes if (onlyDirty) { subscribers.add(...Array.from(info.subscribers.nodes)); } modifiedValues[node.key] = { content: serialize( customSerialize(info.loadable?.contents, node.key), props.maxDepth, props.maxItems, ), nodeType: info.type, deps: Array.from(info.deps).map(n => n.key), }; } resolve(); }); for (const node of subscribers) { if (node != null) { const info = snapshot.getInfo_UNSTABLE(node); modifiedValues[node.key] = { content: serialize( customSerialize(info.loadable?.contents, node.key), props.maxDepth, props.maxItems, ), nodeType: info.type, isSubscriber: true, deps: Array.from(info.deps).map(n => n.key), }; } } } finally { release(); } return { modifiedValues, }; } type MessageEventFromBackground = { data?: ?{ source: string, action: RecoilDevToolsActionsType, snapshotId?: number, }, }; const __RECOIL_DEVTOOLS_EXTENSION__ = { connect: (props: DevToolsConnnectProps) => { debug('CONNECT_PAGE', props); initConnection(props); const {devMode, goToSnapshot, initialSnapshot} = props; const previousSnapshots = new EvictableList<[RecoilSnapshot, () => void]>( props.persistenceLimit, ([_, release]) => release(), ); if (devMode && initialSnapshot != null) { previousSnapshots.add([initialSnapshot, initialSnapshot.retain()]); } let loadedSnapshot, releaseLoadedSnapshot; function setLoadedSnapshot(s) { releaseLoadedSnapshot?.(); [loadedSnapshot, releaseLoadedSnapshot] = [s, s?.retain()]; } setLoadedSnapshot(initialSnapshot); const backgroundMessageListener = (message: MessageEventFromBackground) => { if (message.data?.source === ExtensionSourceContentScript) { if (message.data?.action === RecoilDevToolsActions.GO_TO_SNAPSHOT) { const [snapshot] = previousSnapshots.get(message.data?.snapshotId) ?? []; setLoadedSnapshot(snapshot); if (snapshot != null) { goToSnapshot(snapshot); } } } }; window.addEventListener('message', backgroundMessageListener); // This function is called when a trasaction is detected return { disconnect() { window.removeEventListener('message', backgroundMessageListener); }, async track( txID: number, snapshot: RecoilSnapshot, _previousSnapshot: RecoilSnapshot, ) { // if we just went to a snapshot, we don't need to record a new transaction if ( loadedSnapshot != null && loadedSnapshot.getID() === snapshot.getID() ) { return; } // reset the just loaded snapshot setLoadedSnapshot(null); // On devMode we accumulate the list of rpevious snapshots // to be able to time travel if (devMode) { previousSnapshots.add([snapshot, snapshot.retain()]); } window.postMessage( { action: RecoilDevToolsActions.UPDATE, source: ExtensionSource, txID, message: await normalizeSnapshot(snapshot, true, props), }, '*', ); }, }; }, }; async function initConnection(props: DevToolsConnnectProps) { const initialValues = await normalizeSnapshot( props.initialSnapshot, false, props, ); window.postMessage( { action: RecoilDevToolsActions.INIT, source: ExtensionSource, props: { devMode: props.devMode, name: props?.name ?? document.title, persistenceLimit: props.persistenceLimit, initialValues: initialValues.modifiedValues, }, }, '*', ); } window.__RECOIL_DEVTOOLS_EXTENSION__ = __RECOIL_DEVTOOLS_EXTENSION__; if (__DEV__) { window.__RECOIL_DEVTOOLS_EXTENSION_DEV__ = __RECOIL_DEVTOOLS_EXTENSION__; } debug('EXTENSION_EXPOSED'); ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/ConnectionContext.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type Connection from '../../utils/Connection'; const React = require('react'); const ConnectionContext: React$Context = React.createContext(null); module.exports = ConnectionContext; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Devpanel.html ================================================
================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/CollapsibleItem.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {getStyle} = require('../../../utils/getStyle'); const React = require('react'); const {useState} = require('react'); const styles = { holder: { marginTop: 5, }, labelHovered: { background: '#ddd', }, labelRoot: { paddingLeft: 16, }, labelHolder: { padding: 5, }, hovered: { background: '#efefef', }, selectorHolder: { height: 5, width: 5, display: 'inline-block', marginRight: 8, }, selector: { display: 'inline-block', cursor: 'pointer', }, collapsedSelector: { borderLeft: '5px solid #bbb', borderBottom: '5px solid transparent', borderTop: '5px solid transparent', transform: 'translate(3px, 1px)', }, expandedSelector: { borderTop: '5px solid #bbb', borderRight: '5px solid transparent', borderLeft: '5px solid transparent', transform: 'translate(0px, -1px)', }, valueHolder: { marginLeft: '24px', display: 'flex', flexWrap: 'wrap', justifyContent: 'stretch', }, }; type Props = { children?: React.Node, label: React.Node, collapsible?: boolean, startCollapsed?: ?boolean, inContainer?: boolean, isRoot?: boolean, }; const noop = () => {}; function CollapsibleItem({ children, label, collapsible = true, startCollapsed = true, inContainer = false, isRoot = false, }: Props): React.Node { const [collapsed, setCollapsed] = useState(startCollapsed); const [isHovered, setIsHovered] = useState(false); return (
setIsHovered(true) : noop} onMouseLeave={isRoot ? () => setIsHovered(false) : noop}>
{collapsible && ( setCollapsed(!collapsed)} /> )} {label}
{!Boolean(collapsed) && children != null && (
{children}
)}
); } module.exports = CollapsibleItem; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/DiffItem.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import ItemDependencies from './ItemDependencies'; const {formatForDiff} = require('../../../utils/Serialization'); const ConnectionContext = require('../ConnectionContext'); const {useSelectedTransaction} = require('../useSelectionHooks'); const CollapsibleItem = require('./CollapsibleItem'); const ItemDescription = require('./ItemDescription'); const ItemLabel = require('./ItemLabel'); const ItemMoreItems = require('./ItemMoreItems'); const JsonDiff = require('jsondiffpatch-for-react').default; const nullthrows = require('nullthrows'); const React = require('react'); const {useMemo} = require('react'); const {useContext} = require('react'); const styles = { valuesHolder: { display: 'flex', justifyContent: 'stretch', width: '100%', marginTop: 10, marginBottom: 10, }, }; type Props = { name: string, startCollapsed?: boolean, isRoot?: boolean, }; function DiffItem({ name, startCollapsed = false, isRoot = false, }: Props): React.Node { const connection = nullthrows(useContext(ConnectionContext)); const [txID] = useSelectedTransaction(); const {tree} = connection; const {value, previous} = useMemo( () => ({ value: tree.get(name, txID), previous: tree.get(name, txID - 1), }), [tree, txID, name], ); return ( } startCollapsed={startCollapsed}>
); } export default DiffItem; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/Item.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Node} from '../../../types/DevtoolsTypes'; import type {SerializedValue} from '../../../utils/Serialization'; import ItemDependencies from './ItemDependencies'; import ItemValue from './ItemValue'; const ConnectionContext = require('../ConnectionContext'); const {useSelectedTransaction} = require('../useSelectionHooks'); const CollapsibleItem = require('./CollapsibleItem'); const ItemDescription = require('./ItemDescription'); const {hasItemDescription} = require('./ItemDescription'); const ItemLabel = require('./ItemLabel'); const ItemMoreItems = require('./ItemMoreItems'); const nullthrows = require('nullthrows'); const React = require('react'); const {useContext} = require('react'); type KeyProps = { name: string | number, content: SerializedValue, startCollapsed?: ?boolean, node?: ?Node, isRoot?: boolean, }; function Item({ name, content, startCollapsed, node, isRoot = false, }: KeyProps): React.Node { const connection = nullthrows(useContext(ConnectionContext)); const [txID] = useSelectedTransaction(); const deps = isRoot ? connection.dependencies.get(name.toString(), txID) : null; const hasDescription = hasItemDescription(content); return ( 0)} startCollapsed={startCollapsed} label={ <> {hasDescription ? ( ) : ( )} }>
{hasDescription && ( )} {isRoot && }
); } export default Item; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/ItemDependencies.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import Item from './Item'; const {SerializedValueType} = require('../../../utils/Serialization'); const ConnectionContext = require('../ConnectionContext'); const {useSelectedTransaction} = require('../useSelectionHooks'); const CollapsibleItem = require('./CollapsibleItem'); const nullthrows = require('nullthrows'); const React = require('react'); const {useContext} = require('react'); const styles = { label: { color: '#666', }, container: { padding: 6, background: 'white', marginTop: 6, marginBottom: 10, }, }; type Props = { name: string, }; function ItemDependencies({name}: Props): React.Node { const connection = nullthrows(useContext(ConnectionContext)); const [txID] = useSelectedTransaction(); const deps = connection.dependencies.get(name, txID); if (deps == null || deps.size === 0) { return null; } return (
{deps.size} {deps.size === 1 ? 'dependency' : 'dependencies'} }>
{Array.from(deps).map(dep => ( ))}
); } export default ItemDependencies; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/ItemDescription.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow * @format * @oncall recoil */ 'use strict'; import type {SerializedValue} from '../../../utils/Serialization'; const {SerializedValueType} = require('../../../utils/Serialization'); const React = require('react'); const styles = { description: { color: 'red', }, previous: { backgroundColor: 'red', color: 'white', marginRight: 10, padding: '2px 4px', borderRadius: 3, textDecoration: 'line-through', }, }; type KeyProps = { content: ?SerializedValue, previous?: ?SerializedValue, }; function totalLength(content: SerializedValue): number { return (content.v?.length ?? 0) + (content.e ?? 0); } const ItemDescriptionRenderers = { [SerializedValueType.error]: (): string => `Error`, [SerializedValueType.map]: (value: SerializedValue): string => `Map ${totalLength(value)} entries`, [SerializedValueType.set]: (value: SerializedValue): string => `Set ${totalLength(value)} entries`, [SerializedValueType.object]: (value: SerializedValue): string => `{} ${totalLength(value)} keys`, [SerializedValueType.array]: (value: SerializedValue): string => `[] ${totalLength(value)} items`, [SerializedValueType.function]: (): string => `Function`, [SerializedValueType.symbol]: (): string => `Symbol`, }; function ItemDescription({content}: KeyProps): React.Node { if (content == null || !hasItemDescription(content)) { return null; } // $FlowFixMe: hasItemDescription makes sure this works const description = ItemDescriptionRenderers[content.t](content); return {description}; } const hasItemDescription = function (content: ?SerializedValue): boolean { return ItemDescriptionRenderers.hasOwnProperty(content?.t); }; module.exports = ItemDescription; module.exports.hasItemDescription = hasItemDescription; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/ItemLabel.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Node} from '../../../types/DevtoolsTypes'; const {getStyle} = require('../../../utils/getStyle'); const NodeName = require('./NodeName').default; const React = require('react'); const styles = { label: { marginRight: 5, color: '#6A51B2', fontSize: 12, }, isRoot: { fontSize: 12, }, }; type KeyProps = { name: string | number, node: ?Node, isRoot?: boolean, }; function ItemLabel({name, node, isRoot = false}: KeyProps): React$MixedElement { return ( : ); } module.exports = ItemLabel; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/ItemMoreItems.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow * @format * @oncall recoil */ 'use strict'; import type {SerializedValue} from '../../../utils/Serialization'; const {SerializedValueType} = require('../../../utils/Serialization'); const React = require('react'); const styles = { description: { color: '#666', marginTop: 10, marginBottom: 10, }, }; type KeyProps = { content: ?SerializedValue, }; const Renderers = { [SerializedValueType.map]: (value: number): string => `... ${value} more entries`, [SerializedValueType.set]: (value: number): string => `... ${value} more entries`, [SerializedValueType.object]: (value: number): string => `... ${value} more keys`, [SerializedValueType.array]: (value: number): string => `... ${value} more items`, }; function ItemMoreItems({content}: KeyProps): React.Node { if ( content == null || !Renderers.hasOwnProperty(content?.t) || content.e == null || content.e === 0 ) { return null; } // $FlowFixMe: Renderers.hasOwnProperty makes sure this works const description = Renderers[content.t](content.e); return
{description}
; } module.exports = ItemMoreItems; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/ItemValue.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {SerializedValue} from '../../../utils/Serialization'; import Item from './Item'; const {SerializedValueType} = require('../../../utils/Serialization'); const React = require('react'); const styles = { blockValue: { marginTop: 0, }, value: { fontWeight: 'bold', }, }; type ValueSpanProps = { children: React.Node, }; const ValueSpan = ({children}: ValueSpanProps): React.Node => { return {children}; }; const ItemRenderers = { [SerializedValueType.null]: function SerializedValueTypeNull({}) { return null; }, [SerializedValueType.undefined]: function SerializedValueTypeUndefined({}) { return undefined; }, [SerializedValueType.array]: ({ value, startCollapsed, }: { value: $ReadOnlyArray, startCollapsed: ?boolean, }) => value.map((it, i) => ( )), [SerializedValueType.object]: ({ value, startCollapsed, }: { value: $ReadOnlyArray<$ReadOnlyArray>, startCollapsed: ?boolean, }) => { return ItemRenderers[SerializedValueType.map]({value, startCollapsed}); }, [SerializedValueType.set]: ({ value, startCollapsed, }: { value: $ReadOnlyArray, startCollapsed: ?boolean, }) => { return ItemRenderers[SerializedValueType.array]({value, startCollapsed}); }, [SerializedValueType.map]: ({ value, startCollapsed, }: { value: $ReadOnlyArray<$ReadOnlyArray>, startCollapsed: ?boolean, }) => { return value.map(([name, content], i) => ( )); }, [SerializedValueType.date]: function SerializedValueTypeDate({ value, }: { value: Date, }) { return {new Date(value).toISOString()}; }, [SerializedValueType.function]: function SerializedValueTypeFunction({ value, }: { value: string, }) { return {value}; }, [SerializedValueType.symbol]: function SerializedValueTypeSymbol({ value, }: { value: string, }) { return Symbol({value}); }, [SerializedValueType.error]: ({value}: {value: string}) => ItemRenderers[SerializedValueType.primitive]({value}), [SerializedValueType.promise]: function SerializedValueTypePromise({ value: _value, }: { value: string, }) { return Promise{''}; }, [SerializedValueType.primitive]: function SerializedValueTypePrimitive({ value, }: { value: string | number, }) { if (typeof value === 'string') { return "{value}"; } else if (typeof value?.toString === 'function') { return {value.toString()}; } return {value}; }, }; type Props = { content: ?SerializedValue, inline?: boolean, startCollapsed?: ?boolean, }; function ItemValue({ content, inline = false, startCollapsed, }: Props): React.Node { let markup = content == null ? 'undefined' : // $FlowFixMe ItemRenderers[content.t]({ // $FlowFixMe value: content.v, startCollapsed, }); return inline ? markup :
{markup}
; } export default ItemValue; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/NodeName.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Node} from '../../../types/DevtoolsTypes'; import React from 'react'; const styles = { label: { display: 'inline-block', alignItems: 'center', }, selector: { marginRight: 5, fontSize: 8, background: 'red', color: 'white', fontWeight: 'bold', borderRadius: 24, padding: '1px 2px', verticalAlign: 'middle', }, }; type KeyProps = { name: string | number, node: ?Node, }; export default function NodeName({name, node}: KeyProps): React$MixedElement { return ( {node?.type === 'selector' && ( S )} {name} ); } ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Items/index.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const DiffItem = require('./DiffItem'); const ItemDependencies = require('./ItemDependencies'); module.exports = { DiffItem, ItemDependencies, }; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupApp.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type Connection from '../../utils/Connection'; import type Store from '../../utils/Store'; const {RecoilDevToolsActions} = require('../../constants/Constants'); const ConnectionContext = require('./ConnectionContext'); const PopupComponent = require('./PopupComponent'); const React = require('react'); const {useEffect, useRef, useState} = require('react'); type AppProps = { store: Store, }; function PopupApp({ store, }: AppProps): React$Element< React$ComponentType<{children?: React$Node, value: ?Connection, ...}>, > { const tabId = chrome.devtools?.inspectedWindow?.tabId ?? null; const [selectedConnection, setSelectedConnection] = useState(tabId); const [maxTransactionId, setMaxTransactionId] = useState( store.getConnection(selectedConnection)?.transactions.getLast(), ); const port = useRef(null); // Subscribing to store events useEffect(() => { if (port.current !== null) { port.current.disconnect(); } port.current = chrome.runtime.connect(); port.current.postMessage({ action: RecoilDevToolsActions.SUBSCRIBE_POPUP, }); port.current?.onMessage.addListener(msg => { if ( msg.action === RecoilDevToolsActions.CONNECT && tabId != null && msg.connectionId === tabId ) { setSelectedConnection(tabId); } else if (msg.connectionId === selectedConnection) { if (msg.action === RecoilDevToolsActions.UPDATE_STORE) { setMaxTransactionId(msg.msgId); } else if (msg.action === RecoilDevToolsActions.DISCONNECT) { setSelectedConnection(null); } } }); }, [selectedConnection, tabId]); return ( ); } module.exports = PopupApp; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupComponent.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const ConnectionContext = require('./ConnectionContext'); const Header = require('./PopupHeader'); const Main = require('./PopupMainContent'); const Sidebar = require('./PopupSidebar'); const {useSelectedTransaction} = require('./useSelectionHooks'); const React = require('react'); const {useContext, useEffect, useState} = require('react'); const styles = { app: { textAlign: 'center', display: 'flex', height: '100vh', flexDirection: 'column', padding: '0', boxSizing: 'border-box', }, notFound: { marginTop: 16, }, body: { display: 'flex', flexGrow: 1, overflow: 'hidden', }, }; type Props = $ReadOnly<{ maxTransactionId: number, }>; const PopupComponent = ({maxTransactionId}: Props): React.Node => { const connection = useContext(ConnectionContext); const [selectedTX, setSelectedTX] = useSelectedTransaction(); useEffect(() => { // when a new transaction is detected and the previous one was selected // move to the new transaction if (maxTransactionId - 1 === selectedTX) { setSelectedTX(maxTransactionId); } }, [maxTransactionId, selectedTX, setSelectedTX]); // when switching connections, move to the last transaction useEffect(() => { if (connection != null) { setSelectedTX(connection.transactions.getLast()); } }, [connection, setSelectedTX]); const [selectedMainTab, setSelectedMainTab] = useState('Diff'); if (connection == null) { return (
No Recoil connection found.
); } return (
); }; module.exports = PopupComponent; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupDependencyGraph.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {createSankeyData} = require('../../utils/GraphUtils'); const Sankey = require('../../utils/sankey/Sankey'); const { butterflyGraphLayout, flowGraphLayout, } = require('../../utils/sankey/SankeyGraphLayout'); const ConnectionContext = require('./ConnectionContext'); const {useSelectedTransaction} = require('./useSelectionHooks'); const nullthrows = require('nullthrows'); const React = require('react'); const {useContext, useMemo, useState} = require('react'); const styles = { references: { display: 'flex', paddingLeft: 16, }, graph: { display: 'flex', }, level: { marginRight: '30px', overflow: 'visible', }, item: { padding: '5px 10px 28px', position: 'relative', textAlign: 'left', borderRadius: '3px', ':hover': { backgroundColor: '#eee', }, }, itemDecoration: { content: '""', position: 'absolute', backgroundColor: 'green', borderRadius: '30px', height: '16px', width: '16px', bottom: '10px', left: '10px', }, itemDimmed: { opacity: 0.5, }, referenceHolder: { marginRight: 24, display: 'flex', alignItems: 'center', }, referenceColor: { marginRight: 10, height: 20, width: 30, border: '1px solid black', }, button: { padding: '6px 10px', cursor: 'pointer', }, }; type ReferenceProps = { color: string, legend: string, }; function ColorReference({color, legend}: ReferenceProps): React.Node { return (
{legend}
); } function PopupDependencyGraph(): React.Node { const connection = nullthrows(useContext(ConnectionContext)); const [txID] = useSelectedTransaction(); const [focalNodeKey, setFocalNodeKey] = useState(); const {edges, nodes} = useMemo(() => { const nodeDeps = connection.dependencies.getSnapshot(txID); const nodeWeights = connection.nodesState.getSnapshot(txID); return createSankeyData(nodeDeps, nodeWeights); }, [txID, connection.dependencies, connection.nodesState]); const layout = useMemo(() => { return focalNodeKey == null ? flowGraphLayout({ nodePadding: '30%', nodeAlignment: 'entry', }) : butterflyGraphLayout(focalNodeKey, { nodePadding: '30%', depthOfField: 1000, }); }, [focalNodeKey]); return ( <>
n, getNodeName: n => (n.length < 25 ? n : `${n.substring(0, 22)}...`), getNodeValue: _n => 1, }} links={{ data: edges, getLinkValue: l => l.value, getLinkSourceKey: l => l.source, getLinkTargetKey: l => l.target, }} nodeStyles={{ fill: n => { const type = connection.getNode(n.key)?.type; return type === 'selector' ? 'red' : type === 'atom' ? 'blue' : '#ccc'; }, stroke: 'black', 'shape-rendering': 'crispEdges', cursor: 'pointer', }} nodeLabelStyles={{ fill: 'darkred', 'font-weight': 'bold', 'font-size': 'small', }} nodeEvents={{ click: (_evt, node) => setFocalNodeKey(node?.key), }} layout={layout} nodeThickness={20} linkStyles={{stroke: 'lightblue', opacity: 0.4}} linkColor={_l => 'lightblue'} getNodeTooltip={n => String(n.key)} getLinkTooltip={n => `${n.source?.key ?? ''} => ${n.target?.key ?? ''}` } />
); } module.exports = PopupDependencyGraph; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupDiff.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const ConnectionContext = require('./ConnectionContext'); const DiffItem = require('./Items/DiffItem').default; const {useSelectedTransaction} = require('./useSelectionHooks'); const React = require('react'); const {useContext} = require('react'); function PopupDiff(): React.Node { const connection = useContext(ConnectionContext); const [txID] = useSelectedTransaction(); if (connection == null) { return null; } const transaction = connection.transactions.get(txID); return ( <> {transaction?.modifiedValues?.map(({name}) => ( ))} ); } module.exports = PopupDiff; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupHeader.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {MainTabsType} from '../../constants/Constants'; const {MainTabs} = require('../../constants/Constants'); const Tabs = require('./Tabs'); const {useFilter} = require('./useSelectionHooks'); const React = require('react'); const styles = { header: { display: 'flex', borderBottom: '1px solid #ccc', minHeight: 36, background: '#E9F3FF', }, filterInput: { width: '100%', height: '100%', outline: 'none', boxSizing: 'border-box', background: 'transparent', border: 0, paddingLeft: 16, }, sidebar: { width: '30%', flexShrink: 0, flexGrow: 0, borderRight: '1px solid #ccc', }, main: { flexGrow: 1, textAlign: 'left', display: 'flex', }, }; type Props = $ReadOnly<{ selectedMainTab: MainTabsType, setSelectedMainTab: MainTabsType => void, }>; /** * @explorer-desc * DevTools Popup Header */ function PopupHeader({ selectedMainTab, setSelectedMainTab, }: Props): React.MixedElement { const [filter, setFilter] = useFilter(); return (
setFilter(e.target.value)} />
); } module.exports = PopupHeader; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupMainContent.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {MainTabsType} from '../../constants/Constants'; const {MainTabsTitle} = require('../../constants/Constants'); const ConnectionContext = require('./ConnectionContext'); const DependencyGraph = require('./PopupDependencyGraph'); const Diff = require('./PopupDiff'); const Snapshot = require('./PopupSnapshot').default; const React = require('react'); const {useContext} = require('react'); const styles = { main: { flexGrow: 1, textAlign: 'left', overflowY: 'hidden', height: '100%', maxHeight: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'stretch', backgroundColor: 'white', padding: '16px 0', boxSizing: 'border-box', }, header: { backgroundColor: '#bbb', color: 'white', flexGrow: 0, flexShrink: 0, padding: '10px 16px', }, content: { padding: '16px 0', flexGrow: 1, overflowY: 'scroll', }, head: { display: 'flex', fontWeight: 'bold', paddingLeft: 16, }, title: { fontWeight: 'bold', fontSize: '24px', marginRight: 16, }, }; type Props = $ReadOnly<{ selectedMainTab: MainTabsType, }>; function MainContent({selectedMainTab}: Props): React.Node { const connection = useContext(ConnectionContext); if (connection == null) { return null; } return (
{MainTabsTitle[selectedMainTab]}
{selectedMainTab === 'Diff' && } {selectedMainTab === 'State' && } {selectedMainTab === 'Graph' && }
); } module.exports = MainContent; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupScript.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {BackgroundPage} from '../../types/DevtoolsTypes'; const PopupApp = require('./PopupApp'); const React = require('react'); const {render} = require('react-dom'); const {RecoilRoot} = require('recoil'); chrome.runtime.getBackgroundPage(({store}: BackgroundPage) => { render( , window.document.querySelector('#app-container'), ); }); ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupSidebar.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {TransactionType} from '../../types/DevtoolsTypes'; const ConnectionContext = require('./ConnectionContext'); const Transaction = require('./PopupSidebarTransaction'); const {useSelectedTransaction} = require('./useSelectionHooks'); const {useFilter} = require('./useSelectionHooks'); const React = require('react'); const {useContext, useMemo} = require('react'); const styles = { sidebar: { backgroundColor: 'white', overflowY: 'scroll', height: '100%', maxHeight: '100%', textAlign: 'center', width: '30%', borderRight: '1px solid #ccc', flexShrink: 0, }, }; /** * @explorer-desc * DevTools Popup Sidebar */ function Sidebar(): React.MixedElement { const connection = useContext(ConnectionContext); const [selected, setSelected] = useSelectedTransaction(); const [filter] = useFilter(); const allTransactions: ?Array = connection?.transactions?.getArray(); const transactions = useMemo(() => { if (allTransactions == null) { return []; } return filter !== '' ? allTransactions.filter(tx => tx.modifiedValues.some( node => node.name.toLowerCase().indexOf(filter.toLowerCase()) !== -1, ), ) : allTransactions; }, [filter, allTransactions]); return ( ); } module.exports = Sidebar; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupSidebarTransaction.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {TransactionType} from '../../types/DevtoolsTypes'; const ConnectionContext = require('./ConnectionContext'); const NodeName = require('./Items/NodeName').default; const React = require('react'); const {useCallback, useContext} = require('react'); const styles = { transaction: { cursor: 'pointer', padding: '10px 16px', borderBottom: '1px solid #E0E0E0', ':hover': { backgroundColor: '#eee', }, }, transactionSelected: { backgroundColor: '#E9F3FF', }, itemHeader: { display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#666', }, body: { textAlign: 'left', overflowX: 'scroll', }, subscriber: { color: '#666', marginRight: 8, }, atom: { color: '#6A51B2', marginRight: 8, }, gotoButton: { border: '1px solid #666', borderRadius: 3, padding: 2, marginLeft: 16, }, }; type Props = $ReadOnly<{ transaction: TransactionType, previous: ?TransactionType, isSelected: boolean, setSelected: number => void, }>; const Transaction = ({ transaction, previous, isSelected, setSelected, }: Props): React.Node => { const connection = useContext(ConnectionContext); // When creating a new TX that is selected // scroll to make it visible const DOMNode = useCallback( node => { if (isSelected && node !== null) { node.scrollIntoView(); } }, [isSelected], ); const modifiedNodes: React.Node[] = []; const subscriberNodes: React.Node[] = []; for (let modifiedValue of transaction.modifiedValues) { const nextList = modifiedValue.isSubscriber ? subscriberNodes : modifiedNodes; nextList.push( , ); } const gotoCallback = evt => { evt.stopPropagation(); connection?.goToSnapshot(transaction.id); setSelected(transaction.id); }; return (
setSelected(transaction.id)}>
{transaction.id >= 0 ? Tx {transaction.id} : }
{previous?.ts != null ? `${(transaction.ts - previous.ts) / 1000}s` : transaction.ts.toTimeString().split(' ')[0]} {connection?.devMode && transaction.id > 0 && ( Jump )}
{modifiedNodes}
{subscriberNodes}
); }; module.exports = Transaction; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/PopupSnapshot.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const AtomList = require('./Snapshot/AtomList.js').default; const SelectorList = require('./Snapshot/SelectorList.js').default; const SnapshotSearch = require('./Snapshot/SnapshotSearch.js').default; const SearchContext = require('./Snapshot/SearchContext.js').default; const React = require('react'); const {useState} = require('react'); const styles = { container: { paddingLeft: 8, }, item: { marginBottom: 16, }, }; function SnapshotRenderer(): React.Node { const [searchVal, setSearchVal] = useState(''); return (
); } export default SnapshotRenderer; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Snapshot/AtomList.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ import ConnectionContext from '../ConnectionContext'; import Item from '../Items/Item'; import SearchContext from './SearchContext'; import {useAtomsList} from './snapshotHooks'; import React, {useContext} from 'react'; export default function AtomsList(): React$Node { const {searchVal} = useContext(SearchContext); const connection = useContext(ConnectionContext); const atoms = useAtomsList() ?? []; const filteredAtoms = atoms.filter(({name}) => name.toLowerCase().includes(searchVal.toLowerCase()), ); return ( <>

Atoms

{filteredAtoms.length > 0 ? filteredAtoms.map(({name, content}) => ( )) : 'No atoms to show.'} ); } ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Snapshot/SearchContext.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ import type {SetterOrUpdater} from '../../../../../../packages/recoil/hooks/Recoil_Hooks'; import React from 'react'; type SearchContext = { searchVal: string, setSearchVal: SetterOrUpdater, }; const context: React$Context = React.createContext({searchVal: '', setSearchVal: () => {}}); export default context; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Snapshot/SelectorList.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ import ConnectionContext from '../ConnectionContext'; import Item from '../Items/Item'; import SearchContext from './SearchContext'; import {useSelectorsList} from './snapshotHooks'; import React, {useContext} from 'react'; export default function SelectorList(): React$Node { const {searchVal} = useContext(SearchContext); const connection = useContext(ConnectionContext); const selectors = useSelectorsList() ?? []; const filteredSelectors = selectors.filter(({name}) => name.toLowerCase().includes(searchVal.toLowerCase()), ); return ( <>

Selectors

{filteredSelectors.length > 0 ? filteredSelectors.map(({name, content}) => ( )) : 'No selectors to show.'} ); } ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Snapshot/SnapshotSearch.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ import React, {useCallback, useContext, useRef} from 'react'; import debounce from '../../../utils/debounce'; import SearchContext from './SearchContext'; export default function SnapshotSearch(): React$MixedElement { const {searchVal, setSearchVal} = useContext(SearchContext); const inputRef = useRef(null); // eslint-disable-next-line react-hooks/exhaustive-deps const handleKeyDown = useCallback( debounce((_e: SyntheticKeyboardEvent) => { setSearchVal(inputRef.current?.value ?? ''); }, 300), [setSearchVal], ); return (
Search:
Currently filtering for: {searchVal}
); } ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Snapshot/snapshotHooks.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ import type {SerializedValue} from '../../../utils/Serialization'; import ConnectionContext from '../ConnectionContext'; import {useSelectedTransaction} from '../useSelectionHooks'; import {useContext, useMemo} from 'react'; export const useAtomsList = (): ?Array => { const connection = useContext(ConnectionContext); const [txID] = useSelectedTransaction(); const {snapshot, sortedKeys} = useMemo(() => { const localSnapshot = connection?.tree.getSnapshot(txID); return { snapshot: localSnapshot, sortedKeys: Object.keys(localSnapshot ?? {}).sort(), }; }, [connection, txID]); const atoms = useMemo(() => { const value = []; sortedKeys.forEach(key => { const node = connection?.getNode(key); const content = snapshot?.[key]; if (node != null && node.type === 'atom' && content != null) { value.push({name: key, content}); } }); return value; }, [connection, snapshot, sortedKeys]); if (snapshot == null || connection == null) { return null; } return atoms; }; type NodeInfo = { name: string, content: SerializedValue, }; export const useSelectorsList = (): ?Array => { const connection = useContext(ConnectionContext); const [txID] = useSelectedTransaction(); const {snapshot, sortedKeys} = useMemo(() => { const localSnapshot = connection?.tree.getSnapshot(txID); return { snapshot: localSnapshot, sortedKeys: Object.keys(localSnapshot ?? {}).sort(), }; }, [connection, txID]); const selectors = useMemo(() => { const value = []; sortedKeys.forEach(key => { const node = connection?.getNode(key); const content = snapshot?.[key]; if (node != null && node.type === 'selector' && content != null) { value.push({name: key, content}); } }); return value; }, [connection, snapshot, sortedKeys]); if (snapshot == null || connection == null) { return null; } return selectors; }; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/Tabs.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const React = require('react'); const styles = { tabs: { display: 'flex', }, tab: { backgroundColor: '#fff', padding: '0px 15px', display: 'flex', height: 36, cursor: 'pointer', fontSize: '16px', display: 'flex', alignItems: 'center', boxSizing: 'border-box', outline: 'none', borderRight: '1px solid #ccc', }, tabSelected: { backgroundColor: '#1877F2', color: 'white', fontWeight: 'bold', }, }; /** * @explorer-desc * Selecting options via Tabs */ function Tabs({ tabs, selected, onSelect, }: $ReadOnly<{ tabs: $ReadOnlyArray, selected: TType, onSelect: TType => void, }>): React.Node { return (
{tabs.map((tab, i) => { return ( onSelect(tab)}> {tab} ); })}
); } module.exports = Tabs; ================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/index.html ================================================
================================================ FILE: packages-ext/recoil-devtools/src/pages/Popup/useSelectionHooks.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {SetterOrUpdater} from 'recoil'; import {atom, useRecoilState} from 'recoil'; const FilterAtom = atom({ key: 'filter-atom', default: '', }); export const useFilter = (): [string, SetterOrUpdater] => { return useRecoilState(FilterAtom); }; const SelectecTransactionAtom = atom({ key: 'selected-tx', default: 0, }); export const useSelectedTransaction = (): [number, SetterOrUpdater] => { return useRecoilState(SelectecTransactionAtom); }; ================================================ FILE: packages-ext/recoil-devtools/src/types/DevtoolsTypes.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {SerializedValue} from '../utils/Serialization'; import type Store from '../utils/Store'; import type {Snapshot} from 'recoil'; const RecoilDevToolsActions = require('../constants/Constants'); export type RecoilSnapshot = Snapshot; export type SnapshotType = { [string]: SerializedValue, }; export type TransactionNodeType = { name: string, isSubscriber: boolean, }; export type TransactionType = { ts: Date, id: number, modifiedValues: $ReadOnlyArray, }; export type DependenciesSetType = Set; export type DependenciesSnapshotType = {[string]: DependenciesSetType}; export type NodesSnapshotType = {[string]: NodeState}; export type BackgroundPage = { store: Store, }; export type DevToolsOptions = $ReadOnly<{ name?: string, persistenceLimit?: number, maxDepth?: number, maxItems?: number, serializeFn?: (mixed, string) => mixed, initialSnapshot?: ?RecoilSnapshot, devMode: boolean, }>; export type DevToolsConnnectProps = $ReadOnly<{ ...DevToolsOptions, goToSnapshot: mixed => void, }>; export type RecoilDevToolsActionsType = $Values; export type PostMessageData = $ReadOnly<{ source?: string, action?: RecoilDevToolsActionsType, props?: DevToolsOptions, txID?: number, }>; export type ValuesMessageType = { [string]: { content: SerializedValue, nodeType: NodeTypeValues, isSubscriber?: boolean, deps: string[], }, }; export type BackgroundPostMessage = $ReadOnly<{ action?: RecoilDevToolsActionsType, message?: BackgroundPostMessageContent, txID: number, chunk?: string, isFinalChunk?: boolean, data?: ?{ initialValues: ValuesMessageType, persistenceLimit?: number, devMode?: boolean, }, }>; export type BackgroundPostMessageContent = $ReadOnly<{ modifiedValues?: ValuesMessageType, }>; export type Sender = { tab: { id: number, }, id: number, }; export type NodeTypeValues = 'selector' | 'atom' | void; // Stable data related to a node export type Node = { type: NodeTypeValues, }; // Data related to a node that may change per transaction export type NodeState = { updateCount: number, }; ================================================ FILE: packages-ext/recoil-devtools/src/utils/Connection.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { BackgroundPostMessage, DependenciesSetType, Node, NodeState, TransactionType, ValuesMessageType, } from '../types/DevtoolsTypes'; import type {SerializedValue} from './Serialization'; const {RecoilDevToolsActions} = require('../constants/Constants'); const {depsHaveChaged} = require('../utils/GraphUtils'); const EvictableList = require('./EvictableList'); const TXHashTable = require('./TXHashtable'); const nullthrows = require('nullthrows'); class Connection { id: number; displayName: string; tree: TXHashTable; dependencies: TXHashTable; transactions: EvictableList; nodes: Map; nodesState: TXHashTable; devMode: boolean; port: chrome$Port; constructor( id: number, persistenceLimit: number = 50, initialValues?: ?ValuesMessageType, displayName?: ?string, devMode?: ?boolean, port: chrome$Port, ) { this.id = nullthrows(id); this.displayName = displayName ?? 'Recoil Connection'; this.tree = new TXHashTable(persistenceLimit); this.nodesState = new TXHashTable(persistenceLimit); this.dependencies = new TXHashTable(persistenceLimit); this.transactions = new EvictableList(persistenceLimit); this.nodes = new Map(); this.devMode = devMode ?? false; this.port = port; if (initialValues != null && Object.keys(initialValues).length > 0) { this.initializeValues(initialValues); } } initializeValues(values: ValuesMessageType) { this.transactions.add({ modifiedValues: [{name: 'INIT', isSubscriber: false}], id: 0, ts: new Date(), }); this.persistValues(values, 0); } processMessage(msg: BackgroundPostMessage, _isInit: boolean = false): number { const txID = this.transactions.getNextIndex(); if (msg.message?.modifiedValues != null) { this.transactions.add({ modifiedValues: Object.keys(msg.message.modifiedValues).map(key => ({ name: key, isSubscriber: msg.message?.modifiedValues?.[key].isSubscriber === true, })), id: txID, ts: new Date(), }); this.persistValues(msg.message?.modifiedValues, txID); } return txID; } persistValues(values: ?ValuesMessageType, txID: number) { if (values == null) { return; } Object.keys(values).forEach((key: string) => { const item = values[key]; this.nodes.set(key, { type: item?.nodeType, }); this.tree.set(key, item?.content, txID); this.nodesState.set( key, { updateCount: (this.nodesState.get(key)?.updateCount ?? 0) + 1, }, txID, ); const newDeps = new Set(item?.deps ?? []); if (depsHaveChaged(this.dependencies.get(key), newDeps)) { this.dependencies.set(key, newDeps, txID); } }); } getNode(name: string | number): ?Node { return this.nodes.get(String(name)); } goToSnapshot(id: number): void { this.port?.postMessage({ action: RecoilDevToolsActions.GO_TO_SNAPSHOT, connectionId: this.id, snapshotId: id, }); } } module.exports = Connection; ================================================ FILE: packages-ext/recoil-devtools/src/utils/EvictableList.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; // TODO: make custom implementation using map for O(1) evictions class EvictableList { list: Map; capacity: number; index: number; onEvict: ?(TType) => void; constructor(maxCapacity: number = 50, onEvict?: TType => void) { this.list = new Map(); this.index = 0; this.capacity = maxCapacity; this.onEvict = onEvict; } add(elm: TType) { this.list.set(this.index, elm); this.index++; if (this.index > this.capacity) { this.evict(); } } evict() { const keyToEvict = this.list.keys().next().value; if (keyToEvict != null) { const onEvict = this.onEvict; if (onEvict) { const value = this.list.get(keyToEvict); if (value !== undefined) { onEvict(value); } } this.list.delete(keyToEvict); } } getNextIndex(): number { return this.index; } getLast(): number { return this.index - 1; } getLastValue(): ?TType { return this.index > 0 ? this.get(this.index - 1) : undefined; } getSize(): number { return this.list.size; } getIterator(): Iterable { return this.list.values(); } getArray(): Array { return Array.from(this.getIterator()); } get(id: ?number): ?TType { if (id === null || id === undefined) { return null; } return this.list.get(id); } findFirst(fn: TType => boolean): ?TType { let l = this.index - this.list.size; let r = Math.max(this.index - 1, 0); let found = undefined; while (l <= r) { const mid = Math.floor((l + r) / 2); const item = this.get(mid); if (item != null && fn(item)) { found = this.get(mid); r = mid - 1; } else { l = mid + 1; } } return found; } findLast(fn: TType => boolean): ?TType { let l = this.index - this.list.size; let r = Math.max(this.index - 1, 0); let found = undefined; while (l <= r) { const mid = Math.floor((l + r) / 2); const item = this.get(mid); if (item != null && fn(item)) { found = this.get(mid); l = mid + 1; } else { r = mid - 1; } } return found; } } module.exports = EvictableList; ================================================ FILE: packages-ext/recoil-devtools/src/utils/GraphUtils.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { DependenciesSetType, DependenciesSnapshotType, NodesSnapshotType, } from '../types/DevtoolsTypes'; const nullthrows = require('nullthrows'); function depsHaveChaged( prev: ?DependenciesSetType, next: DependenciesSetType, ): boolean { if (prev == null || next == null) { return true; } if (prev.size !== next.size) { return true; } for (const dep of next) { if (!prev.has(dep)) { return true; } } return false; } function createGraph(deps: DependenciesSnapshotType): { // TODO: define proper types levels: $ReadOnlyArray<$ReadOnlyArray>, edges: $ReadOnlyArray, } { const nodes = new Map(); const edges = []; const levels = [[]]; let queue = Object.keys(deps); let solved; let it = 0; do { it++; solved = 0; const newQueue = []; for (const key of queue) { const blockers = deps[key]; let add = true; let level = 0; const links = []; for (const blocker of blockers) { if (nodes.has(blocker)) { const info = nodes.get(blocker); level = Math.max(level, nullthrows(info)[0] + 1); links.push(info); } else { add = false; break; } } if (add) { if (!levels[level]) { levels[level] = []; } const coors = [level, levels[level].length]; nodes.set(key, coors); levels[level].push(key); links.forEach(link => { edges.push([link, coors]); }); solved++; } else { newQueue.push(key); } } queue = newQueue; } while (solved > 0 && queue.length && it < 10); return {levels, edges}; } function flattenLevels( levels: $ReadOnlyArray<$ReadOnlyArray>, ): $ReadOnlyArray<{x: number, y: number, name: string}> { const result = []; levels.forEach((level, x) => { level.forEach((name, y) => { result.push({x, y, name}); }); }); return result; } function createSankeyData( deps: DependenciesSnapshotType, nodeWeights: NodesSnapshotType, ): { nodes: string[], edges: $ReadOnlyArray<{value: number, source: string, target: string}>, } { const nodes = Object.keys(deps); const edges = nodes.reduce((agg, target) => { agg.push( ...Array.from(deps[target]).map(source => ({ value: nodeWeights[source]?.updateCount ?? 1, source, target, })), ); return agg; }, []); return {nodes, edges}; } module.exports = { createGraph, depsHaveChaged, flattenLevels, createSankeyData, }; ================================================ FILE: packages-ext/recoil-devtools/src/utils/Logger.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; function debug(...args: $ReadOnlyArray) { if (__DEV__) { /* eslint-disable-next-line fb-www/no-console */ console.log(...args); } } function warn(...args: $ReadOnlyArray) { if (typeof console !== 'undefined') { console.warn(...args); } } module.exports = {debug, warn}; ================================================ FILE: packages-ext/recoil-devtools/src/utils/ObjectEntries.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict * @format * @oncall recoil */ // flowlint ambiguous-object-type:error /** * The return type of Object.entries() in Flow is Array>, * even if the object's type is stricter. * * This helper provides a way to carry Flow typing through to the result. * * BEWARE: `Object.entries` coerces to numeric keys to strings, * so this function does too. E.g., `objectEntries({1: 'lol'})` is * equivalent to `[['1', 'lol']]` and NOT `[[1, 'lol']]`. Thus, Flow will * incorrectly type the return in these cases. */ export default function objectEntries(obj: { +[TKey]: TValue, ... }): Array<[TKey, TValue]> { if (__DEV__) { if (obj instanceof Map) { // eslint-disable-next-line fb-www/no-console console.error( "objectEntries doesn't work on Map instances; use instance.entries() instead", ); } } // $FlowFixMe[unclear-type] return (Object.entries(obj): any); } ================================================ FILE: packages-ext/recoil-devtools/src/utils/ObjectValues.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict * @format * @oncall recoil */ // flowlint ambiguous-object-type:error 'use strict'; /** * The return type of Object.values() in Flow is Array. This is because * Flow doesn't require objects to be $Exact, so it cannot guarantee that * an object matching `{foo: string}` isn't actually `{foo: 'bar', baz: 123}` at * runtime. * * But... for code using object-as-map, e.g. `{[fooID: FBID]: Foo}`, this is * just too common. So wrap Flow and lie slightly about the types. */ export default function objectValues(obj: { +[key: mixed]: TValue, ... }): Array { // $FlowFixMe[unclear-type] return (Object.values(obj): any); } ================================================ FILE: packages-ext/recoil-devtools/src/utils/Serialization.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow * @format * @oncall recoil */ 'use strict'; const SerializedValueType = Object.freeze({ null: '0', undefined: '1', set: '2', map: '3', object: '4', error: '5', array: '6', primitive: '7', function: '8', promise: '9', date: 'a', symbol: 'b', }); export type SupportedSerializedValueTypes = $Values; export type SerializedValue = { t: SupportedSerializedValueTypes, v?: any, e?: number, }; const DefaultMaxDepth = 5; const DefaultMaxItems = 1000; /* Converts mixed values to a representation that can be serialized * by window.postMessage */ // TODO: Handle promises and immutable function serialize( item: mixed, maxDepth: number = DefaultMaxDepth, maxItems: number = DefaultMaxItems, parents?: ?Set, depth: number = 0, ): SerializedValue { if (parents == null) { parents = new Set(); } if (depth > maxDepth) { return {t: SerializedValueType.primitive, v: '[Max Depth Reached]'}; } if (item && typeof item === 'object') { if (parents.has(item)) { return {t: SerializedValueType.primitive, v: '[Circular Reference]'}; } parents.add(item); } let serialized: ?SerializedValue = null; if (item === null) { serialized = {t: SerializedValueType.null}; } else if (item === undefined) { serialized = {t: SerializedValueType.undefined}; } else if (Array.isArray(item)) { const {iterable, exceeds} = getExceedingItems(item, maxItems); serialized = maybeAddExceeds( { t: SerializedValueType.array, v: iterable.map(it => serialize(it, maxDepth, maxItems, parents, depth + 1), ), }, exceeds, ); } else if (item instanceof Set) { const {iterable, exceeds} = getExceedingItems( Array.from(item), maxItems, ); serialized = maybeAddExceeds( { t: SerializedValueType.set, v: iterable.map(it => serialize(it, maxDepth, maxItems, parents, depth + 1), ), }, exceeds, ); } else if (item instanceof Map) { const {iterable, exceeds} = getExceedingItems<[mixed, mixed]>( Array.from(item.entries()), maxItems, ); serialized = maybeAddExceeds( { t: SerializedValueType.map, v: iterable.map(entry => entry.map(it => serialize(it, maxDepth, maxItems, parents, depth + 1), ), ), }, exceeds, ); } else if (item instanceof Promise || typeof item?.then === 'function') { serialized = {t: SerializedValueType.promise, v: 'Promise'}; } else if (item instanceof Error) { serialized = { t: SerializedValueType.error, v: capStringLength(item.toString(), 150), }; } else if (item instanceof Date) { serialized = {t: SerializedValueType.date, v: item.getTime()}; } else if (typeof item === 'symbol') { serialized = { t: SerializedValueType.symbol, v: item.description, }; } else if (typeof item === 'function') { serialized = { t: SerializedValueType.function, v: capStringLength(String(item), 150), }; } else if (typeof item === 'object') { const {iterable, exceeds} = getExceedingItems<[string, mixed]>( Object.entries(item), maxItems, ); serialized = maybeAddExceeds( { t: SerializedValueType.object, v: iterable.map(entry => entry.map(it => serialize(it, maxDepth, maxItems, parents, depth + 1), ), ), }, exceeds, ); } else { serialized = {t: SerializedValueType.primitive, v: item}; } if (item && typeof item === 'object') { parents.delete(item); } return serialized; } function maybeAddExceeds( value: SerializedValue, exceeds: number, ): SerializedValue { if (exceeds > 0) { value.e = exceeds; } return value; } function getExceedingItems( items: $ReadOnlyArray, maxItems: number, ): {exceeds: number, iterable: $ReadOnlyArray} { if (items.length > maxItems) { return { iterable: items.slice(0, maxItems), exceeds: items.length - maxItems, }; } return {iterable: items, exceeds: 0}; } /* * Restores a value that was treated by `serialize` function */ // TODO: Handle promises and immutable function deserialize(item: ?SerializedValue): mixed { if (item == null) { return null; } const {v: value, t: type} = item; if (type === SerializedValueType.null) { return null; } else if (type === SerializedValueType.undefined) { return undefined; } else if (type === SerializedValueType.set) { return new Set(value?.map(deserialize)); } else if (type === SerializedValueType.map) { return new Map(value?.map(entry => entry.map(deserialize))); } else if (type === SerializedValueType.date) { return new Date(value ?? 0); } else if (type === SerializedValueType.function) { // function cannot be restored :( return value; } else if (type === SerializedValueType.error) { // Errors are shown as strings return value; } else if (type === SerializedValueType.symbol) { return Symbol(value); } else if (type === SerializedValueType.array) { return value?.map(deserialize); } else if (type === SerializedValueType.object) { return value?.reduce((prev, [key, val]) => { prev[deserialize(key)] = deserialize(val); return prev; }, {}); } else if (type === SerializedValueType.function) { return String(item); } else { return value; } } function formatForDiff(item: ?SerializedValue): mixed { if (item == null) { return 'undefined'; } const {v: value, t: type} = item; if (type === SerializedValueType.null) { return null; } else if (type === SerializedValueType.undefined) { return 'undefined'; } else if (type === SerializedValueType.set) { return value?.map(formatForDiff); } else if (type === SerializedValueType.map) { return value?.reduce((prev, [key, val]) => { prev[formatForDiff(key)] = formatForDiff(val); return prev; }, {}); } else if (type === SerializedValueType.date) { return new Date(value ?? 0); } else if (type === SerializedValueType.error) { // Errors are compared as strings return value; } else if (type === SerializedValueType.function) { // function cannot be restored :( return value; } else if (type === SerializedValueType.symbol) { return Symbol(value); } else if (type === SerializedValueType.array) { return value?.map(formatForDiff); } else if (type === SerializedValueType.object) { return value?.reduce((prev, [key, val]) => { prev[formatForDiff(key)] = formatForDiff(val); return prev; }, {}); } else if (type === SerializedValueType.function) { return String(item); } else { return value; } } function capStringLength(str: string, newLength: number): string { if (str.length > 150) { return `${str.slice(0, newLength)} (...)`; } return str; } module.exports = {serialize, deserialize, formatForDiff, SerializedValueType}; ================================================ FILE: packages-ext/recoil-devtools/src/utils/Store.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type { BackgroundPostMessage, RecoilDevToolsActionsType, ValuesMessageType, } from '../types/DevtoolsTypes'; const {RecoilDevToolsActions} = require('../constants/Constants'); const Connection = require('./Connection'); class Store { connections: Map; connectionIndex: number; subscriptions: Set; lastConnection: ?string; constructor() { this.connections = new Map(); this.connectionIndex = 0; this.subscriptions = new Set(); } connect( connectionId: number, persistenceLimit?: number, initialValues?: ValuesMessageType, displayName: ?string, devMode: ?boolean, port: chrome$Port, ) { this.connections.set( connectionId, new Connection( connectionId, persistenceLimit, initialValues, displayName, devMode, port, ), ); this.connectionIndex++; this.trigger(RecoilDevToolsActions.CONNECT, {connectionId}); } disconnect(connectionId: number): void { this.connections.delete(connectionId); this.trigger(RecoilDevToolsActions.DISCONNECT, {connectionId}); } hasConnection(id: number): boolean { return this.connections.has(id); } getConnection(id: ?number): ?Connection { if (id == null) { return null; } return this.connections.get(id); } getConnectionsArray(): Array { return Array.from(this.connections.values()); } getNewConnectionIndex(): number { return this.connectionIndex; } getLastConnectionId(): number { return Array.from(this.connections)[this.connections.size - 1]?.[0] ?? null; } subscribe(popup: chrome$Port) { if (!this.subscriptions.has(popup)) { this.subscriptions.add(popup); } } unsubscribe(popup: chrome$Port) { this.subscriptions.delete(popup); } processMessage(msg: BackgroundPostMessage, connectionId: number) { const connection = this.connections.get(connectionId); if (connection == null) { return; } const msgId = connection.processMessage(msg); this.trigger(RecoilDevToolsActions.UPDATE_STORE, {connectionId, msgId}); } trigger( evt: RecoilDevToolsActionsType, data: {connectionId: number, msgId?: number, ...}, ): void { for (const popup of this.subscriptions) { popup.postMessage({ action: evt, connectionId: data.connectionId, msgId: data.msgId, }); } } } module.exports = Store; ================================================ FILE: packages-ext/recoil-devtools/src/utils/TXHashtable.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const nullthrows = require('nullthrows'); const EvictableList = require('./EvictableList'); class TXHashTable { map: Map< string, EvictableList<{ transactionId: number, timestamp: number, value: TBaseItem, }>, >; persistenceLimit: number; // TODO: add persistenceLimit and evictions constructor(persistenceLimit?: number = 50) { this.map = new Map(); this.persistenceLimit = persistenceLimit; } reset(): void { this.map = new Map(); } set(atomName: string, value: ?TBaseItem, transactionId: number): void { if (value == null) { return; } if (!this.map.has(atomName)) { this.map.set( atomName, new EvictableList<{ transactionId: number, timestamp: number, value: TBaseItem, }>(this.persistenceLimit), ); } nullthrows(this.map.get(atomName)).add({ transactionId, timestamp: Date.now(), value, }); } get(atomName: string, transactionId?: ?number): ?TBaseItem { const data = this.map.get(atomName); if (data == null || data.getSize() === 0) { return undefined; // or null? } const foundItem = transactionId == null ? data.getLastValue() : data.findLast( item => item != null && item.transactionId <= transactionId, ); if (foundItem == null) { return undefined; } return foundItem.value; } // TODO: memoize getSnapshot(transactionId?: number): {[string]: TBaseItem} { const data = {}; for (const atomName of this.map.keys()) { const value = this.get(atomName, transactionId); if (value !== undefined) { data[atomName] = value; } } return data; } } module.exports = TXHashTable; ================================================ FILE: packages-ext/recoil-devtools/src/utils/__tests__/EvictableListTest.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const EvictableList = require('../EvictableList'); describe('EvictableList tests', () => { const list = new EvictableList(3); list.add('a'); list.add('b'); list.add('c'); it('Initializing and add elements', () => { expect(list.get(0)).toEqual('a'); expect(list.get(1)).toEqual('b'); expect(list.get(2)).toEqual('c'); expect(list.getSize()).toEqual(3); expect(list.getLast()).toEqual(2); }); it('Getting iterators', () => { const iterator = list.getIterator(); const found = []; for (const item of iterator) { found.push(item); } expect(found).toEqual(['a', 'b', 'c']); expect(list.getArray()).toEqual(found); }); it('Evicting older elements', () => { const list = new EvictableList(3); list.add('a'); list.add('b'); list.add('c'); list.add('d'); list.add('e'); const iterator = list.getIterator(); const found = []; for (const item of iterator) { found.push(item); } expect(found).toEqual(['c', 'd', 'e']); expect(list.getArray()).toEqual(found); expect(list.get(0)).toEqual(undefined); expect(list.get(1)).toEqual(undefined); expect(list.get(2)).toEqual('c'); expect(list.get(3)).toEqual('d'); expect(list.get(4)).toEqual('e'); expect(list.getSize()).toEqual(3); expect(list.getLast()).toEqual(4); }); }); describe('EvictableList find first elements with a condition', () => { const list = new EvictableList(3); it('find first among added elements', () => { list.add(10); list.add(15); list.add(20); expect(list.findFirst(n => n > 8)).toEqual(10); expect(list.findFirst(n => n >= 15)).toEqual(15); expect(list.findFirst(n => n > 15)).toEqual(20); expect(list.findFirst(n => Boolean(n))).toEqual(10); }); it("don't include evicted element in the results", () => { list.add(25); list.add(30); expect(list.findFirst(n => n > 8)).toEqual(20); expect(list.findFirst(n => n >= 15)).toEqual(20); expect(list.findFirst(n => n > 15)).toEqual(20); expect(list.findFirst(n => Boolean(n))).toEqual(20); expect(list.findFirst(n => n > 25)).toEqual(30); }); }); describe('EvictableList binary search even elements', () => { const list = new EvictableList(8); list.add(4); list.add(8); list.add(10); list.add(12); list.add(15); list.add(20); list.add(25); list.add(30); it('find element', () => { expect(list.findFirst(n => n >= 2)).toEqual(4); expect(list.findFirst(n => n > 4)).toEqual(8); expect(list.findFirst(n => n >= 9)).toEqual(10); expect(list.findFirst(n => n >= 14)).toEqual(15); expect(list.findFirst(n => n >= 28)).toEqual(30); }); }); describe('EvictableList binary search with non-full list', () => { const list = new EvictableList(8); list.add(4); list.add(8); list.add(10); list.add(12); it('find element', () => { expect(list.findFirst(n => n >= 2)).toEqual(4); expect(list.findFirst(n => n > 4)).toEqual(8); expect(list.findFirst(n => n >= 9)).toEqual(10); expect(list.findFirst(n => n >= 11)).toEqual(12); }); }); describe('EvictableList binary search non-full odd elements', () => { const list = new EvictableList(9); list.add(4); list.add(8); list.add(10); list.add(12); list.add(15); it('find element', () => { expect(list.findFirst(n => n >= 4)).toEqual(4); expect(list.findFirst(n => n >= 9)).toEqual(10); expect(list.findFirst(n => n >= 11)).toEqual(12); expect(list.findFirst(n => n >= 15)).toEqual(15); }); }); describe('EvictableList find last elements with a condition', () => { const list = new EvictableList(3); it('find first among added elements', () => { list.add(10); list.add(15); list.add(20); expect(list.findLast(n => n < 8)).toEqual(undefined); expect(list.findLast(n => n <= 15)).toEqual(15); expect(list.findLast(n => n < 15)).toEqual(10); expect(list.findLast(n => n < 100)).toEqual(20); }); it("don't include evicted element in the results", () => { list.add(25); list.add(30); expect(list.findLast(n => n < 8)).toEqual(undefined); expect(list.findLast(n => n <= 15)).toEqual(undefined); expect(list.findLast(n => n > 15)).toEqual(30); expect(list.findLast(n => Boolean(n))).toEqual(30); expect(list.findLast(n => n <= 25)).toEqual(25); }); }); describe('EvictableList find last binary search even elements', () => { const list = new EvictableList(8); list.add(4); list.add(8); list.add(10); list.add(12); list.add(15); list.add(20); list.add(25); list.add(30); it('find element', () => { expect(list.findLast(n => n >= 2)).toEqual(30); expect(list.findLast(n => n <= 2)).toEqual(undefined); expect(list.findLast(n => n < 10)).toEqual(8); expect(list.findLast(n => n <= 10)).toEqual(10); expect(list.findLast(n => n <= 18)).toEqual(15); expect(list.findLast(n => n <= 50)).toEqual(30); }); }); describe('EvictableList find last binary search with non-full list', () => { const list = new EvictableList(8); list.add(4); list.add(8); list.add(10); list.add(12); it('find element', () => { expect(list.findLast(n => n >= 2)).toEqual(12); expect(list.findLast(n => n < 4)).toEqual(undefined); expect(list.findLast(n => n <= 4)).toEqual(4); expect(list.findLast(n => n <= 9)).toEqual(8); expect(list.findLast(n => n <= 12)).toEqual(12); }); }); describe('EvictableList find last binary search non-full odd elements', () => { const list = new EvictableList(9); list.add(4); list.add(8); list.add(10); list.add(12); list.add(15); it('find element', () => { expect(list.findLast(n => n <= 4)).toEqual(4); expect(list.findLast(n => n <= 9)).toEqual(8); expect(list.findLast(n => n <= 11)).toEqual(10); expect(list.findLast(n => n <= 15)).toEqual(15); }); }); ================================================ FILE: packages-ext/recoil-devtools/src/utils/__tests__/Recoil_DevTools_GraphUtils.test.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const {createGraph, depsHaveChaged} = require('../GraphUtils'); describe('base cases', () => { const emptySet = new Set(); it('empty snapshot and deps', () => { expect(createGraph({})).toEqual({levels: [[]], edges: []}); }); it('only snapshot', () => { expect(createGraph({a: new Set(), b: new Set()})).toEqual({ levels: [['a', 'b']], edges: [], }); }); it('snapshot with single dep', () => { expect(createGraph({a: emptySet, b: emptySet, c: new Set(['a'])})).toEqual({ levels: [['a', 'b'], ['c']], edges: [ [ [0, 0], [1, 0], ], ], }); }); it('more deps', () => { expect( createGraph({ c: new Set(['b']), d: new Set(['a', 'b']), a: emptySet, b: emptySet, }), ).toEqual({ levels: [ ['a', 'b'], ['c', 'd'], ], edges: [ [ [0, 1], [1, 0], ], [ [0, 0], [1, 1], ], [ [0, 1], [1, 1], ], ], }); }); it('nested deps', () => { expect( createGraph({ a: emptySet, b: emptySet, e: new Set(['c']), c: new Set(['b']), d: new Set(['a', 'b']), }), ).toEqual({ levels: [['a', 'b'], ['c', 'd'], ['e']], edges: [ [ [0, 1], [1, 0], ], [ [0, 0], [1, 1], ], [ [0, 1], [1, 1], ], [ [1, 0], [2, 0], ], ], }); }); it('not found deps are ignored', () => { expect( createGraph({ a: emptySet, b: emptySet, e: new Set(['c']), c: new Set(['b']), d: new Set(['a', 'b']), f: new Set(['g', 'a']), }), ).toEqual({ levels: [['a', 'b'], ['c', 'd'], ['e']], edges: [ [ [0, 1], [1, 0], ], [ [0, 0], [1, 1], ], [ [0, 1], [1, 1], ], [ [1, 0], [2, 0], ], ], }); }); }); describe('depsHaveChaged util', () => { const newSet = new Set(['a', 'b']); expect(depsHaveChaged(null, newSet)).toBeTruthy(); expect(depsHaveChaged(new Set(['a']), newSet)).toBeTruthy(); expect(depsHaveChaged(new Set(['a', 'b', 'c']), newSet)).toBeTruthy(); expect(depsHaveChaged(new Set(['a', 'b']), newSet)).toBeFalsy(); expect(depsHaveChaged(new Set(['b', 'a']), newSet)).toBeFalsy(); }); ================================================ FILE: packages-ext/recoil-devtools/src/utils/__tests__/Recoil_DevTools_TXHashtable.test.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const HashTable = require('../TXHashtable'); describe('Timestamp hashtable', () => { const hash = new HashTable(); it('creates object with methods', () => { expect(hash).toBeDefined(); expect(hash.get).toBeDefined(); expect(hash.getSnapshot).toBeDefined(); expect(hash.set).toBeDefined(); }); it('get empty snapshot before transactions', () => { expect(hash.getSnapshot(0)).toEqual({}); }); it('simple set', () => { hash.set('test', 99, 5); expect(hash.get('test')).toEqual(99); expect(hash.get('test', 5)).toEqual(99); expect(hash.get('test', 7)).toEqual(99); expect(hash.get('test', 2)).toEqual(undefined); }); it('adding values for same key', () => { hash.set('test', 199, 10); expect(hash.get('test')).toEqual(199); expect(hash.get('test', 5)).toEqual(99); expect(hash.get('test', 7)).toEqual(99); expect(hash.get('test', 2)).toEqual(undefined); expect(hash.get('test', 10)).toEqual(199); expect(hash.get('test', 12)).toEqual(199); }); it('get latest snapshot', () => { hash.set('other', 199, 7); expect(hash.getSnapshot()).toEqual({other: 199, test: 199}); }); it('get timed snapshot', () => { expect(hash.getSnapshot(7)).toEqual({other: 199, test: 99}); }); it('get timed snapshot with missing keys', () => { expect(hash.getSnapshot(5)).toEqual({test: 99}); }); }); ================================================ FILE: packages-ext/recoil-devtools/src/utils/__tests__/SerializationTest.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; const { SerializedValueType, deserialize, serialize, } = require('../Serialization'); describe('Preparing objects to be sent via postMessage', () => { it('string', () => { expect(serialize('test')).toEqual({ t: SerializedValueType.primitive, v: 'test', }); expect(serialize('')).toEqual({t: SerializedValueType.primitive, v: ''}); }); it('number', () => { expect(serialize(90)).toEqual({t: SerializedValueType.primitive, v: 90}); expect(serialize(-90)).toEqual({ t: SerializedValueType.primitive, v: -90, }); }); it('boolean', () => { expect(serialize(true)).toEqual({ t: SerializedValueType.primitive, v: true, }); expect(serialize(false)).toEqual({ t: SerializedValueType.primitive, v: false, }); }); it('empty values', () => { expect(serialize(null)).toEqual({ t: SerializedValueType.null, }); expect(serialize(undefined)).toEqual({ t: SerializedValueType.undefined, }); }); it('arrays', () => { expect(serialize([1, 2, 3])).toEqual({ t: SerializedValueType.array, v: [ {t: SerializedValueType.primitive, v: 1}, {t: SerializedValueType.primitive, v: 2}, {t: SerializedValueType.primitive, v: 3}, ], }); }); it('objects', () => { expect(serialize({a: 3})).toEqual({ t: SerializedValueType.object, v: [ [ {t: SerializedValueType.primitive, v: 'a'}, {t: SerializedValueType.primitive, v: 3}, ], ], }); expect(serialize({b: new Set([1, 2])})).toEqual({ t: SerializedValueType.object, v: [ [ {t: SerializedValueType.primitive, v: 'b'}, { t: SerializedValueType.set, v: [ {t: SerializedValueType.primitive, v: 1}, {t: SerializedValueType.primitive, v: 2}, ], }, ], ], }); }); it('dates', () => { expect(serialize(new Date(12345678))).toEqual({ t: SerializedValueType.date, v: 12345678, }); }); it('maps', () => { const map = new Map([ ['test', 1234], ['other', 'serialized'], ]); expect(serialize(map)).toEqual({ t: SerializedValueType.map, v: [ [ { t: SerializedValueType.primitive, v: 'test', }, { t: SerializedValueType.primitive, v: 1234, }, ], [ { t: SerializedValueType.primitive, v: 'other', }, { t: SerializedValueType.primitive, v: 'serialized', }, ], ], }); map.set('nested', new Set([9, 2, 3])); expect(serialize(map)).toEqual({ t: SerializedValueType.map, v: [ [ { t: SerializedValueType.primitive, v: 'test', }, { t: SerializedValueType.primitive, v: 1234, }, ], [ { t: SerializedValueType.primitive, v: 'other', }, { t: SerializedValueType.primitive, v: 'serialized', }, ], [ { t: SerializedValueType.primitive, v: 'nested', }, { t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: 9, }, { t: SerializedValueType.primitive, v: 2, }, { t: SerializedValueType.primitive, v: 3, }, ], }, ], ], }); const nonPrimitiveKeys = new Map(); nonPrimitiveKeys.set(new Map([[2, 3]]), 'test'); expect(serialize(nonPrimitiveKeys)).toEqual({ t: SerializedValueType.map, v: [ [ { t: SerializedValueType.map, v: [ [ { t: SerializedValueType.primitive, v: 2, }, { t: SerializedValueType.primitive, v: 3, }, ], ], }, { t: SerializedValueType.primitive, v: 'test', }, ], ], }); }); it('sets', () => { const set = new Set(['test', 1234]); expect(serialize(set)).toEqual({ t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: 'test', }, { t: SerializedValueType.primitive, v: 1234, }, ], }); set.add(new Set([1, 2, 3])); expect(serialize(set)).toEqual({ t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: 'test', }, { t: SerializedValueType.primitive, v: 1234, }, { t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: 1, }, { t: SerializedValueType.primitive, v: 2, }, { t: SerializedValueType.primitive, v: 3, }, ], }, ], }); }); it('symbols', () => { expect(serialize(Symbol('test'))).toEqual({ t: SerializedValueType.symbol, v: 'test', }); }); it('errors', () => { expect(serialize(new Error('test'))).toEqual({ t: SerializedValueType.error, v: 'Error: test', }); expect( serialize( new Error( 'test 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890', ), ), ).toEqual({ t: SerializedValueType.error, v: 'Error: test 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 (...)', }); }); }); describe('Restore objects prepared with `serialze`', () => { it('string', () => { expect(deserialize({t: SerializedValueType.primitive, v: 'test'})).toEqual( 'test', ); expect(deserialize({t: SerializedValueType.primitive, v: ''})).toEqual(''); }); it('number', () => { expect(deserialize({t: SerializedValueType.primitive, v: 90})).toEqual(90); expect(deserialize({t: SerializedValueType.primitive, v: -90})).toEqual( -90, ); }); it('boolean', () => { expect(deserialize({t: SerializedValueType.primitive, v: true})).toEqual( true, ); expect(deserialize({t: SerializedValueType.primitive, v: false})).toEqual( false, ); }); it('empty values', () => { expect( deserialize({ t: SerializedValueType.null, }), ).toEqual(null); expect( deserialize({ t: SerializedValueType.undefined, }), ).toEqual(undefined); }); it('arrays', () => { expect( deserialize({ t: SerializedValueType.array, v: [ {t: SerializedValueType.primitive, v: 1}, {t: SerializedValueType.primitive, v: 2}, {t: SerializedValueType.primitive, v: 3}, ], }), ).toEqual([1, 2, 3]); }); it('objects', () => { expect( deserialize({ t: SerializedValueType.object, v: [ [ {t: SerializedValueType.primitive, v: 'a'}, {t: SerializedValueType.primitive, v: 3}, ], ], }), ).toEqual({a: 3}); expect( deserialize({ t: SerializedValueType.object, v: [ [ {t: SerializedValueType.primitive, v: 'b'}, { t: SerializedValueType.set, v: [ {t: SerializedValueType.primitive, v: 1}, {t: SerializedValueType.primitive, v: 2}, ], }, ], ], }), ).toEqual({b: new Set([1, 2])}); }); it('dates', () => { expect( deserialize({ t: SerializedValueType.date, v: 12345678, }), ).toEqual(new Date(12345678)); }); it('maps', () => { const map = new Map([ ['test', 1234], ['other', 'serialized'], ]); expect( deserialize({ t: SerializedValueType.map, v: [ [ { t: SerializedValueType.primitive, v: 'test', }, { t: SerializedValueType.primitive, v: 1234, }, ], [ { t: SerializedValueType.primitive, v: 'other', }, { t: SerializedValueType.primitive, v: 'serialized', }, ], ], }), ).toEqual(map); map.set('nested', new Set([9, 2, 3])); expect( deserialize({ t: SerializedValueType.map, v: [ [ { t: SerializedValueType.primitive, v: 'test', }, { t: SerializedValueType.primitive, v: 1234, }, ], [ { t: SerializedValueType.primitive, v: 'other', }, { t: SerializedValueType.primitive, v: 'serialized', }, ], [ { t: SerializedValueType.primitive, v: 'nested', }, { t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: 9, }, { t: SerializedValueType.primitive, v: 2, }, { t: SerializedValueType.primitive, v: 3, }, ], }, ], ], }), ).toEqual(map); const nonPrimitiveKeys = new Map(); nonPrimitiveKeys.set(new Map([[2, 3]]), 'test'); expect( deserialize({ t: SerializedValueType.map, v: [ [ { t: SerializedValueType.map, v: [ [ { t: SerializedValueType.primitive, v: 2, }, { t: SerializedValueType.primitive, v: 3, }, ], ], }, { t: SerializedValueType.primitive, v: 'test', }, ], ], }), ).toEqual(nonPrimitiveKeys); }); it('sets', () => { const set = new Set(['test', 1234]); expect( deserialize({ t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: 'test', }, { t: SerializedValueType.primitive, v: 1234, }, ], }), ).toEqual(set); set.add(new Set([1, 2, 3])); expect( deserialize({ t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: 'test', }, { t: SerializedValueType.primitive, v: 1234, }, { t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: 1, }, { t: SerializedValueType.primitive, v: 2, }, { t: SerializedValueType.primitive, v: 3, }, ], }, ], }), ).toEqual(set); }); it('Symbol', () => { const symbol = ((deserialize({ t: SerializedValueType.symbol, v: 'test', }): any): Symbol); expect(typeof symbol).toEqual('symbol'); expect(symbol.description).toEqual('test'); }); it('errors', () => { expect( deserialize({ t: SerializedValueType.error, v: 'Error: test', }), ).toEqual('Error: test'); }); }); describe('Serializing circular references', () => { const data = {a: 2, c: {}}; data.c = data; it('avoid circular references in first level keys', () => { expect(serialize(data)).toEqual({ t: SerializedValueType.object, v: [ [ {t: SerializedValueType.primitive, v: 'a'}, {t: SerializedValueType.primitive, v: 2}, ], [ {t: SerializedValueType.primitive, v: 'c'}, {t: SerializedValueType.primitive, v: '[Circular Reference]'}, ], ], }); }); }); describe('Serializing nested circular references', () => { const data = {a: 2, c: {}}; data.c = {a: 2, c: data}; it('avoid circular references in nested objects (only by reference)', () => { expect(serialize(data)).toEqual({ t: SerializedValueType.object, v: [ [ {t: SerializedValueType.primitive, v: 'a'}, {t: SerializedValueType.primitive, v: 2}, ], [ {t: SerializedValueType.primitive, v: 'c'}, { t: SerializedValueType.object, v: [ [ {t: SerializedValueType.primitive, v: 'a'}, {t: SerializedValueType.primitive, v: 2}, ], [ {t: SerializedValueType.primitive, v: 'c'}, {t: SerializedValueType.primitive, v: '[Circular Reference]'}, ], ], }, ], ], }); }); }); describe('Serializing circular references within arrays and sets', () => { const data = []; const b = [data]; data.push(b); it('avoid circular references in nested objects (only by reference)', () => { expect(serialize(data)).toEqual({ t: SerializedValueType.array, v: [ { t: SerializedValueType.array, v: [{t: SerializedValueType.primitive, v: '[Circular Reference]'}], }, ], }); }); const set = new Set(); const c = new Set(); c.add(set); set.add(c); it('avoid circular references in nested objects (only by reference)', () => { expect(serialize(set)).toEqual({ t: SerializedValueType.set, v: [ { t: SerializedValueType.set, v: [{t: SerializedValueType.primitive, v: '[Circular Reference]'}], }, ], }); }); }); describe('serialize object with depth greater than allowed', () => { const data = { '1': { '2': { '3': { '4': { '5': { '6': { '7': {t: 2}, }, }, }, }, }, }, }; it('more than 5 levels', () => { expect(serialize(data)).toEqual({ t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: '1', }, { t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: '2', }, { t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: '3', }, { t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: '4', }, { t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: '5', }, { t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: '[Max Depth Reached]', }, { t: SerializedValueType.primitive, v: '[Max Depth Reached]', }, ], ], }, ], ], }, ], ], }, ], ], }, ], ], }, ], ], }); }); it('Reducing maxDepth to 1 level', () => { expect(serialize(data, 1)).toEqual({ t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: '1', }, { t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: '[Max Depth Reached]', }, { t: SerializedValueType.primitive, v: '[Max Depth Reached]', }, ], ], }, ], ], }); }); }); describe('limiting number of items', () => { const data = ['1', '2', '3', '4', '5']; expect(serialize(data, 1, 1)).toEqual({ t: SerializedValueType.array, v: [ { t: SerializedValueType.primitive, v: '1', }, ], e: 4, }); expect(serialize(data, 1, 2)).toEqual({ t: SerializedValueType.array, v: [ { t: SerializedValueType.primitive, v: '1', }, { t: SerializedValueType.primitive, v: '2', }, ], e: 3, }); const set = new Set(data); expect(serialize(set, 1, 1)).toEqual({ t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: '1', }, ], e: 4, }); expect(serialize(set, 1, 2)).toEqual({ t: SerializedValueType.set, v: [ { t: SerializedValueType.primitive, v: '1', }, { t: SerializedValueType.primitive, v: '2', }, ], e: 3, }); const map = new Map(); map.set(1, '1'); map.set(2, '2'); map.set(3, '3'); map.set(4, '4'); map.set(5, '5'); expect(serialize(map, 1, 1)).toEqual({ t: SerializedValueType.map, v: [ [ { t: SerializedValueType.primitive, v: 1, }, { t: SerializedValueType.primitive, v: '1', }, ], ], e: 4, }); expect(serialize(map, 1, 2)).toEqual({ t: SerializedValueType.map, v: [ [ { t: SerializedValueType.primitive, v: 1, }, { t: SerializedValueType.primitive, v: '1', }, ], [ { t: SerializedValueType.primitive, v: 2, }, { t: SerializedValueType.primitive, v: '2', }, ], ], e: 3, }); const obj = { a: [1, 2, 3], b: 'test', }; expect(serialize(obj, 2, 1)).toEqual({ t: SerializedValueType.object, v: [ [ { t: SerializedValueType.primitive, v: 'a', }, { t: SerializedValueType.array, v: [ { t: SerializedValueType.primitive, v: 1, }, ], e: 2, }, ], ], e: 1, }); }); ================================================ FILE: packages-ext/recoil-devtools/src/utils/debounce.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ // $FlowFixMe[unclear-type] We want the flexibility of any here type DebouncedFunc = (...args: Array) => void; export default function debounce( func: DebouncedFunc, wait: number, immediate?: boolean, ): DebouncedFunc { let timeout; return function (...args: Array): void { const context = this; const later = function () { timeout = null; if (!Boolean(immediate)) func.apply(context, args); }; const callNow = Boolean(immediate) && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } ================================================ FILE: packages-ext/recoil-devtools/src/utils/getStyle.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; type CssMap = { [string]: string | number, }; function getEntries(obj: {[string]: T}): Array<[string, T]> { const keys: string[] = Object.keys(obj); return keys.map(key => [key, obj[key]]); } export const getStyle = ( source: {[key: string]: CssMap}, entries: {[string]: boolean}, ): {...} | {[string]: number | string} => { const classNameMap = getEntries(entries); return classNameMap.reduce((acc, [key, val]) => { let nextAcc = {...acc}; if (Boolean(val)) { nextAcc = {...nextAcc, ...source[key]}; } return nextAcc; }, {}); }; ================================================ FILE: packages-ext/recoil-devtools/src/utils/sankey/CV2_D3.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict-local * @format * @oncall obviz */ 'use strict'; import type {Selection as D3Selection} from 'd3-selection'; import objectEntries from '../ObjectEntries'; const d3 = { ...require('d3-selection'), ...require('d3-transition'), }; // Styles objects use CSS style names for the keys (not using camel case). // The values are either static values or a function that takes a data element // and returns the new value. Values may be null to remove a style. export type Styles = $ReadOnly<{ [string]: null | string | number | (T => string | number | null), }>; // Attributes objects use attribute names for keys. The values are either // static values or a function which takes a data element and returns the value. // Values may be null to remove the attribute. export type Attributes = $ReadOnly<{ [string]: null | string | number | (T => string | number | null), }>; // Events objects are similar, only use DOM event typenames for the keys // such as click or mouseenter. export type Events = $ReadOnly<{ [string]: null | (T => void), }>; type Options = { animationDurationMS: number, }; const OPTION_DEFAULTS: Options = { animationDurationMS: 2000, }; class Selection { // $FlowFixMe[value-as-type] all: D3Selection; // $FlowFixMe[value-as-type] new: D3Selection; // $FlowFixMe[value-as-type] old: D3Selection; // $FlowFixMe[value-as-type] update: D3Selection; options: Options; constructor( props: { // $FlowFixMe[value-as-type] all: D3Selection, // $FlowFixMe[value-as-type] new: D3Selection, // $FlowFixMe[value-as-type] old: D3Selection, // $FlowFixMe[value-as-type] update: D3Selection, }, options: Options, ) { this.all = props.all; this.new = props.new; this.old = props.old; this.update = props.update; this.options = options; } // Select nested elements based on the selector and bind data to them bind( selector: string, data: $ReadOnlyArray, key: U => string | number, ): Selection { const [tag, className] = selector.split('.'); const updateSelection = this.all.selectAll(selector).data(data, key); updateSelection .interrupt('binding') .transition('binding') .duration(this.options.animationDurationMS / 2) .style('opacity', 1); const newSelector = updateSelection .enter() .append(tag) .classed(className, true); newSelector.style('opacity', 0); newSelector .transition('binding') .duration(this.options.animationDurationMS) .style('opacity', 1); const oldSelector = updateSelection.exit(); oldSelector .transition('binding') .duration(this.options.animationDurationMS / 2) .style('opacity', 0) .remove(); const allSelector = updateSelection.merge(newSelector); return new Selection( { all: allSelector, new: newSelector, old: oldSelector, update: updateSelection, }, this.options, ); } // Select a nested element for each element in the current selection. // Each element inherits the data bound to it from the parent selection. select(selector: string): Selection { const [tag, className] = selector.split('.'); // Add child elements to newly created elements in the current selection const newSelection = this.new.append(tag); // If the current selection contains existing elements that do not have // the child elements, then add them to existing elements. if (newSelection.empty()) { this.all.each(function () { const parent = d3.select(this); const child = parent .selectAll(selector) .data(parent.data()) .enter() .append(tag); if (className != null) { child.classed(className, true); } }); } if (className != null) { newSelection.classed(className, true); } return new Selection( { all: this.all.select(selector), new: newSelection, old: this.old.select(selector), update: this.update.select(selector), }, this.options, ); } // Update attributes for the selection. Animate them to their new values. attr(attrs?: Attributes): Selection { if (attrs != null) { function attr(selection, attrs) { for (const [attr, value] of objectEntries(attrs)) { selection.attr(attr, value ?? null); } } attr(this.new, attrs); attr( this.update .transition('attrs') .duration(this.options.animationDurationMS), attrs, ); } return this; } // Apply styles to the selection style(styles?: Styles): Selection { if (styles != null) { for (const [style, value] of objectEntries(styles)) { this.all.style(style, value ?? null); } } return this; } // Add event listeners to the selection on(events?: Events): Selection { if (events != null) { for (const [event, handler] of objectEntries(events)) { this.all.on(event, handler); } } return this; } // Set the inner text of the elements in the selection. text(str: string | (T => string)): Selection { this.all.text(str); return this; } // Set the inner HTML for the elements in the selection. html(str: string | (T => string)): Selection { this.all.html(str); return this; } } export function select(el: Element, options: $Shape): Selection { const selection = d3.select(el); return new Selection( { all: selection, new: d3.select(), old: d3.select(), update: selection, }, {...OPTION_DEFAULTS, ...options}, ); } ================================================ FILE: packages-ext/recoil-devtools/src/utils/sankey/CV2_memoize.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import isImmutable from './isImmutable'; import Immutable from 'immutable'; const KEY = Symbol('CV2_cacheKeyFromObject.KEY'); const TIME_WARNING_THRESHOLD_MS = 15; /** * Convert the given object into something that hashable that can be used as an * Immutable Map key. The current implementation recursively converts arrays * and plain objects to Lists and Maps, but it stops when hitting an Immutable * structure or a class instance. TODO: T21531272 it should go deeper than that. */ function cacheKeyFromObject(object: mixed): mixed { if (typeof object === 'object' && object !== null && !isImmutable(object)) { const t0 = window.performance ? performance.now() : 0; // $FlowFixMe let answer: Immutable.Map = Immutable.Map(); // $FlowIssue[sketchy-null-mixed] #9606986 Symbols are not supported // $FlowIssue[incompatible-type] #9606986 Symbols are not supported if (object[KEY]) { answer = answer.set('key', object[KEY]); } else { Object.entries(object).forEach(([key, value]) => { answer = typeof value === 'object' && value !== null && // $FlowIssue[incompatible-type] #9606986 Symbols are not supported value[KEY] != null ? answer.set(key, value[KEY]) : answer.set(key, Immutable.fromJS(value)); }); } const t1 = window.performance ? performance.now() : 0; if (__DEV__) { if (t1 - t0 > TIME_WARNING_THRESHOLD_MS) { // eslint-disable-next-line fb-www/no-console console.error('Spent', t1 - t0, 'milliseconds computing a cache key.'); } } return answer; } else { return object; } } export default function memoize(fn: Arg => Result): Arg => Result { let map; return (arg: Arg) => { if (!map) { map = Immutable.Map(); } const key = cacheKeyFromObject(arg); if (map.has(key)) { // $FlowFixMe[incompatible-return] return map.get(key); } else { const result = fn(arg); map = map.set(key, result); return result; } }; } ================================================ FILE: packages-ext/recoil-devtools/src/utils/sankey/Sankey.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Events, Styles} from './CV2_D3'; import type {Graph, Key, Link, Node} from './SankeyGraph'; const {select} = require('./CV2_D3'); const {generateGraph} = require('./SankeyGraph'); const d3Interpolate = require('d3-interpolate'); const d3Scale = require('d3-scale'); const React = require('react'); const {useLayoutEffect, useMemo, useRef} = require('react'); export type LinkData = $ReadOnly<{ data: $ReadOnlyArray, getLinkValue: L => number, getLinkSourceKey: L => ?Key, getLinkTargetKey: L => ?Key, }>; // Optional to provide node data if you would like to set node values other // that that derived from incoming and outgoing links or use names distinct from keys, etc. export type NodeData = $ReadOnly<{ data: $ReadOnlyArray, getNodeKey: N => Key, getNodeName: N => string, getNodeValue?: N => number, // If not provided, node value is based on max of incoming or outgoing links }>; type Layout = $ReadOnly<{ graph: Graph, depthDomain: [number, number], positionDomain: [number, number], }>; export type LayoutFunction = ({ graph: Graph, positionRange: [number, number], depthRange: [number, number], }) => Layout; type Props = $ReadOnly<{ height: number, width: number, margin?: number, orientation: 'horizontal' | 'vertical', layout: LayoutFunction, links: LinkData, // Input link data nodes?: NodeData, // If not provided, nodes are derived from link data getNodeTooltip?: (Node) => string, nodeStyles?: Styles>, nodeClass?: string | ((Node) => string), nodeLabelStyles?: Styles>, nodeLabelClass?: string | ((Node) => string), nodeEvents?: Events>, // Height of a node in pixels (x-axis in horizontal, y-axis in vertical orientation) nodeThickness?: number, // Align node labels nodeLabelAlignment?: 'inward' | 'right', // Curvature for curved link paths (range 0-1, defaults to 0.5) getLinkTooltip?: (Link) => string, linkColor: string | ((Link) => string), linkStyles?: Styles>, linkClass?: string | ((Link) => string), linkEvents?: Events>, linkCurvature?: number, // Threshold of link width to thickness when to switch between link path rendering approaches // using a spline with width for thin lines vs a filled in path for thick lines linkThickPathThreshold?: number, valueFormatter?: number => string, // Formatter for values in default tooltips animationDurationMS?: number, // Animation duration in ms // Optional SVG defs to use for filters and other styling defs?: React.Node, }>; // Helper to convert something that is either a value or a function to get the // value to always be a function so we can simply call it to get the value. // $FlowFixMe[unclear-type] const functor: (any) => T = (x: T) => // $FlowFixMe[escaped-generic] // $FlowFixMe[incompatible-type] typeof x === 'function' ? x : () => x; /** * @explorer-desc * Sankey Visualization inspired from https://drarmstr.github.io/chartcollection/examples/#sankey/ */ function Sankey({ height, width, margin = 10, orientation, layout: layoutFunction, links: linkData, nodes: nodeData, getNodeTooltip, nodeStyles, nodeClass, nodeLabelStyles, nodeLabelClass, nodeEvents, nodeThickness = 20, nodeLabelAlignment = 'inward', getLinkTooltip, linkColor, linkStyles, linkClass, linkEvents, linkCurvature = 0.5, linkThickPathThreshold = 0.75, valueFormatter = String, animationDurationMS = 2000, defs, }: Props): React.MixedElement { const ref = useRef(null); // Chart Size const positionRange = useMemo( () => [0, (orientation === 'horizontal' ? height : width) - margin * 2], [orientation, height, width, margin], ); const depthRange = useMemo( () => [ 0, (orientation === 'horizontal' ? width : height) - nodeThickness - margin * 2, ], [orientation, height, width, margin, nodeThickness], ); // Generate Graph const graph = useMemo( () => generateGraph({nodeData, linkData}), [nodeData, linkData], ); // Layout Graph const layout = useMemo( () => layoutFunction({ graph, positionRange, depthRange, }), [depthRange, graph, layoutFunction, positionRange], ); // Render Sankey via D3 useLayoutEffect(() => { // Setup Scales const depth = d3Scale .scaleLinear() .domain(layout.depthDomain) .rangeRound(depthRange); const breadth = d3Scale .scaleLinear() .domain(layout.positionDomain) .range(positionRange); // Select host if (ref.current == null) { return; } const svg = select(ref.current, {animationDurationMS}); // Bind Data const linksSelection = svg.select('g.links').bind( 'g.link', layout.graph.links.filter(l => l.visible), l => l.key, ); const nodesSelection = svg.select('g.nodes').bind( 'svg.node', layout.graph.nodes.filter(n => n.visible), n => n.key, ); // Render Nodes const nodeDepth = n => depth(n.depth); const nodePosition = n => breadth(n.position); const nodeWidth = n => breadth(n.value); nodesSelection.attr({ x: orientation === 'horizontal' ? nodeDepth : nodePosition, y: orientation === 'horizontal' ? nodePosition : nodeDepth, width: orientation === 'horizontal' ? nodeThickness : nodeWidth, height: orientation === 'horizontal' ? nodeWidth : nodeThickness, }); // Node Rects nodesSelection .select('rect') .attr({width: '100%', height: '100%', class: nodeClass ?? null}) .style(nodeStyles) .on(nodeEvents); // Node Tooltips nodesSelection .select('title') .text( getNodeTooltip ? n => getNodeTooltip(n) : n => `${n.name}\n${valueFormatter(n.value)}`, ); // Render Labels const labelsSelection = nodesSelection.select('text').text(n => n.name); const labelAlignRight = n => nodeLabelAlignment === 'right' || n.depth <= layout.depthDomain[1] / 2; if (orientation === 'horizontal') { nodesSelection.style({overflow: 'visible'}); labelsSelection.attr({ y: n => nodeWidth(n) / 2, x: n => (labelAlignRight(n) ? nodeThickness : 0), dx: n => (labelAlignRight(n) ? '0.25em' : '-0.25em'), 'text-anchor': n => (labelAlignRight(n) ? 'start' : 'end'), 'dominant-baseline': 'middle', }); } else { nodesSelection.style({overflow: 'hidden'}); labelsSelection.attr({ x: n => breadth(n.value) / 2, dy: nodeThickness / 2, 'text-anchor': 'middle', 'dominant-baseline': 'middle', }); } labelsSelection .attr({class: nodeLabelClass ?? ''}) .style({'pointer-events': 'none'}) .style(nodeLabelStyles); // Link Tooltips linksSelection .select('title') .html(l => getLinkTooltip ? getLinkTooltip(l) : `${l.source?.name ?? '[UNKNOWN]'}  →  ${ l.target?.name ?? '[UNKNOWN]' }\n${valueFormatter(l.value)}`, ); const isThickCurve = (l: Link) => { const depthDelta = depth(l.targetDepth) - depth(l.sourceDepth); return ( depthDelta > nodeThickness && breadth(l.value) > depthDelta * linkThickPathThreshold ); }; // Render Links linksSelection .select('path') .attr({ d: l => { const linkBreadth = breadth(l.value); const sourceDepth = depth(l.sourceDepth) + nodeThickness; const targetDepth = depth(l.targetDepth); // Control point depths to define link curvature // Allow back-edges to swoop around const isBackEdgeCurve = targetDepth <= sourceDepth; const depthInterpolator = d3Interpolate.interpolateRound( sourceDepth, targetDepth, ); const backEdgeCurvature = isBackEdgeCurve ? Math.pow(l.sourceDepth - l.targetDepth, 0.5) + 1 : 0; const sourceControlPointDepth = !isBackEdgeCurve ? depthInterpolator(linkCurvature) : sourceDepth + (depth(backEdgeCurvature) - depth(0)); const targetControlPointDepth = !isBackEdgeCurve ? depthInterpolator(1 - linkCurvature) : targetDepth - (depth(backEdgeCurvature) - depth(0)); // Browsers can introduce rendering artifacts if curves are too wide, // to avoid this, if a link is too thick then outline and fill the path instead. if (!isThickCurve(l)) { // Browsers may not apply the fade gradient mask properly for a straight // path. In this case, jitter the faded end of the line slightly. // (As of Chrome 9/3/20) const isStraight = breadth(l.sourcePosition).toFixed(3) === breadth(l.targetPosition).toFixed(3); const sourcePosition = breadth(l.sourcePosition + l.value / 2) + (l.fadeSource && isStraight ? 1 : 0); const targetPosition = breadth(l.targetPosition + l.value / 2) + (l.fadeTarget && isStraight ? 1 : 0); return orientation === 'horizontal' ? `M${sourceDepth},${sourcePosition}` + // Start of curve `C${sourceControlPointDepth},${sourcePosition}` + // First control point ` ${targetControlPointDepth},${targetPosition}` + // Second conrol point ` ${targetDepth},${targetPosition}` // End of curve : `M${sourcePosition},${sourceDepth}` + // Start of curve `C${sourcePosition},${sourceControlPointDepth}` + // First control point ` ${targetPosition},${targetControlPointDepth}` + // Second conrol point ` ${targetPosition},${targetDepth}`; // End of curve } else { const sourcePosition = breadth(l.sourcePosition); const targetPosition = breadth(l.targetPosition); return orientation === 'horizontal' ? `M${sourceDepth},${sourcePosition}` + // Start of curve `C${sourceControlPointDepth},${sourcePosition}` + // First control point ` ${targetControlPointDepth},${targetPosition}` + // Second conrol point ` ${targetDepth},${targetPosition}` + // End of curve `v${linkBreadth}` + `C${targetControlPointDepth},${ targetPosition + linkBreadth }` + // Second control point ` ${sourceControlPointDepth},${ sourcePosition + linkBreadth }` + // First conrol point ` ${sourceDepth},${sourcePosition + linkBreadth}` + `Z` : `M${sourcePosition},${sourceDepth}` + // Start of curve `C${sourcePosition},${sourceControlPointDepth}` + // First control point ` ${targetPosition},${targetControlPointDepth}` + // Second conrol point ` ${targetPosition},${targetDepth}` + // End of curve `h${linkBreadth}` + `C${ targetPosition + linkBreadth },${targetControlPointDepth}` + // Second control point ` ${ sourcePosition + linkBreadth },${sourceControlPointDepth}` + // First conrol point ` ${sourcePosition + linkBreadth},${sourceDepth}` + `Z`; } }, 'stroke-width': l => !isThickCurve(l) ? Math.max(1, breadth(l.value)) : 0, mask: l => l.fadeSource ? 'url(#mask_fade_left)' : l.fadeTarget ? 'url(#mask_fade_right)' : null, class: linkClass ?? '', }) .style({ ...linkStyles, fill: l => (isThickCurve(l) ? functor(linkColor)(l) : 'none'), stroke: l => (isThickCurve(l) ? 'none' : functor(linkColor)(l)), 'fill-opacity': l => (isThickCurve(l) ? 1 : 0), 'stroke-opacity': l => (isThickCurve(l) ? 0 : 1), }) .on(linkEvents); }, [ animationDurationMS, layout, depthRange, getLinkTooltip, getNodeTooltip, linkClass, linkColor, linkCurvature, linkEvents, linkStyles, linkThickPathThreshold, nodeClass, nodeEvents, nodeLabelAlignment, nodeLabelClass, nodeLabelStyles, nodeStyles, nodeThickness, orientation, positionRange, valueFormatter, ]); return ( {defs} ); } module.exports = Sankey; ================================================ FILE: packages-ext/recoil-devtools/src/utils/sankey/SankeyGraph.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict-local * @format * @oncall obviz */ 'use strict'; import type {LinkData, NodeData} from './Sankey'; const objectValues = require('../ObjectValues').default; const d3Array = require('d3-array'); export type Key = string | number; export type Link = { data: L | Key, key: Key, value: number, source?: Node, target?: Node, visible: boolean, sourceDepth: number, sourcePosition: number, targetDepth: number, targetPosition: number, backedge: boolean, fadeSource: boolean, fadeTarget: boolean, }; export type Node = { data: N | Key, +key: Key, +name: string, value: number, depth: number, visible: boolean, position: number, +sourceLinks: Array>, +targetLinks: Array>, linkWeight: number, }; export type Graph = $ReadOnly<{ links: $ReadOnlyArray>, nodes: $ReadOnlyArray>, }>; // Utility functions const sortDesc = (array: Array, accessor: T => number): Array => array.sort((a, b) => accessor(b) - accessor(a)); // Generate the Node and Link objects for layout. These may be mutated. function generateGraph({ nodeData, linkData, }: { nodeData?: NodeData, linkData: LinkData, }): Graph { // Prepare the Nodesx const nodesByKey: {[Key]: Node} = {}; if (nodeData != null) { for (const n of nodeData.data) { const key: Key = nodeData.getNodeKey(n); nodesByKey[key] = { data: n, key, name: nodeData.getNodeName(n), value: nodeData.getNodeValue?.(n) ?? 0, visible: true, depth: 0, position: 0, sourceLinks: [], targetLinks: [], linkWeight: 0, }; } } else { // Generate nodes based on the proivded links const keys: Set = new Set(); for (const l of linkData.data) { const linkSourceKey = linkData.getLinkSourceKey(l); if (linkSourceKey != null) { keys.add(linkSourceKey); } const linkTargetKey = linkData.getLinkTargetKey(l); if (linkTargetKey != null) { keys.add(linkTargetKey); } } for (const key of keys) { if (key != null) { nodesByKey[key] = { data: key, key, name: String(key), value: 0, visible: true, depth: 0, position: 0, sourceLinks: [], targetLinks: [], linkWeight: 0, }; } } } // Prepare the Links const links: Array> = linkData.data.map(l => { const sourceKey = linkData.getLinkSourceKey(l); const targetKey = linkData.getLinkTargetKey(l); const source = sourceKey != null ? nodesByKey[sourceKey] : undefined; const target = targetKey != null ? nodesByKey[targetKey] : undefined; const link = { data: l, key: `${sourceKey ?? ''}/${targetKey ?? ''}`, value: linkData.getLinkValue(l), source, target, visible: true, sourceDepth: 0, sourcePosition: 0, targetDepth: 0, targetPosition: 0, backedge: false, fadeSource: source == null, fadeTarget: target == null, }; link.source?.targetLinks.push(link); link.target?.sourceLinks.push(link); return link; }); // Only include connected nodes const nodes = objectValues(nodesByKey).filter( node => node.sourceLinks.length || node.targetLinks.length, ); // Compute the node values for (const node of nodes) { const sourceWeight: number = d3Array.sum(node.sourceLinks, l => l.value); const targetWeight: number = d3Array.sum(node.targetLinks, l => l.value); node.value = Math.max(node.value, sourceWeight, targetWeight); // Sort links for deterministic back-edge detection sortDesc(node.sourceLinks, l => l.value); sortDesc(node.targetLinks, l => l.value); } // Detect Back Edges / Cycles sortDesc(nodes, node => node.value); // sort for deterministic back-edge detection const visited: {[Key]: boolean} = {}; for (const node of nodes) { if (visited[node.key]) { continue; } const stack: Array = []; function detectBackedge(node) { visited[node.key] = true; stack.push(node); for (const link of node.targetLinks) { link.backedge = stack.indexOf(link.target) !== -1; if (link.target != null && !visited[link.target.key]) { detectBackedge(link.target); } } stack.pop(); } detectBackedge(node); } // Return the Graph return {links, nodes}; } function updateVisibility(graph: Graph, nodesSet: Set>) { // Update node visibility for (const node of graph.nodes) { node.visible = false; } for (const node of nodesSet) { node.visible = true; } // Update link visibility for (const link of graph.links) { // Set which links point to missing nodes link.fadeSource = !link.source?.visible; link.fadeTarget = !link.target?.visible; link.visible = !link.fadeSource || !link.fadeTarget; } } module.exports = { generateGraph, updateVisibility, }; ================================================ FILE: packages-ext/recoil-devtools/src/utils/sankey/SankeyGraphLayout.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow strict-local * @format * @oncall obviz */ 'use strict'; import type {LayoutFunction} from './Sankey'; import type {Graph, Key, Link, Node} from './SankeyGraph'; const compactArray = require('./compactArray').default; const {updateVisibility} = require('./SankeyGraph'); const d3Array = require('d3-array'); const d3Collection = require('d3-collection'); type PositionLayoutOptions = { // Either a number of pixels to attempt to use between each node. If there // are too many nodes, then fewer pixels may be used. It can also be a string // with a percentage value of the vertical space to use for padding, divided // among all of the nodes. (defaults to 20%) nodePadding: number | string, // Number of iterations to run the layout algorithm iterations: number, // Factor to adjust the strength of each iteration (smaller will quiesce faster) alpha: number, // Factor 0-1 to weight aligning nodes with their targets vs their sources. // A Larger number makes big links straigter while a smaller number avoids crossed links targetLinksWeight: number, // How to sort nodes // outward/inward will place larger nodes on either end or the middle in // an attempt to avoid crossing links sortNodes: 'ascending' | 'descending' | 'outward' | 'inward', ... }; type FlowLayoutOptions = { ...PositionLayoutOptions, // Limit the number of nodes in the graph. Links to nodes that are omitted will fade out. // Currently this will select a contiguous sub-graph from the root nodes based on // following the most heavily weighted links. nodeLimit?: number, // Set node depth based on depth from entry or fully justify leaf nodes at the deepest side nodeAlignment: 'entry' | 'both', ... }; const FLOW_LAYOUT_DEFAULT: FlowLayoutOptions = { nodePadding: '20%', iterations: 32, alpha: 0.99, targetLinksWeight: 0.2, sortNodes: 'descending', nodeAlignment: 'entry', }; type ButterflyLayoutOptions = { ...PositionLayoutOptions, // The depth of either callers or callees from the focal node to include in the graph depthOfField: number, // Limit the number of nodes in the graph. Links to nodes that are omitted will fade out. // Currently this will select a contiguous sub-graph from the root nodes based on // following the most heavily weighted links. nodeLimit?: number, ... }; const BUTTERFLY_LAYOUT_DEFAULT: ButterflyLayoutOptions = { depthOfField: 1, nodePadding: '20%', iterations: 32, alpha: 0.99, targetLinksWeight: 0.2, sortNodes: 'descending', }; // Utility functions const sortAsc = (array: Array, accessor: T => number): Array => array.sort((a, b) => accessor(a) - accessor(b)); const sortDesc = (array: Array, accessor: T => number): Array => array.sort((a, b) => accessor(b) - accessor(a)); // Limit the graph to just the top n nodes. // We could simply sort the nodes and slice the biggest ones. However, that // can lead to graphs with disconnected fragments. Instead, traverse the // graph from the roots and select nodes connected via the largest links. function limitNodes( graph: Graph, nodeLimit: ?number, rootNodes: Array> = graph.nodes.filter( node => node.sourceLinks.length === 0, ), ): ?Graph { if (nodeLimit == null || nodeLimit >= graph.nodes.length) { return graph; } let nodesToAdd = nodeLimit; const nodesSet: Set> = new Set(); let nextLinks = rootNodes.flatMap(node => node.targetLinks.concat(node.sourceLinks), ); // $FlowFixMe[incompatible-type-arg] Uncovered while typing recoil-devtools const consideredLinks: Set> = new Set(nextLinks); function addNode(node) { if (!nodesToAdd) { return; } nodesSet.add(node); const newLinks = node.targetLinks .concat(node.sourceLinks) .filter(link => !consideredLinks.has(link)); newLinks.forEach(link => consideredLinks.add(link)); nextLinks = nextLinks.concat(newLinks); nodesToAdd--; } while (nodesToAdd && nextLinks.length) { sortDesc(nextLinks, link => link.value); const link = nextLinks.shift(); for (const node of [link.target, link.source]) { if (node && !nodesSet.has(node)) { addNode(node); } } } updateVisibility(graph, nodesSet); } // Traditional Sankey flow layout. // Assign depths to each node based on how they flow function flowLayoutNodeDepths( graph: Graph, layoutOptions: FlowLayoutOptions, ): [number, number] { const visibleNodes = graph.nodes.filter(node => node.visible); // Compute the depth of each node let remainingNodes = visibleNodes; let depth = 0; while (remainingNodes.length) { const nextNodes = []; for (const node of remainingNodes) { node.depth = depth; for (const targetLink of node.targetLinks.filter(l => !l.backedge)) { if (!targetLink.fadeTarget && targetLink.target != null) { nextNodes.push(targetLink.target); } } } remainingNodes = nextNodes; depth++; } const maxDepth = depth - 1; // Right align nodes with no targets if requested if (layoutOptions.nodeAlignment === 'both') { for (const node of visibleNodes) { if (!node.targetLinks.length) { node.depth = maxDepth; } } } return [0, maxDepth]; } // Butterfly "caller / callee" style layout. // Place focal node in the center and fan out callers on the left and callees on the right function butterflyLayoutNodeDepths( graph: Graph, focalNode: Node, layoutOptions: ButterflyLayoutOptions, ): [number, number] { const nodesSet = new Set([focalNode]); const depthDomain = [0, 0]; function addNeighbors( node: Node, direction: 'target' | 'source', depth: number, ) { const neighborLinks = direction === 'target' ? node.targetLinks : node.sourceLinks; const neighbors = compactArray( neighborLinks.map(link => direction === 'target' ? link.target : link.source, ), ); const newNodes = []; for (const neighbor of neighbors) { if (!nodesSet.has(neighbor)) { newNodes.push(neighbor); neighbor.depth = depth; direction === 'target' ? (depthDomain[1] = Math.max(depthDomain[1], depth)) : (depthDomain[0] = Math.min(depthDomain[0], depth)); nodesSet.add(neighbor); } } if (Math.abs(depth) < layoutOptions.depthOfField) { for (const neighbor of newNodes) { addNeighbors( neighbor, direction, depth + (direction === 'target' ? 1 : -1), ); } } } focalNode.depth = 0; if (layoutOptions.depthOfField >= 1) { addNeighbors(focalNode, 'target', 1); addNeighbors(focalNode, 'source', -1); } updateVisibility(graph, nodesSet); return [depthDomain[0] - 0.5, depthDomain[1] + 0.5]; } // After the nodes have been assigned depths use an iterative algorithm to adjust // their positions so they flow by trying to align connected nodes near each other. function layoutPositions( graph: Graph, layoutOptions: PositionLayoutOptions, maxBreadth: number, ): [number, number] { const visibleNodes = graph.nodes.filter(node => node.visible); // Prepare set of depths const depths: Array< Array> & {paddingPercent: number, padding: number}, > = d3Collection .nest() .key(n => n.depth) .entries(visibleNodes) .map(g => g.values); sortAsc(depths, depth => depth[0]?.depth); // d3.nest().sortKeys() didn't work? // Calculate node padding and positions // Start by determining the percentage of each column to use for padding const {nodePadding} = layoutOptions; if (typeof nodePadding === 'number') { for (const depth of depths) { depth.paddingPercent = Math.max( (nodePadding * (depth.length - 1)) / maxBreadth, 0.8, ); } } else if (nodePadding.charAt(nodePadding.length - 1) === '%') { for (const depth of depths) { depth.paddingPercent = depth.length === 1 ? 0 : +nodePadding.slice(0, -1) / 100; depth.paddingPercent = depth.paddingPercent === 1 ? 0.999 : depth.paddingPercent; } } else { throw new Error( `Unsupported nodePadding parameter: ${String(nodePadding)}`, ); } // Calculate maximum breadth, including padding const maxPosition: number = d3Array.max( depths.map( depth => d3Array.sum(depth, node => node.value) / (1 - depth.paddingPercent), ), ); // Calculate node padding for each depth for (const depth of depths) { depth.padding = depth.length === 1 ? 0 : (maxPosition * depth.paddingPercent) / (depth.length - 1); } // Detect collisions and move nodes to avoid overlap function collisionDetection() { for (const depth of depths) { sortAsc(depth, n => n.position); // Push overlapping nodes down let position = 0; for (const node of depth) { const delta = position - node.position; if (delta > 0) { node.position += delta; } position = node.position + node.value + depth.padding; } // If they extend past the edge, then push some nodes back const lastNode = depth[depth.length - 1]; if (lastNode.position + lastNode.value > maxPosition) { position = maxPosition; for (const node of [...depth].reverse()) { const delta = node.position + node.value - position; if (delta > 0) { node.position -= delta; } else { break; } position = node.position - depth.padding; } } } } // Assign node link weights for (const node of visibleNodes) { const sourceWeight: number = d3Array.sum(node.sourceLinks, l => l.value); const targetWeight: number = d3Array.sum(node.targetLinks, l => l.value); node.linkWeight = sourceWeight + targetWeight * layoutOptions.targetLinksWeight; } function layoutLinks() { for (const depth of depths) { const padding = depth.length > 1 ? depth.padding : depth.length === 1 ? maxPosition - depth[0].value : 0; for (const node of depth) { sortAsc(node.sourceLinks, link => link.source?.position ?? Infinity); let trailingPosition = node.position - padding / 2; let trailingPadding = padding / (node.sourceLinks.length - 1); let position = node.position; for (const sourceLink of node.sourceLinks) { sourceLink.targetDepth = node.depth; sourceLink.targetPosition = position; position += sourceLink.value; // Trailing link to missing node if (sourceLink.fadeSource) { sourceLink.sourceDepth = node.depth - 1; sourceLink.sourcePosition = trailingPosition; } trailingPosition += sourceLink.value + trailingPadding; } sortAsc(node.targetLinks, link => link.target?.position ?? Infinity); position = node.position; trailingPosition = node.position - padding / 2; trailingPadding = padding / (node.targetLinks.length - 1); for (const targetLink of node.targetLinks) { targetLink.sourceDepth = node.depth; targetLink.sourcePosition = position; position += targetLink.value; // Trailing link to missing node if (targetLink.fadeTarget) { targetLink.targetDepth = node.depth + 1; targetLink.targetPosition = trailingPosition; } trailingPosition += targetLink.value + trailingPadding; } } } } // Give nodes and links an initial position for (const [i, depth] of depths.entries()) { // Sort the nodes layoutOptions.sortNodes === 'ascending' || layoutOptions.sortNodes === 'inward' ? sortAsc(depth, node => node.value) : sortDesc(depth, node => node.value); if ( layoutOptions.sortNodes === 'outward' || layoutOptions.sortNodes === 'inward' ) { // Arrange nodes for the first depth with large nodes on each end in an // attempt to avoid links crossing over each other. const newDepthNodes = [ ...depth.filter((_, i) => !(i % 2)), ...depth .reverse() .slice(depth.length % 2) .filter((_, i) => !(i % 2)), ]; for (const [i, node] of newDepthNodes.entries()) { depth[i] = node; } } if (!i) { let position = 0; for (const node of depths[0]) { node.position = position; position += node.value + depths[0].padding; } } else { // For each subsequent depth, align the nodes to the right of their sources // in an attempt for flatter links for (const node of depth) { let weightedPosition = 0; let sourceLinkValue = 0; let totalWeightedPosition = 0; let totalSourceLinkValue = 0; for (const sourceLink of node.sourceLinks) { const source = sourceLink.source; if (source == null) { continue; } totalWeightedPosition += source.position * sourceLink.value; totalSourceLinkValue += sourceLink.value; // Only provide initial layout for forward links, not back edges if (source.depth >= node.depth) { continue; } weightedPosition += source.position * sourceLink.value; sourceLinkValue += sourceLink.value; } if (sourceLinkValue) { node.position = weightedPosition / sourceLinkValue; } else if (totalSourceLinkValue) { // If all source links were back-edges, then just average them all node.position = totalWeightedPosition / totalSourceLinkValue; } else { // If there are no source links at all, then average the target links // This can't happen in a normal Sankey, since all nodes with no // sources are in the first depth, but it can happen with a butterfly. let targetLinkValue = 0; for (const targetLink of node.targetLinks) { const target = targetLink.target; if (target == null) { continue; } weightedPosition += target.position * targetLink.value; targetLinkValue += targetLink.value; } node.position = weightedPosition / (targetLinkValue || 1); } } } } collisionDetection(); layoutLinks(); // Now iterate on the layout to shift nodes closer to their neighbors // based on the values of their links let alpha = 1; for (let iteration = 0; iteration < layoutOptions.iterations; iteration++) { alpha *= layoutOptions.alpha; for (const depth of depths) { for (const node of depth) { let delta = 0; for (const sourceLink of node.sourceLinks) { // Not back-edges if (sourceLink.targetDepth > sourceLink.sourceDepth) { delta += (sourceLink.sourcePosition - sourceLink.targetPosition) * sourceLink.value; } } for (const targetLink of node.targetLinks) { // Not back-edges if (targetLink.targetDepth > targetLink.sourceDepth) { delta += (targetLink.targetPosition - targetLink.sourcePosition) * targetLink.value * // Weight alignment with target nodes less, to avoid cross-over layoutOptions.targetLinksWeight; } } delta /= node.linkWeight; node.position += delta * alpha; } } collisionDetection(); layoutLinks(); } return [0, maxPosition]; } const flowGraphLayout = ( layoutOptions: $Shape = FLOW_LAYOUT_DEFAULT, ): LayoutFunction => { const layoutOptionsConfig = { ...FLOW_LAYOUT_DEFAULT, ...layoutOptions, }; return ({graph, positionRange}) => { limitNodes(graph, layoutOptionsConfig.nodeLimit); const depthDomain = flowLayoutNodeDepths(graph, layoutOptionsConfig); const positionDomain = layoutPositions( graph, layoutOptionsConfig, positionRange[1], ); return {graph, positionDomain, depthDomain}; }; }; const butterflyGraphLayout = ( focalNodeKey: Key, layoutOptions: $Shape = BUTTERFLY_LAYOUT_DEFAULT, ): LayoutFunction => { const layoutOptionsConfig = { ...BUTTERFLY_LAYOUT_DEFAULT, ...layoutOptions, focalNodeKey, }; return ({graph, positionRange}) => { const focalNode = graph.nodes.find( n => n.key === layoutOptionsConfig.focalNodeKey, ); if (focalNode == null) { throw new Error( `Unable to find focal node: ${layoutOptionsConfig.focalNodeKey}`, ); } const depthDomain = butterflyLayoutNodeDepths( graph, focalNode, layoutOptionsConfig, ); limitNodes(graph, layoutOptionsConfig.nodeLimit, [focalNode]); const positionDomain = layoutPositions( graph, layoutOptionsConfig, positionRange[1], ); return {graph, positionDomain, depthDomain}; }; }; module.exports = { flowGraphLayout, butterflyGraphLayout, }; ================================================ FILE: packages-ext/recoil-devtools/src/utils/sankey/compactArray.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict * @format * @typechecks */ 'use strict'; /** * Returns a new Array containing all the element of the source array except * `null` and `undefined` ones. This brings the benefit of strong typing over * `Array.prototype.filter`. */ export default function compactArray(array: $ReadOnlyArray): Array { const result = []; for (let i = 0; i < array.length; ++i) { const elem = array[i]; if (elem != null) { result.push(elem); } } return result; } ================================================ FILE: packages-ext/recoil-devtools/src/utils/sankey/isImmutable.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @flow * @format * @oncall recoil */ 'use strict'; import immutable from 'immutable'; // The version of immutable in www is out of date and doesn't implement // isImmutable; this can be removed when it is upgraded to v4. For forwards- // compatibility, use the implementation from the library if it exists: const isImmutable: Object => boolean = (immutable: any).isImmutable || function isImmutable(o: Object): boolean { return !!( o['@@__IMMUTABLE_ITERABLE__@@'] || o['@@__IMMUTABLE_KEYED__@@'] || o['@@__IMMUTABLE_INDEXED__@@'] || o['@@__IMMUTABLE_ORDERED__@@'] || o['@@__IMMUTABLE_RECORD__@@'] ); }; export default isImmutable; ================================================ FILE: packages-ext/recoil-devtools/utils/build.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @format * @oncall recoil */ 'use strict'; const config = require('../webpack.config'); const webpack = require('webpack'); delete config.chromeExtensionBoilerplate; webpack(config, function (err) { if (err) throw err; }); ================================================ FILE: packages-ext/recoil-devtools/utils/env.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @format * @oncall recoil */ 'use strict'; module.exports = { NODE_ENV: process.env.NODE_ENV || 'development', PORT: process.env.PORT || 3000, }; ================================================ FILE: packages-ext/recoil-devtools/utils/webserver.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @format * @oncall recoil */ 'use strict'; const config = require('../webpack.config'); const env = require('./env'); const path = require('path'); const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); const options = config.chromeExtensionBoilerplate || {}; const excludeEntriesToHotReload = options.notHotReload || []; for (const entryName in config.entry) { if (excludeEntriesToHotReload.indexOf(entryName) === -1) { config.entry[entryName] = [ 'webpack-dev-server/client?http://localhost:' + env.PORT, 'webpack/hot/dev-server', ].concat(config.entry[entryName]); } } config.plugins = [new webpack.HotModuleReplacementPlugin()].concat( config.plugins || [], ); delete config.chromeExtensionBoilerplate; const compiler = webpack(config); const server = new WebpackDevServer(compiler, { hot: true, contentBase: path.join(__dirname, '../build'), headers: { 'Access-Control-Allow-Origin': '*', }, disableHostCheck: true, }); server.listen(env.PORT); ================================================ FILE: packages-ext/recoil-devtools/webpack.config.js ================================================ /** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * Recoil DevTools browser extension. * * @format * @oncall recoil */ 'use strict'; const env = require('./utils/env'); const {CleanWebpackPlugin} = require('clean-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const fileSystem = require('fs-extra'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); const webpack = require('webpack'); const WriteFilePlugin = require('write-file-webpack-plugin'); // load the secrets var alias = { 'react-dom': '@hot-loader/react-dom', }; var buildFolder = 'recoil_devtools_ext'; var secretsPath = path.join(__dirname, 'secrets.' + env.NODE_ENV + '.js'); var fileExtensions = [ 'jpg', 'jpeg', 'png', 'gif', 'eot', 'otf', 'svg', 'ttf', 'woff', 'woff2', ]; if (fileSystem.existsSync(secretsPath)) { alias['secrets'] = secretsPath; } var options = { mode: env.NODE_ENV || 'development', entry: { popup: path.join(__dirname, 'src', 'pages', 'Popup', 'PopupScript.js'), devtools: path.join( __dirname, 'src', 'pages', 'Devtools', 'DevtoolsScript.js', ), background: path.join( __dirname, 'src', 'pages', 'Background', 'Background.js', ), contentScript: path.join( __dirname, 'src', 'pages', 'Content', 'ContentScript.js', ), pageScript: path.join(__dirname, 'src', 'pages', 'Page', 'PageScript.js'), }, chromeExtensionBoilerplate: { notHotReload: ['contentScript'], }, output: { path: path.resolve(__dirname, buildFolder), filename: '[name].bundle.js', }, module: { rules: [ { test: /\.css$/, loader: 'style-loader!css-loader', }, { test: new RegExp('.(' + fileExtensions.join('|') + ')$'), loader: 'file-loader?name=[name].[ext]', exclude: /node_modules/, }, { test: /\.html$/, loader: 'html-loader', exclude: /node_modules/, }, { test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/, }, ], }, resolve: { alias, extensions: fileExtensions .map(extension => '.' + extension) .concat(['.jsx', '.js', '.css']), }, plugins: [ new webpack.ProgressPlugin(), new webpack.DefinePlugin({ __DEV__: env.NODE_ENV !== 'production', }), // clean the build folder new CleanWebpackPlugin({ verbose: true, cleanStaleWebpackAssets: false, }), // expose and write the allowed env vars on the compiled bundle new webpack.EnvironmentPlugin(['NODE_ENV']), new CopyWebpackPlugin([ { from: path.join(__dirname, 'src', 'assets', 'img', 'icon-34.png'), to: path.join(__dirname, buildFolder), }, ]), new CopyWebpackPlugin([ { from: path.join(__dirname, 'src', 'assets', 'img', 'icon-128.png'), to: path.join(__dirname, buildFolder), }, ]), new CopyWebpackPlugin( [ { from: 'src/manifest.json', to: path.join(__dirname, buildFolder), force: true, transform(content, _path) { // generates the manifest file using the package.json informations return Buffer.from( JSON.stringify({ description: process.env.npm_package_description, version: process.env.npm_package_version, ...JSON.parse(content.toString()), }), ); }, }, ], { logLevel: 'info', copyUnmodified: true, }, ), new HtmlWebpackPlugin({ template: path.join(__dirname, 'src', 'pages', 'Popup', 'index.html'), filename: 'popup.html', chunks: ['popup'], }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'src', 'pages', 'Popup', 'Devpanel.html'), filename: 'devpanel.html', chunks: ['popup'], }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'src', 'pages', 'Devtools', 'index.html'), filename: 'devtools.html', chunks: ['devtools'], }), new HtmlWebpackPlugin({ template: path.join( __dirname, 'src', 'pages', 'Background', 'index.html', ), filename: 'background.html', chunks: ['background'], }), new WriteFilePlugin(), ], }; if (env.NODE_ENV === 'development') { options.devtool = 'cheap-module-eval-source-map'; } module.exports = options; ================================================ FILE: packages-ext/todo-example/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: packages-ext/todo-example/README.md ================================================ # Getting Started with Create React App This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ## Available Scripts In the project directory, you can run: ### `yarn start` Runs the app in the development mode.\ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.\ You will also see any lint errors in the console. ### `yarn test` Launches the test runner in the interactive watch mode.\ See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. ### `yarn build` Builds the app for production to the `build` folder.\ It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes.\ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. ### `yarn eject` **Note: this is a one-way operation. Once you `eject`, you can’t go back!** If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). ### Code Splitting This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) ### Analyzing the Bundle Size This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) ### Making a Progressive Web App This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) ### Advanced Configuration This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) ### Deployment This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) ### `yarn build` fails to minify This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) ================================================ FILE: packages-ext/todo-example/package.json ================================================ { "name": "todo-example", "version": "0.1.0", "private": true, "dependencies": { "react": "^17.0.1", "react-dom": "^17.0.1", "react-scripts": "4.0.3", "recoil": "^0.1.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: packages-ext/todo-example/public/index.html ================================================ React App
================================================ FILE: packages-ext/todo-example/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: packages-ext/todo-example/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: packages-ext/todo-example/src/App.css ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ .todo-container { display: grid; place-content: center; height: 100vh; } ================================================ FILE: packages-ext/todo-example/src/App.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ import './App.css'; import {TodoList} from './components/Todo/TodoList'; import React from 'react'; import {RecoilRoot} from 'recoil'; function App() { return (
); } export default App; ================================================ FILE: packages-ext/todo-example/src/components/Todo/TodoItem.jsx ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ import type {TItem} from './Todo_types'; import {todoListState} from './Todo_state'; import React from 'react'; import {useRecoilState} from 'recoil'; export const TodoItem = ({item, index}: {item: TItem, index: number}) => { const [todoList, setTodoList] = useRecoilState(todoListState); const editItemText = ({target: {value}}) => { const newList = replaceItemAtIndex(todoList, index, { ...item, text: value, }); setTodoList(newList); }; const toggleItemCompletion = () => { const newList = replaceItemAtIndex(todoList, index, { ...item, isComplete: !item.isComplete, }); setTodoList(newList); }; const deleteItem = () => { const newList = removeItemAtIndex(todoList, index); setTodoList(newList); }; return (
); }; function replaceItemAtIndex(arr, index, newValue) { return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; } function removeItemAtIndex(arr, index) { return [...arr.slice(0, index), ...arr.slice(index + 1)]; } ================================================ FILE: packages-ext/todo-example/src/components/Todo/TodoItemCreator.jsx ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ import {todoListState} from './Todo_state'; import React, {useState} from 'react'; import {useSetRecoilState} from 'recoil'; export const TodoItemCreator = () => { const [inputValue, setInputValue] = useState(''); const setTodoList = useSetRecoilState(todoListState); const addItem = () => { setTodoList(oldTodoList => [ ...oldTodoList, { id: getId(), text: inputValue, isComplete: false, }, ]); setInputValue(''); }; const onChange = ({target: {value}}) => { setInputValue(value); }; return (
); }; // utility for creating unique Id let id = 0; function getId() { return id++; } ================================================ FILE: packages-ext/todo-example/src/components/Todo/TodoList.jsx ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ import {filteredTodoListState} from './Todo_state'; import {TodoItem} from './TodoItem'; import {TodoItemCreator} from './TodoItemCreator'; import {TodoListFilters} from './TodoListFilters'; import {TodoListStats} from './TodoListStats'; import React from 'react'; import {useRecoilValue} from 'recoil'; export const TodoList = () => { const todoList = useRecoilValue(filteredTodoListState); return ( <> {todoList.map((todoItem, index) => ( ))} ); }; ================================================ FILE: packages-ext/todo-example/src/components/Todo/TodoListFilters.jsx ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ import {todoListFilterState} from './Todo_state'; import React from 'react'; import {useRecoilState} from 'recoil'; export const TodoListFilters = () => { const [filter, setFilter] = useRecoilState(todoListFilterState); const updateFilter = ({target: {value}}) => { setFilter(value); }; return ( <> Filter: ); }; ================================================ FILE: packages-ext/todo-example/src/components/Todo/TodoListStats.jsx ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ import {todoListStatsState} from './Todo_state'; import React from 'react'; import {useRecoilValue} from 'recoil'; export const TodoListStats = () => { const {totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted} = useRecoilValue(todoListStatsState); const formattedPercentCompleted = Math.round(percentCompleted); return (
  • Total items: {totalNum}
  • Items completed: {totalCompletedNum}
  • Items not completed: {totalUncompletedNum}
  • Percent completed: {formattedPercentCompleted}
); }; ================================================ FILE: packages-ext/todo-example/src/components/Todo/Todo_state.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ import {atom, selector} from 'recoil'; export const todoListState = atom({ key: 'todoListState', default: [], }); export const todoListFilterState = atom({ key: 'todoListFilterState', default: 'Show All', }); export const filteredTodoListState = selector({ key: 'filteredTodoListState', get: ({get}) => { const filter = get(todoListFilterState); const list = get(todoListState); switch (filter) { case 'Show Completed': return list.filter(item => item.isComplete); case 'Show Uncompleted': return list.filter(item => !item.isComplete); default: return list; } }, }); export const todoListStatsState = selector({ key: 'todoListStatsState', get: ({get}) => { const todoList = get(todoListState); const totalNum = todoList.length; const totalCompletedNum = todoList.filter(item => item.isComplete).length; const totalUncompletedNum = totalNum - totalCompletedNum; const percentCompleted = totalNum === 0 ? 0 : (totalCompletedNum / totalNum) * 100; return { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted, }; }, }); ================================================ FILE: packages-ext/todo-example/src/components/Todo/Todo_types.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ export type TItem = { id: number, text: string, isComplete: boolean, }; ================================================ FILE: packages-ext/todo-example/src/index.css ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } ================================================ FILE: packages-ext/todo-example/src/index.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ import App from './App'; import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; ReactDOM.render( , document.getElementById('root'), ); ================================================ FILE: relay.config.js ================================================ module.exports = { // Configuration options accepted by the `relay-compiler` command-line tool and `babel-plugin-relay`. src: './packages/recoil-relay/__tests__', schema: './packages/recoil-relay/__tests__/mock-graphql/schema.graphql', exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**'], }; ================================================ FILE: rollup.config.js ================================================ import alias from '@rollup/plugin-alias'; import babel from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; import nodeResolve from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; import path from 'path'; import {terser} from 'rollup-plugin-terser'; const inputFile = 'packages/recoil/Recoil_index.js'; const externalLibs = ['react', 'react-dom']; const projectRootDir = path.resolve(__dirname); const defaultNodeResolveConfig = {}; const nodeResolvePlugin = nodeResolve(defaultNodeResolveConfig); const commonPlugins = [ babel({ presets: ['@babel/preset-react', '@babel/preset-flow'], plugins: [ 'babel-preset-fbjs/plugins/dev-expression', '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-proposal-optional-chaining', '@babel/transform-flow-strip-types', ], babelHelpers: 'bundled', }), { resolveId: source => { if (source === 'React') { return {id: 'react', external: true}; } if (source === 'ReactDOMLegacy_DEPRECATED') { return {id: 'react-dom', external: true}; } if (source === 'ReactNative') { return {id: 'react-native', external: true}; } return null; }, }, alias({ entries: [ { find: 'recoil-shared', replacement: path.resolve(projectRootDir, 'packages/shared'), }, ], }), nodeResolvePlugin, commonjs(), ]; const developmentPlugins = [ ...commonPlugins, replace({ 'process.env.NODE_ENV': JSON.stringify('development'), }), ]; const productionPlugins = [ ...commonPlugins, replace({ 'process.env.NODE_ENV': JSON.stringify('production'), }), terser({mangle: false}), ]; const configs = [ // CommonJS { input: inputFile, output: { file: `cjs/recoil.js`, format: 'cjs', exports: 'named', }, external: externalLibs, plugins: commonPlugins, }, // ES { input: inputFile, output: { file: `es/recoil.js`, format: 'es', exports: 'named', }, external: externalLibs, plugins: commonPlugins, }, // React Native { input: inputFile, output: { file: `native/recoil.js`, format: 'es', exports: 'named', }, external: [...externalLibs, 'react-native'], plugins: commonPlugins.map(plugin => { // Replace the default nodeResolve plugin if (plugin === nodeResolvePlugin) { return nodeResolve({ ...defaultNodeResolveConfig, extensions: ['.native.js', '.js'], }); } return plugin; }), }, // ES for Browsers { input: inputFile, output: { file: `es/recoil.mjs`, format: 'es', exports: 'named', }, external: externalLibs, plugins: productionPlugins, }, // UMD Development { input: inputFile, output: { file: `umd/recoil.js`, format: 'umd', name: 'Recoil', exports: 'named', globals: { react: 'React', 'react-dom': 'ReactDOM', }, }, external: externalLibs, plugins: developmentPlugins, }, // UMD Production { input: inputFile, output: { file: `umd/recoil.min.js`, format: 'umd', name: 'Recoil', exports: 'named', globals: { react: 'React', 'react-dom': 'ReactDOM', }, }, external: externalLibs, plugins: productionPlugins, }, ]; export default configs; ================================================ FILE: scripts/build.mjs ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ import {rollup} from 'rollup'; import {createInputOption, createOutputOption} from './rollup-configs.mjs'; import {exec} from 'child_process'; import * as fs from 'fs'; import {projectRootDir} from './project-root-dir.mjs'; import {createErrorHandler} from './utils.mjs'; // constants const BUILD_TARGET = 'build'; const PACKAGES = { recoil: { inputFile: 'Recoil_index.js', umdName: 'Recoil', builds: { common: ['cjs', 'es'], dev: ['umd'], prod: ['es-browsers', 'umd-prod'], native: ['native'], }, }, refine: { inputFile: 'Refine_index.js', umdName: 'Refine', builds: { common: ['cjs', 'es'], dev: ['umd'], prod: ['es-browsers', 'umd-prod'], }, }, 'recoil-sync': { inputFile: 'RecoilSync_index.js', umdName: 'RecoilSync', builds: { common: ['cjs', 'es'], dev: ['umd'], prod: ['es-browsers', 'umd-prod'], }, }, 'recoil-relay': { inputFile: 'RecoilRelay_index.js', umdName: 'RecoilRelay', builds: { common: ['cjs', 'es'], dev: ['umd'], prod: ['es-browsers', 'umd-prod'], }, }, }; const args = process.argv.slice(2); const target = args[0]; if (target === 'all' || target == null) { buildAll(); } else { if (PACKAGES[target] == null) { throw new Error(`Unknown build target ${target}`); } buildPackage(target, PACKAGES[target]); } async function buildAll() { console.log('Building all packages...\n'); await Promise.all( Object.entries(PACKAGES).map(([target, config]) => buildPackage(target, config) ) ); } async function buildPackage(target, config) { console.log(`Building ${target}...`); await Promise.all( Object.entries(config.builds).map(([buildType, packageTypes]) => buildRollup( `recoil (${buildType})`, createInputOption(buildType, target, config.inputFile), packageTypes.map((type) => createOutputOption(type, target, config.umdName) ) ) ) ); console.log('Copying files...'); fs.copyFile( `${projectRootDir}/packages/${target}/package-for-release.json`, `${BUILD_TARGET}/${target}/package.json`, fs.constants.COPYFILE_FICLONE, createErrorHandler('Failed to copy package-for-release.json'), ); fs.copyFile( `${projectRootDir}/README${target === 'recoil' ? '' : '-' + target}.md`, `${BUILD_TARGET}/${target}/README.md`, fs.constants.COPYFILE_FICLONE, createErrorHandler(`Failed to copy README-${target}.md`), ); fs.copyFile( `${projectRootDir}/CHANGELOG-${target}.md`, `${BUILD_TARGET}/${target}/CHANGELOG.md`, fs.constants.COPYFILE_FICLONE, createErrorHandler(`Failed to copy CHANGELOG-${target}.md`), ); fs.copyFile( `${projectRootDir}/LICENSE`, `${BUILD_TARGET}/${target}/LICENSE`, fs.constants.COPYFILE_FICLONE, createErrorHandler('Failed to copy LICENSE'), ); console.log('Copying index.d.ts for TypeScript support...'); fs.copyFile( `${projectRootDir}/typescript/${target}.d.ts`, `${BUILD_TARGET}/${target}/index.d.ts`, fs.constants.COPYFILE_FICLONE, createErrorHandler( `Failed to copy ${target}.d.ts for TypeScript index.d.ts`, ), ); console.log('Generating Flow type files...'); exec( `npx flow-copy-source packages/${target} ${BUILD_TARGET}/${target}/cjs`, err => { createErrorHandler('Failed to copy source files for Flow types')(err); fs.rename( `${BUILD_TARGET}/${target}/cjs/${config.inputFile}.flow`, `${BUILD_TARGET}/${target}/cjs/index.js.flow`, createErrorHandler(`Failed to rename ${config.inputFile}.js.flow`), ); }, ); console.log(`Successfully built ${target}!\n`); } async function buildRollup(name, inputOptions, outputOptionsList) { try { // create a bundle const bundle = await rollup(inputOptions); await Promise.all( outputOptionsList.map((outputOptions) => bundle.write(outputOptions)) ); } catch (error) { createErrorHandler(`Build for package ${name} failed!`)(error); } } ================================================ FILE: scripts/deploy_nightly_build.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format * @oncall recoil */ const {repository} = require('../package.json'); const {execSync} = require('child_process'); const DEST_FOLDER = 'nightly-build-files/'; const DEST_BRANCH = 'nightly'; const COMMIT_MSG = `Publishing a nightly build`; const cwd = process.cwd(); console.log('Starting deploy to Git...'); console.log(`Remove "${DEST_FOLDER}" folder...`); execSync(`rm -rf ${DEST_FOLDER}`, {cwd}); console.log(`Cloning the repository to "${DEST_FOLDER}" folder...`); execSync(`git clone -b ${DEST_BRANCH} ${repository} ${DEST_FOLDER} --depth 1`, { cwd, }); console.log('Copying sources...'); execSync(`cp -r build/recoil/* ${DEST_FOLDER}`, {cwd}); console.log('Committing and pushing...'); execSync( [ `cd ${DEST_FOLDER}`, 'git config --local include.path "$PWD/../.git/config"', // include orginal git config 'git add .', `git commit --allow-empty -m "${COMMIT_MSG}"`, `git push --tags ${repository} ${DEST_BRANCH}`, ].join('&&'), {cwd}, ); console.log('Deploying to git is finished.'); ================================================ FILE: scripts/pack.mjs ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ import * as child_process from 'child_process'; import * as fs from 'fs'; import {createErrorHandler} from './utils.mjs'; const BUILD_TARGET = 'build'; for (const dir of fs.readdirSync(BUILD_TARGET)) { if (fs.lstatSync(`${BUILD_TARGET}/${dir}`).isDirectory()) { console.log(`Packaging ${JSON.stringify(dir)}`); child_process.exec( 'yarn pack', {cwd: `${BUILD_TARGET}/${dir}`}, createErrorHandler(`Failed to package ${dir}`), ); } } ================================================ FILE: scripts/project-root-dir.mjs ================================================ // this file is ignored by prettier because it hits SyntaxError on `import.meta.url` import {fileURLToPath} from 'url'; import path from 'path'; const __filename = fileURLToPath(import.meta.url); export const projectRootDir = path.resolve(__filename, '../..'); ================================================ FILE: scripts/rollup-configs.mjs ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ import alias from '@rollup/plugin-alias'; import {babel} from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; import nodeResolve from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; import path from 'path'; import {terser} from 'rollup-plugin-terser'; import {projectRootDir} from './project-root-dir.mjs'; const externalLibs = [ 'react', 'react-dom', 'react-native', 'recoil', 'transit-js', 'relay-runtime', 'react-relay', ]; const defaultNodeResolveConfig = {}; const nodeResolvePlugin = nodeResolve(defaultNodeResolveConfig); const commonPlugins = [ babel({ presets: ['@babel/preset-react', '@babel/preset-flow'], plugins: [ 'babel-preset-fbjs/plugins/dev-expression', '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-proposal-optional-chaining', '@babel/transform-flow-strip-types', ], babelHelpers: 'bundled', }), { resolveId: source => { if (source === 'React') { return {id: 'react', external: true}; } if (source === 'ReactDOMLegacy_DEPRECATED') { return {id: 'react-dom', external: true}; } if (source === 'ReactNative') { return {id: 'react-native', external: true}; } if (source === 'Recoil') { return {id: 'recoil', external: true}; } return null; }, }, alias({ entries: [ { find: 'recoil-shared', replacement: path.resolve(projectRootDir, 'packages/shared'), }, // temporarily bundle refine into recoil-sync { find: 'refine', replacement: path.resolve( projectRootDir, 'packages/refine/Refine_index.js', ), }, ], }), nodeResolvePlugin, commonjs(), ]; const developmentPlugins = [ ...commonPlugins, replace({ 'process.env.NODE_ENV': JSON.stringify('development'), }), ]; const productionPlugins = [ ...commonPlugins, replace({ 'process.env.NODE_ENV': JSON.stringify('production'), }), terser({mangle: false}), ]; const nativePlugins = commonPlugins.map(plugin => // Replace the default nodeResolvePlugin plugin !== nodeResolvePlugin ? plugin : nodeResolve({ ...defaultNodeResolveConfig, extensions: ['.native.js', '.js'], }) ); export function createInputOption(buildType, folder, inputFile) { switch (buildType) { case 'common': return { input: `packages/${folder}/${inputFile}`, external: externalLibs, plugins: commonPlugins, }; case 'dev': return { input: `packages/${folder}/${inputFile}`, external: externalLibs, plugins: developmentPlugins, }; case 'prod': return { input: `packages/${folder}/${inputFile}`, external: externalLibs, plugins: productionPlugins, }; case 'native': return { input: `packages/${folder}/${inputFile}`, external: externalLibs, plugins: nativePlugins, }; default: throw new Error(`Unknown input type: ${buildType}`); } } const BUILD_TARGET = 'build'; const globals = { react: 'React', 'react-dom': 'ReactDOM', recoil: 'Recoil', 'transit-js': 'transit', 'relay-runtime': 'relay-runtime', 'react-relay': 'react-relay', }; export function createOutputOption(packageType, folder, UMDName) { switch (packageType) { case 'cjs': return { file: `${BUILD_TARGET}/${folder}/cjs/index.js`, format: 'cjs', exports: 'named', }; case 'es': return { file: `${BUILD_TARGET}/${folder}/es/index.js`, format: 'es', exports: 'named', }; case 'es-browsers': return { file: `${BUILD_TARGET}/${folder}/es/index.mjs`, format: 'es', exports: 'named', }; case 'umd': return { file: `${BUILD_TARGET}/${folder}/umd/index.js`, format: 'umd', name: UMDName, exports: 'named', globals, }; case 'umd-prod': return { file: `${BUILD_TARGET}/${folder}/umd/index.min.js`, format: 'umd', name: UMDName, exports: 'named', globals, }; case 'native': return { file: `${BUILD_TARGET}/${folder}/native/index.js`, format: 'es', exports: 'named', }; default: throw new Error(`Unknown output type: ${packageType}`); } } ================================================ FILE: scripts/utils.mjs ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ export function createErrorHandler(message) { return err => { if (err) { console.error(`${message}\n`); throw err; } }; } ================================================ FILE: setupJestMock.js ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @oncall recoil */ const Promise = require('promise-polyfill'); global.Promise = Promise; ================================================ FILE: typescript/index.d.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ // Stub file for dtslint ================================================ FILE: typescript/recoil-relay-test.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ import React = require('react'); import { atom } from 'recoil'; import { EnvironmentKey, RecoilRelayEnvironment, RecoilRelayEnvironmentProvider, graphQLQueryEffect, graphQLSubscriptionEffect, graphQLMutationEffect, graphQLSelector, graphQLSelectorFamily, } from 'recoil-relay'; import { IEnvironment, graphql } from 'relay-runtime'; import { useRelayEnvironment } from 'react-relay'; // Environment const myEnv: IEnvironment = useRelayEnvironment(); // EnvironmentKey const myEnvKey: EnvironmentKey = new EnvironmentKey('test'); // RecoilRelayEnvironment({ // $ExpectType ReactElement | null children: React.createElement('div'), environment: myEnv, environmentKey: myEnvKey, }); RecoilRelayEnvironment({ // $ExpectType ReactElement | null children: React.createElement('div'), environment: myEnv, environmentKey: myEnvKey, extraArg: 'ERROR', // $ExpectError }); RecoilRelayEnvironment({ // $ExpectError children: React.createElement('div'), environment: myEnv, }); // RecoilRelayEnvironmentProvider({ // $ExpectType ReactElement | null children: React.createElement('div'), environment: myEnv, environmentKey: myEnvKey, }); RecoilRelayEnvironmentProvider({ // $ExpectType ReactElement | null children: React.createElement('div'), environment: myEnv, environmentKey: myEnvKey, extraArg: 'ERROR', // $ExpectError }); RecoilRelayEnvironmentProvider({ // $ExpectError children: React.createElement('div'), environment: myEnv, }); // graphQLQueryEffect() atom({ key: 'key', effects: [graphQLQueryEffect({ environment: myEnv, query: graphql`query...`, variables: {foo: 'bar'}, mapResponse: (data: {str: string}) => data.str, })], }); atom({ key: 'key', effects: [graphQLQueryEffect({ environment: myEnvKey, query: graphql`query...`, variables: null, mapResponse: (data: {str: string}) => data.str, })], }); atom({ key: 'key', effects: [graphQLQueryEffect({ // $ExpectError environment: myEnv, query: graphql`query...`, variables: {foo: 'bar'}, mapResponse: (data: {str: string}) => data, })], }); // graphQLSubscriptionEffect() atom({ key: 'key', effects: [graphQLSubscriptionEffect({ environment: myEnv, subscription: graphql`subscription...`, variables: {foo: 'bar'}, mapResponse: (data: {str: string}) => data.str, })], }); atom({ key: 'key', effects: [graphQLSubscriptionEffect({ environment: myEnvKey, subscription: graphql`subscription...`, variables: null, mapResponse: (data: {str: string}) => data.str, })], }); atom({ key: 'key', effects: [graphQLSubscriptionEffect({ // $ExpectError environment: myEnv, subscription: graphql`subscription...`, variables: {foo: 'bar'}, mapResponse: (data: {str: string}) => data, })], }); // graphQLMutationEffect() atom({ key: 'key', effects: [graphQLMutationEffect({ environment: myEnv, mutation: graphql`mutation...`, variables: str => ({foo: str}), })], }); atom({ key: 'key', effects: [graphQLMutationEffect({ environment: myEnvKey, mutation: graphql`mutation...`, variables: () => null, })], }); atom({ key: 'key', effects: [graphQLMutationEffect({ environment: myEnv, mutation: graphql`mutation...`, variables: str => ({foo: str}), updater_UNSTABLE: (store, data) => { store; // $ExpectType RecordSourceSelectorProxy<{ bar: string; }> data; // $ExpectType { bar: string; } }, optimisticUpdater_UNSTABLE: (store, data) => { store; // $ExpectType RecordSourceSelectorProxy<{ bar: string; }> data; // $ExpectType { bar: string; } }, optimisticResponse_UNSTABLE: str => ({bar: str}), })], }); // graphQLSelector() graphQLSelector<{foo: string}, string>({ // $ExpectType RecoilState key: 'key', environment: myEnv, query: graphql`query...`, variables: {foo: '123'}, mapResponse: data => data.str, }); graphQLSelector<{foo: string}, string>({ // $ExpectType RecoilState key: 'key', environment: myEnvKey, query: graphql`query...`, variables: ({get}) => { get; // $ExpectType GetRecoilValue return null; }, mapResponse: data => data.str, }); graphQLSelector<{foo: string}, string>({ // $ExpectType RecoilState key: 'key', environment: myEnv, query: graphql`query...`, variables: {foo: '123'}, mapResponse: data => data.str, mutations: { mutation: graphql`mutation...`, variables: (str: string) => ({eggs: str}), }, }); graphQLSelector<{foo: string}, string>({ // $ExpectType RecoilState key: 'key', environment: myEnv, query: graphql`query...`, variables: {foo: '123'}, mapResponse: data => data.str, extraArg: 'ERROR', // $ExpectError }); graphQLSelector<{foo: string}, string>({ // $ExpectError key: 'key', query: graphql`query...`, variables: {foo: '123'}, mapResponse: data => data.str, }); graphQLSelector<{foo: string}, string>({ // $ExpectType RecoilState key: 'key', environment: myEnv, query: graphql`query...`, variables: 'ERROR', // $ExpectError mapResponse: data => data.str, }); // graphQLSelectorFamily() graphQLSelectorFamily<{foo: string}, string, string>({ // $ExpectType (parameter: string) => RecoilState key: 'key', environment: myEnv, query: graphql`query...`, variables: {foo: '123'}, mapResponse: data => data.str, }); graphQLSelectorFamily<{foo: string}, string, string>({ // $ExpectType (parameter: string) => RecoilState key: 'key', environment: myEnvKey, query: graphql`query...`, variables: (str: string) => ({get}) => { get; // $ExpectType GetRecoilValue return null; }, mapResponse: data => data.str, }); graphQLSelectorFamily<{foo: string}, string, string>({ // $ExpectType (parameter: string) => RecoilState key: 'key', environment: myEnv, query: graphql`query...`, variables: {foo: '123'}, mapResponse: data => data.str, mutations: { mutation: graphql`mutation...`, variables: (str: string) => (param: string) => ({eggs: str}), }, }); graphQLSelectorFamily<{foo: string}, string, string>({ // $ExpectType (parameter: string) => RecoilState key: 'key', environment: myEnv, query: graphql`query...`, variables: {foo: '123'}, mapResponse: data => data.str, extraArg: 'ERROR', // $ExpectError }); graphQLSelectorFamily<{foo: string}, string, string>({ // $ExpectError key: 'key', query: graphql`query...`, variables: {foo: '123'}, mapResponse: data => data.str, }); graphQLSelectorFamily<{foo: string}, string, string>({ // $ExpectType (parameter: string) => RecoilState key: 'key', environment: myEnv, query: graphql`query...`, variables: 'ERROR', // $ExpectError mapResponse: data => data.str, }); ================================================ FILE: typescript/recoil-relay.d.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ /* The current Relay TypeScript definitions aren't as careful as the Flow definitions. The GraphQLTaggedNode type isn't parameterized, so there is nothing that checks that the GraphQL query, subscription, or mutation actually has the expected type for the variables or response. */ import * as React from 'react'; import { RecoilState, AtomEffect, GetRecoilValue, SerializableParam, } from 'recoil'; import { IEnvironment, Variables, GraphQLTaggedNode, SelectorStoreUpdater, UploadableMap, } from 'relay-runtime'; export {}; // The Relay TypeScript definitions only declare the response as "unknown" interface Response { [key: string]: any; } export class EnvironmentKey { constructor(name: string); toJSON(): string; } export const RecoilRelayEnvironment: React.FC<{ environmentKey: EnvironmentKey, environment: IEnvironment, children: React.ReactNode, }>; export const RecoilRelayEnvironmentProvider: React.FC<{ environmentKey: EnvironmentKey, environment: IEnvironment, children: React.ReactNode, }>; export function graphQLQueryEffect(options: { environment: IEnvironment | EnvironmentKey, query: GraphQLTaggedNode, variables: Variables | null, mapResponse: (data: any) => T, }): AtomEffect; export function graphQLSubscriptionEffect(options: { environment: IEnvironment | EnvironmentKey, subscription: GraphQLTaggedNode, variables: Variables | null, mapResponse: (data: any) => T, }): AtomEffect; export function graphQLMutationEffect< T, TData extends Response >(options: { environment: IEnvironment | EnvironmentKey, mutation: GraphQLTaggedNode, variables: (newData: T) => Variables | null, updater_UNSTABLE?: SelectorStoreUpdater, optimisticUpdater_UNSTABLE?: SelectorStoreUpdater, optimisticResponse_UNSTABLE?: (newData: T) => TData, uploadables_UNSTABLE?: UploadableMap, }): AtomEffect; export function graphQLSelector< TVariables extends Variables, T, >(options: { key: string, environment: IEnvironment | EnvironmentKey, query: GraphQLTaggedNode, variables: | TVariables | ((callbacks: {get: GetRecoilValue}) => TVariables | null), mapResponse: ( response: any, callbacks: {get: GetRecoilValue, variables: TVariables}, ) => T, default?: T, mutations?: { mutation: GraphQLTaggedNode, variables: (newData: T) => Variables | null, }, }): RecoilState; export function graphQLSelectorFamily< TVariables extends Variables, P extends SerializableParam, T, TMutationVariables extends Variables = {}, >(options: { key: string, environment: IEnvironment | EnvironmentKey, query: GraphQLTaggedNode, variables: | TVariables | ((parameter: P) => | TVariables | null | ((callbacks: {get: GetRecoilValue}) => TVariables | null) ), mapResponse: ( response: any, callbacks: {get: GetRecoilValue, variables: TVariables}, ) => T | ((paremter: P) => T), default?: T | ((paremter: P) => T), mutations?: { mutation: GraphQLTaggedNode, variables: (newData: T) => | TMutationVariables | null | ((parameter: P) => TMutationVariables | null), }, }): (parameter: P) => RecoilState; ================================================ FILE: typescript/recoil-sync-test.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ import { DefaultValue, RecoilLoadable, AtomEffect, } from 'recoil'; import { // Keys ItemKey, StoreKey, // Core Recoil Sync RecoilSync, syncEffect, // Recoil Sync URL RecoilURLSync, RecoilURLSyncJSON, RecoilURLSyncTransit, urlSyncEffect, } from 'recoil-sync'; import { string, number, } from 'refine'; // Keys const itemKey: ItemKey = 'str'; const storeKey: StoreKey = 'str'; const DEFAULT_VALUE = new DefaultValue(); // RecoilSync(); // $ExpectError RecoilSync({children: null, bad: 'BAD'}); // $ExpectError RecoilSync({children: null, storeKey}); RecoilSync({children: null, storeKey: 0}); // $ExpectError RecoilSync({children: null, read: (x: ItemKey) => undefined}); RecoilSync({children: null, read: (x: ItemKey) => 'any'}); RecoilSync({children: null, read: (x: ItemKey) => DEFAULT_VALUE}); RecoilSync({children: null, read: (x: ItemKey) => Promise.resolve('any')}); RecoilSync({children: null, read: (x: ItemKey) => RecoilLoadable.of('any')}); RecoilSync({children: null, read: (x: number) => 'BAD'}); // $ExpectError RecoilSync({children: null, write: ({diff, allItems}) => { const diffMap: Map = diff; const allItemsMap: Map = allItems; const bad1: Map = allItems; // $ExpectError const bad2: string = allItems; // $ExpectError }}); RecoilSync({children: null, write: ({bad}) => {}}); // $ExpectError RecoilSync({children: null, listen: ({updateItem, updateAllKnownItems}) => { updateItem(); // $ExpectError updateItem(0); // $ExpectError updateItem(itemKey); // $ExpectError updateItem(itemKey, 0); // $ExpectType void updateAllKnownItems(); // $ExpectError updateAllKnownItems(0); // $ExpectError updateAllKnownItems(new Map()); // $ExpectType void }}); // syncEffect() syncEffect(); // $ExpectError syncEffect({refine: string()}); // $ExpectType AtomEffect syncEffect({refine: number()}); // $ExpectType AtomEffect syncEffect({refine: number()}); // $ExpectType AtomEffect syncEffect({ // $ExpectType AtomEffect itemKey, storeKey, refine: number(), syncDefault: true, }); syncEffect({ // $ExpectType AtomEffect refine: number(), read: ({read}) => { read(); // $ExpectError read(0); // $ExpectError read(itemKey); // $ExpectType unknown }, write: ({read, write, reset}) => { read(); // $ExpectError read(0); // $ExpectError read(itemKey); // $ExpectType unknown reset(); // $ExpectError reset(0); // $ExpectError reset(itemKey); // $ExpectType void write(); // $ExpectError write(0); // $ExpectError write(itemKey); // $ExpectError write(itemKey, 'any'); // $ExpectType void }, }); // RecoilURLSync(); // $ExpectError RecoilURLSync({ children: null, storeKey, location: {part: 'queryParams'}, serialize: String, deserialize: (x: string) => x, browserInterface: { replaceURL: (url: string) => {}, pushURL: (url: string) => {}, getURL: () => 'url', listenChangeURL: (handler) => { handler(); // $ExpectType void return () => {}; }, } }); // urlSyncEffect() urlSyncEffect({ // $ExpectType AtomEffect itemKey, storeKey, refine: number(), history: 'push', }); // RecoilURLSyncJSON(); // $ExpectError RecoilURLSyncJSON({ children: null, storeKey, location: {part: 'queryParams'}, serialize: String, // $ExpectError deserialize: (x: string) => x, }); RecoilURLSyncJSON({ children: null, storeKey, location: {part: 'queryParams'}, }); // class MyClass { prop: number; } RecoilURLSyncTransit(); // $ExpectError RecoilURLSyncTransit({ children: null, storeKey, location: {part: 'queryParams'}, serialize: String, // $ExpectError deserialize: (x: string) => x, }); RecoilURLSyncTransit({ children: null, storeKey, location: {part: 'queryParams'}, }); RecoilURLSyncTransit({ children: null, storeKey, location: {part: 'queryParams'}, handlers: [ { class: MyClass, tag: 'TAG', write: inst => inst.prop, read: _x => new MyClass(), }, ], }); ================================================ FILE: typescript/recoil-sync.d.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ import * as React from 'react'; import { DefaultValue, Loadable, AtomEffect, RecoilState, } from 'recoil'; import { Checker, } from '@recoiljs/refine'; //////////////////////// // Core RecoilSync //////////////////////// // Keys export type ItemKey = string; export type StoreKey = string; // - read export type ReadItem = (itemKey: ItemKey) => | DefaultValue | Promise | Loadable | unknown; // - write export type ItemDiff = Map; export type ItemSnapshot = Map; export interface WriteInterface { diff: ItemDiff; allItems: ItemSnapshot; } export type WriteItems = (state: WriteInterface) => void; // - listen export type UpdateItem = (itemKey: ItemKey, newValue: DefaultValue | unknown) => void; export type UpdateItems = (items: ItemSnapshot) => void; export type UpdateAllKnownItems = (items: ItemSnapshot) => void; export interface ListenInterface { updateItem: UpdateItem; updateItems: UpdateItems; updateAllKnownItems: UpdateAllKnownItems; } export type ListenToItems = (callbacks: ListenInterface) => void | (() => void); // export interface RecoilSyncOptions { children: React.ReactNode; storeKey?: StoreKey; write?: WriteItems; read?: ReadItem; listen?: ListenToItems; } export const RecoilSync: React.FC; // syncEffect() - read export interface ReadAtomInterface { read: ReadItem; } export type ReadAtom = (callbacks: ReadAtomInterface) => | DefaultValue | Promise | Loadable | unknown; // syncEffect() - write export type WriteItem = (itemKey: ItemKey, newValue: DefaultValue | unknown) => void; export type ResetItem = (itemKey: ItemKey) => void; export interface WriteAtomInterface { write: WriteItem; reset: ResetItem; read: ReadItem; } export type WriteAtom = (callbacks: WriteAtomInterface, newValue: DefaultValue | T) => void; // syncEffect() export interface SyncEffectOptions { storeKey?: StoreKey; itemKey?: ItemKey; refine: Checker; read?: ReadAtom; write?: WriteAtom; syncDefault?: boolean; } export function syncEffect(opt: SyncEffectOptions): AtomEffect; //////////////////////// // RecoilSync_URL //////////////////////// // export type LocationOption = | {part: 'href'} | {part: 'hash'} | {part: 'search'} | {part: 'queryParams', param?: string}; export interface BrowserInterface { replaceURL?: (url: string) => void; pushURL?: (url: string) => void; getURL?: () => string; listenChangeURL?: (handler: () => void) => () => void; } export interface RecoilURLSyncOptions { children: React.ReactNode; storeKey?: StoreKey; location: LocationOption; serialize: (data: unknown) => string; deserialize: (str: string) => unknown; browserInterface?: BrowserInterface; } export const RecoilURLSync: React.FC; // urlSyncEffect() export type HistoryOption = 'push' | 'replace'; export interface URLSyncEffectOptions extends SyncEffectOptions { history?: HistoryOption; } export function urlSyncEffect(opt: URLSyncEffectOptions): AtomEffect; // JSON export type RecoilURLSyncJSONOptions = Omit, 'deserialize'>; export const RecoilURLSyncJSON: React.FC; // Transit export interface TransitHandler any, S> { tag: string; class: T; write: (data: InstanceType) => S; read: (json: S) => InstanceType; } export interface RecoilURLSyncTransitOptions extends Omit, 'deserialize'> { handlers?: ReadonlyArray>; } export const RecoilURLSyncTransit: React.FC; ================================================ FILE: typescript/recoil-test.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ import { atom, atomFamily, constSelector, DefaultValue, errorSelector, isRecoilValue, noWait, readOnlySelector, RecoilBridge, RecoilRoot, RecoilValue, RecoilState, RecoilValueReadOnly, selector, selectorFamily, Snapshot, snapshot_UNSTABLE, useGetRecoilValueInfo_UNSTABLE, useGotoRecoilSnapshot, useRecoilBridgeAcrossReactRoots_UNSTABLE, useRecoilCallback, useRecoilSnapshot, useRecoilState, useRecoilStateLoadable, useRecoilTransactionObserver_UNSTABLE, useRecoilValue, useRecoilValueLoadable, useResetRecoilState, useSetRecoilState, waitForAll, waitForAllSettled, waitForAny, waitForNone, Loadable, RecoilLoadable, useRecoilTransaction_UNSTABLE, useRecoilRefresher_UNSTABLE, useRecoilStoreID, useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, useRecoilState_TRANSITION_SUPPORT_UNSTABLE, useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE, RecoilEnv, } from 'recoil'; import * as React from 'react'; /* eslint-disable @typescript-eslint/no-explicit-any */ // DefaultValue new DefaultValue(); // atom const myAtom: RecoilState = atom({ key: 'MyAtom', default: 5, }); const myAtomWithoutDefault: RecoilState = atom({ key: 'MyAtomWithoutDefault', }); { const atom1: RecoilState = atom({ key: 'Key', default: RecoilLoadable.of(123), }); const atom2: RecoilState = atom({ key: 'Key', default: atom.value(123), }); } // selector const mySelector1: RecoilValue = selector({ key: 'MySelector1', get: () => 5, }); const mySelector2: RecoilValue = selector({ key: 'MySelector2', get: () => '', }); { const mySelector3: RecoilValue = selector({ key: 'MySelector3', get: () => RecoilLoadable.of(123), }); const mySelector4: RecoilValue = selector({ key: 'MySelector3', get: () => selector.value(123), }); } // $ExpectError selector({ key: 'ExpectedError', get: () => '', }) as RecoilValueReadOnly; const readOnlySelectorSel = selector({ key: 'ReadOnlySelector', get: ({get}) => { get(myAtom) + 10; get(mySelector1); get(5); // $ExpectError return 5; }, }); const writeableSelector = selector({ key: 'WriteableSelector', get: ({get}) => { return get(mySelector1) + 10; }, set: ({get, set, reset}) => { get(myAtom); set(myAtom, 5); set(myAtom, 'hello'); // $ExpectError set(myAtom, new DefaultValue()); reset(myAtom); set(readOnlySelectorSel, 2); // $ExpectError reset(readOnlySelectorSel); // $ExpectError }, }); const callbackSelector = selector({ key: 'CallbackSelector', get: ({getCallback}) => { return getCallback( ({snapshot, set, reset, refresh, transact_UNSTABLE}) => () => { set(myAtom, 5); reset(myAtom); refresh(myAtom); transact_UNSTABLE(({get, set, reset}) => { get(myAtom); // $ExpectType number set(myAtom, 5); reset(myAtom); }); const ret = snapshot.getPromise(mySelector1); // $ExpectType Promise return ret; }, ); }, }); useRecoilValue(callbackSelector); // $ExpectType () => Promise // $ExpectError const selectorError1 = selector({ key: 'SelectorError1', // Missing get() }); selectorError1; const selectorError2 = selector({ key: 'SelectorError2', get: () => null, extraProp: 'error', // $ExpectError }); selectorError2; const selectorError3 = selector({ key: 'SelectorError3', get: ({badCallback}) => null, // $ExpectError }); selectorError3; // RecoilRoot RecoilRoot({children: React.createElement('div')}); // $ExpectType ReactElement | null // $ExpectType ReactElement | null RecoilRoot({ initializeState: ({set, reset}) => { set(myAtom, 5); reset(myAtom); set(readOnlySelectorSel, 2); // $ExpectError set(writeableSelector, 10); setUnvalidatedAtomValues({}); // $ExpectError set(writeableSelector, new DefaultValue()); }, children: React.createElement('div'), }); // $ExpectType ReactElement | null RecoilRoot({ override: true, children: React.createElement('div'), }); // $ExpectType ReactElement | null RecoilRoot({ override: false, children: React.createElement('div'), }); // RecoilEnv RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = false; RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = true; // Loadable function loadableTest(loadable: Loadable) { switch (loadable.state) { case 'hasValue': loadable.contents; // $ExpectType number loadable.getValue(); // $ExpectType number loadable.toPromise(); // $ExpectType Promise loadable.valueMaybe(); // $ExpectType number loadable.valueOrThrow(); // $ExpectType number loadable.errorMaybe(); // $ExpectType undefined loadable.errorOrThrow(); // $ExpectType any loadable.promiseMaybe(); // $ExpectType undefined loadable.promiseOrThrow(); // $ExpectType Promise break; case 'hasError': loadable.contents; // $ExpectType any loadable.getValue(); // $ExpectType number loadable.toPromise(); // $ExpectType Promise loadable.valueMaybe(); // $ExpectType undefined loadable.valueOrThrow(); // $ExpectType number loadable.errorMaybe(); // $ExpectType any loadable.errorOrThrow(); // $ExpectType any loadable.promiseMaybe(); // $ExpectType undefined loadable.promiseOrThrow(); // $ExpectType Promise break; case 'loading': loadable.contents; // $ExpectType Promise loadable.getValue(); // $ExpectType number loadable.toPromise(); // $ExpectType Promise loadable.valueMaybe(); // $ExpectType undefined loadable.valueOrThrow(); // $ExpectType number loadable.errorMaybe(); // $ExpectType undefined loadable.errorOrThrow(); // $ExpectType any loadable.promiseMaybe(); // $ExpectType Promise loadable.promiseOrThrow(); // $ExpectType Promise break; } loadable.valueMaybe()?.toString(); loadable.errorMaybe()?.toString(); loadable.is(loadable); // $ExpectType boolean } // Hooks const roAtom: RecoilValueReadOnly = {} as any; const waAtom: RecoilState = {} as any; const nsAtom: RecoilState = {} as any; // number or string useRecoilValue(roAtom); // $ExpectType string useRecoilValue(waAtom); // $ExpectType string useRecoilState(roAtom); // $ExpectError useRecoilState(waAtom); // $ExpectType [string, SetterOrUpdater] useRecoilState(waAtom); // $ExpectError useRecoilState(waAtom); // $ExpectError useRecoilValue(waAtom); // $ExpectError const t8: string | number = useRecoilValue(waAtom); useRecoilValue(nsAtom); // $ExpectError useRecoilValue(myAtom); // $ExpectType number useRecoilValue(mySelector1); // $ExpectType number useRecoilValue(readOnlySelectorSel); // $ExpectType number useRecoilValue(writeableSelector); // $ExpectType number useRecoilValue({}); // $ExpectError useRecoilValueLoadable(myAtom); // $ExpectType Loadable useRecoilValueLoadable(readOnlySelectorSel); // $ExpectType Loadable useRecoilValueLoadable(writeableSelector); // $ExpectType Loadable useRecoilValueLoadable({}); // $ExpectError useRecoilState(myAtom); // $ExpectType [number, SetterOrUpdater] useRecoilState(writeableSelector); // $ExpectType [number, SetterOrUpdater] useRecoilState(readOnlySelectorSel); // $ExpectError useRecoilState({}); // $ExpectError useRecoilStateLoadable(myAtom); useRecoilStateLoadable(writeableSelector); useRecoilStateLoadable(readOnlySelectorSel); // $ExpectError useRecoilStateLoadable({}); // $ExpectError useSetRecoilState(myAtom); // $ExpectType SetterOrUpdater useSetRecoilState(writeableSelector); // $ExpectType SetterOrUpdater useSetRecoilState(readOnlySelectorSel); // $ExpectError useSetRecoilState({}); // $ExpectError useResetRecoilState(myAtom); // $ExpectType Resetter useResetRecoilState(writeableSelector); // $ExpectType Resetter useResetRecoilState(readOnlySelectorSel); // $ExpectError useResetRecoilState({}); // $ExpectError useGetRecoilValueInfo_UNSTABLE(myAtom); // $ExpectError useGetRecoilValueInfo_UNSTABLE()(myAtom); // $ExpectType RecoilStateInfo useGetRecoilValueInfo_UNSTABLE()(mySelector2); // $ExpectType RecoilStateInfo useGetRecoilValueInfo_UNSTABLE()({}); // $ExpectError useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(roAtom); // $ExpectType string useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(waAtom); // $ExpectType string useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(myAtom); // $ExpectType number useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(mySelector1); // $ExpectType number useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(readOnlySelectorSel); // $ExpectType number useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(writeableSelector); // $ExpectType number useRecoilValue_TRANSITION_SUPPORT_UNSTABLE({}); // $ExpectError useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(myAtom); // $ExpectType Loadable useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(readOnlySelectorSel); // $ExpectType Loadable useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(writeableSelector); // $ExpectType Loadable useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE({}); // $ExpectError useRecoilState_TRANSITION_SUPPORT_UNSTABLE(myAtom); // $ExpectType [number, SetterOrUpdater] useRecoilState_TRANSITION_SUPPORT_UNSTABLE(writeableSelector); // $ExpectType [number, SetterOrUpdater] useRecoilState_TRANSITION_SUPPORT_UNSTABLE(readOnlySelectorSel); // $ExpectError useRecoilState_TRANSITION_SUPPORT_UNSTABLE({}); // $ExpectError useRecoilCallback( ({snapshot, set, reset, refresh, gotoSnapshot, transact_UNSTABLE}) => async () => { snapshot; // $ExpectType Snapshot snapshot.getID(); // $ExpectType SnapshotID await snapshot.getPromise(mySelector1); // $ExpectType number const loadable: Loadable = snapshot.getLoadable(mySelector1); gotoSnapshot(snapshot); gotoSnapshot(3); // $ExpectError gotoSnapshot(myAtom); // $ExpectError // eslint-disable-next-line @typescript-eslint/no-unused-vars const state: 'hasValue' | 'hasError' | 'loading' = loadable.state; loadable.contents; // $ExpectType any switch (loadable.state) { case 'hasValue': loadable.contents; // $ExpectType number break; case 'hasError': loadable.contents; // $ExpectType any break; case 'loading': loadable.contents; // $ExpectType Promise break; } set(myAtom, 5); set(myAtom, 'hello'); // $ExpectError reset(myAtom); refresh(myAtom); const release = snapshot.retain(); // $ExpectType () => void release(); // $ExpectType void snapshot.isRetained(); // $ExpectType boolean transact_UNSTABLE(({get, set, reset}) => { const x: number = get(myAtom); // eslint-disable-line @typescript-eslint/no-unused-vars set(myAtom, 1); set(myAtom, x => x + 1); reset(myAtom); }); }, ); // eslint-disable-next-line @typescript-eslint/no-unused-vars const transact: (p: number) => void = useRecoilTransaction_UNSTABLE( ({get, set, reset}) => (p: number) => { const x: number = get(myAtom); // eslint-disable-line @typescript-eslint/no-unused-vars set(myAtom, 1); set(myAtom, x => x + 1); reset(myAtom); }, ); /** * useRecoilTransactionObserver_UNSTABLE() */ { useRecoilTransactionObserver_UNSTABLE(({snapshot, previousSnapshot}) => { snapshot.getLoadable(myAtom); // $ExpectType Loadable snapshot.getPromise(mySelector1); // $ExpectType Promise snapshot.getPromise(mySelector2); // $ExpectType Promise previousSnapshot.getLoadable(myAtom); // $ExpectType Loadable previousSnapshot.getPromise(mySelector1); // $ExpectType Promise previousSnapshot.getPromise(mySelector2); // $ExpectType Promise for (const node of Array.from( snapshot.getNodes_UNSTABLE({isModified: true}), )) { const loadable = snapshot.getLoadable(node); // $ExpectType Loadable // eslint-disable-next-line @typescript-eslint/no-unused-vars const state: 'hasValue' | 'hasError' | 'loading' = loadable.state; } }); } /** * useGotoRecoilSnapshot() */ { const snapshot: Snapshot = {} as any; const gotoSnap = useGotoRecoilSnapshot(); gotoSnap(snapshot); gotoSnap(5); // $ExpectError gotoSnap(myAtom); // $ExpectError } /** * useRecoilSnapshot() */ { useRecoilSnapshot(); // $ExpectType Snapshot } /** * useRecoilRefresher() */ { useRecoilRefresher_UNSTABLE(); // $ExpectError useRecoilRefresher_UNSTABLE(false); // $ExpectError const refresher = useRecoilRefresher_UNSTABLE(mySelector1); refresher(false); // $ExpectError refresher(mySelector1); // $ExpectError refresher(); // $ExpectType void } /** * useRecoilBridgeAcrossReactRoots() */ { const RecoilBridgeComponent: typeof RecoilBridge = useRecoilBridgeAcrossReactRoots_UNSTABLE(); RecoilBridgeComponent({children: React.createElement('div')}); // eslint-disable-next-line @typescript-eslint/no-empty-function RecoilBridgeComponent({ children: React.createElement('div'), initializeState: () => {}, // $ExpectError }); } /** * ueRecoilStoreID() */ { useRecoilStoreID(2); // $ExpectError useRecoilStoreID(); // $ExpectType StoreID } // Other isRecoilValue(4); isRecoilValue(myAtom); isRecoilValue(null); isRecoilValue(mySelector1); /** * ================ UTILS ================ */ /** * atomFamily() tests */ { const myAtomFam = atomFamily({ key: 'myAtomFam1', default: (param: number) => param, }); const atm = myAtomFam(2); // $ExpectType RecoilState useRecoilValue(atm); // $ExpectType number myAtomFam(''); // $ExpectError // eslint-disable-next-line @typescript-eslint/no-unused-vars const myAtomFamilyWithoutDefault: (number: number) => RecoilState = atomFamily({key: 'MyAtomFamilyWithoutDefault'}); // eslint-disable-next-line @typescript-eslint/no-unused-vars const myAsyncAtomFamily: (number: number) => RecoilState = atomFamily< number, number >({ key: 'MyAsyncAtomFamily', default: (param: number) => Promise.resolve(param), }); } /** * selectorFamily() tests */ { const mySelectorFam = selectorFamily({ key: 'myAtomFam1', get: (param: number) => ({get}) => { get(mySelector1); // $ExpectType number return param; }, }); const atm = mySelectorFam(2); // $ExpectType RecoilValueReadOnly useRecoilValue(atm); // $ExpectType number mySelectorFam(''); // $ExpectError useRecoilState(mySelectorFam(3)); // $ExpectError const mySelectorFamWritable = selectorFamily({ key: 'myAtomFam1', get: (param: number) => ({get}) => { get(mySelector1); // $ExpectType number return param; }, set: (param: number) => () => { param; // $ExpectType number }, }); useRecoilState(mySelectorFamWritable(3))[0]; // $ExpectType number const mySelectorFamArray = selectorFamily({ key: 'myAtomFam1', get: (param: ReadonlyArray) => () => [...param, 9], }); mySelectorFamArray([1, 2, 3]); class MySerializableClass { toJSON() { return 'test'; } } const myJsonSerializableSelectorFam = selectorFamily({ key: 'mySelectorFam1', get: (param: {date: Date; class: MySerializableClass}) => () => param.date.toString() + JSON.stringify(param.class.toJSON()), }); myJsonSerializableSelectorFam({ date: new Date(), class: new MySerializableClass(), }); const callbackSelectorFamily = selectorFamily({ key: 'CallbackSelector', get: (param: number) => ({getCallback}) => { return getCallback(({snapshot}) => async (num: number) => { num; // $ExpectType number // $ExpectType number const ret = await snapshot.getPromise( mySelectorFamWritable(param + num), ); return ret; }); }, }); useRecoilValue(callbackSelectorFamily('hi')); // $ExpectError const cb = useRecoilValue(callbackSelectorFamily(1)); // $ExpectType (num: number) => Promise cb('hi'); // $ExpectError cb(2); // $ExpectType // $ExpectError const selectorFamilyError1 = selectorFamily({ key: 'SelectorFamilyError1', // Missing get() }); selectorFamilyError1; const selectorFamilyError2 = selectorFamily({ key: 'SelectorFamilyError2', get: () => () => null, extraProp: 'error', // $ExpectError }); selectorFamilyError2; const selectorFamilyError3 = selectorFamily({ key: 'SelectorFamilyError3', get: () => // $ExpectError ({badCallback}) => null, }); selectorFamilyError3; } /** * constSelector() tests */ { const mySel = constSelector(1); const mySel2 = constSelector('hello'); const mySel3 = constSelector([1, 2]); const mySel4 = constSelector({a: 1, b: '2'}); useRecoilValue(mySel); // $ExpectType 1 useRecoilValue(mySel2); // $ExpectType "hello" useRecoilValue(mySel3); // $ExpectType number[] useRecoilValue(mySel4); // $ExpectType { a: number; b: string; } constSelector(new Map([['k', 'v']])); // $ExpectType RecoilValueReadOnly> constSelector(new Set(['str'])); // $ExpectType RecoilValueReadOnly> class MyClass {} constSelector(new MyClass()); // $ExpectError } /** * errorSelector() tests */ { const mySel = errorSelector('Error msg'); useRecoilValue(mySel); // $ExpectType never errorSelector(2); // $ExpectError errorSelector({}); // $ExpectError } /** * readOnlySelector() tests */ { const myWritableSel: RecoilState = {} as any; readOnlySelector(myWritableSel); // $ExpectType RecoilValueReadOnly } /** * noWait() tests */ { const numSel: RecoilValueReadOnly = {} as any; const mySel = noWait(numSel); useRecoilValue(mySel); // $ExpectType Loadable } /** * waitForNone() tests */ { const numSel: RecoilValueReadOnly = {} as any; const strSel: RecoilValueReadOnly = {} as any; const mySel = waitForNone([numSel, strSel]); const mySel2 = waitForNone({a: numSel, b: strSel}); useRecoilValue(mySel)[0]; // $ExpectType Loadable useRecoilValue(mySel)[1]; // $ExpectType Loadable useRecoilValue(mySel2).a; // $ExpectType Loadable useRecoilValue(mySel2).b; // $ExpectType Loadable } /** * waitForAny() tests */ { const numSel: RecoilValueReadOnly = {} as any; const strSel: RecoilValueReadOnly = {} as any; const mySel = waitForAny([numSel, strSel]); const mySel2 = waitForAny({a: numSel, b: strSel}); useRecoilValue(mySel)[0]; // $ExpectType Loadable useRecoilValue(mySel)[1]; // $ExpectType Loadable useRecoilValue(mySel2).a; // $ExpectType Loadable useRecoilValue(mySel2).b; // $ExpectType Loadable } /** * waitForAll() tests */ { const numSel: RecoilValueReadOnly = {} as any; const strSel: RecoilValueReadOnly = {} as any; const mySel = waitForAll([numSel, strSel]); const mySel2 = waitForAll({a: numSel, b: strSel}); useRecoilValue(mySel)[0]; // $ExpectType number useRecoilValue(mySel)[1]; // $ExpectType string useRecoilValue(mySel2).a; // $ExpectType number useRecoilValue(mySel2).b; // $ExpectType string } /** * waitForAllSettled() tests */ { const numSel: RecoilValueReadOnly = {} as any; const strSel: RecoilValueReadOnly = {} as any; const mySel = waitForAllSettled([numSel, strSel]); const mySel2 = waitForAllSettled({a: numSel, b: strSel}); useRecoilValue(mySel)[0]; // $ExpectType Loadable useRecoilValue(mySel)[1]; // $ExpectType Loadable useRecoilValue(mySel2).a; // $ExpectType Loadable useRecoilValue(mySel2).b; // $ExpectType Loadable } /** * effects on atom() */ { atom({ key: 'thisismyrandomkey', default: 0, effects: [ ({ node, storeID, trigger, setSelf, onSet, resetSelf, getPromise, getLoadable, getInfo_UNSTABLE, }) => { node; // $ExpectType RecoilState storeID; // $ExpectType StoreID trigger; // $ExpectType "set" | "get" setSelf(new DefaultValue()); setSelf(1); setSelf('a'); // $ExpectError setSelf(Promise.resolve(new DefaultValue())); setSelf(Promise.resolve(1)); setSelf(Promise.resolve('a')); // $ExpectError setSelf(() => new DefaultValue()); setSelf(() => 1); setSelf(() => 'a'); // $ExpectError setSelf(() => Promise.resolve(new DefaultValue())); setSelf(() => Promise.resolve(1)); setSelf(() => Promise.resolve('a')); // $ExpectError setSelf(old => { old; // $ExpectType number | DefaultValue return old; }); onSet((val, oldVal, isReset) => { val; // $ExpectType number oldVal; // $ExpectType number | DefaultValue isReset; // $ExpectType boolean }); onSet('a'); // $ExpectError resetSelf(); resetSelf('a'); // $ExpectError getPromise(); // $ExpectError getPromise('a'); // $ExpectError getPromise(node); // $ExpectType Promise getLoadable(); // $ExpectError getLoadable('a'); // $ExpectError getLoadable(node); // $ExpectType Loadable getInfo_UNSTABLE(); // $ExpectError getInfo_UNSTABLE('a'); // $ExpectError getInfo_UNSTABLE(node); // $ExpectType RecoilStateInfo }, ], }); } /** * effects on atomFamily() */ { atomFamily({ key: 'myrandomatomfamilykey', default: (param: number) => param, effects: param => [ ({ node, storeID, trigger, setSelf, onSet, resetSelf, getPromise, getLoadable, getInfo_UNSTABLE, }) => { param; // $ExpectType number node; // $ExpectType RecoilState storeID; // $ExpectType StoreID trigger; // $ExpectType "set" | "get" setSelf(new DefaultValue()); setSelf(1); setSelf('a'); // $ExpectError setSelf(Promise.resolve(new DefaultValue())); setSelf(Promise.resolve(1)); setSelf(Promise.resolve('a')); // $ExpectError setSelf(() => new DefaultValue()); setSelf(() => 1); setSelf(() => 'a'); // $ExpectError setSelf(() => Promise.resolve(new DefaultValue())); setSelf(() => Promise.resolve(1)); setSelf(() => Promise.resolve('a')); // $ExpectError setSelf(old => { old; // $ExpectType number | DefaultValue return old; }); onSet(val => { val; // $ExpectType number }); onSet('a'); // $ExpectError resetSelf(); resetSelf('a'); // $ExpectError getPromise(); // $ExpectError getPromise('a'); // $ExpectError getPromise(node); // $ExpectType Promise getLoadable(); // $ExpectError getLoadable('a'); // $ExpectError getLoadable(node); // $ExpectType Loadable getInfo_UNSTABLE(); // $ExpectError getInfo_UNSTABLE('a'); // $ExpectError getInfo_UNSTABLE(node); // $ExpectType RecoilStateInfo }, ], }); } /** * snapshot_UNSTABLE() */ { snapshot_UNSTABLE(mutableSnapshot => mutableSnapshot.set(myAtom, 1)) .getLoadable(mySelector1) .valueOrThrow(); } { snapshot_UNSTABLE( mutableSnapshot => mutableSnapshot.set(myAtom, '1'), // $ExpectError ) .getLoadable(mySelector1) .valueOrThrow(); } /** * cachePolicy_UNSTABLE on selector() and selectorFamily() */ { selector({ key: 'ReadOnlySelectorSel_cachePolicy2', get: () => {}, cachePolicy_UNSTABLE: { eviction: 'keep-all', }, }); selector({ key: 'ReadOnlySelectorSel_cachePolicy3', get: () => {}, cachePolicy_UNSTABLE: {eviction: 'lru'}, // $ExpectError }); selector({ key: 'ReadOnlySelectorSel_cachePolicy4', get: () => {}, cachePolicy_UNSTABLE: { eviction: 'lru', maxSize: 10, }, }); selector({ key: 'ReadOnlySelectorSel_cachePolicy2', get: () => {}, cachePolicy_UNSTABLE: { eviction: 'most-recent', }, }); selectorFamily({ key: 'ReadOnlySelectorFSel_cachePolicy2', get: () => () => {}, cachePolicy_UNSTABLE: { eviction: 'keep-all', }, }); selectorFamily({ key: 'ReadOnlySelectorFSel_cachePolicy3', get: () => () => {}, cachePolicy_UNSTABLE: {eviction: 'lru'}, // $ExpectError }); selectorFamily({ key: 'ReadOnlySelectorFSel_cachePolicy4', get: () => () => {}, cachePolicy_UNSTABLE: { eviction: 'lru', maxSize: 10, }, }); selectorFamily({ key: 'ReadOnlySelectorFSel_cachePolicy2', get: () => () => {}, cachePolicy_UNSTABLE: { eviction: 'most-recent', }, }); } /* eslint-enable @typescript-eslint/no-explicit-any */ /** * Loadable Factory Tests */ { RecoilLoadable.of('x'); // $ExpectType Loadable RecoilLoadable.of(Promise.resolve('x')); // $ExpectType Loadable RecoilLoadable.of(RecoilLoadable.of('x')); // $ExpectType Loadable RecoilLoadable.error('x'); // $ExpectType ErrorLoadable RecoilLoadable.loading(); // $ExpectType LoadingLoadable const allLoadableArray = RecoilLoadable.all([ RecoilLoadable.of('str'), RecoilLoadable.of(123), ]); allLoadableArray.map(x => { x[0]; // $ExpectType string x[1]; // $ExpectType number x[2]; // $ExpectError }); const allLoadableObj = RecoilLoadable.all({ str: RecoilLoadable.of('str'), num: RecoilLoadable.of(123), }); allLoadableObj.map(x => { x.str; // $ExpectType string x.num; // $ExpectType number x.void; // $ExpectError }); const mixedAllLoadableArray = RecoilLoadable.all([ RecoilLoadable.of('str'), 'str', Promise.resolve('str'), ]).map(x => { x[0]; // $ExpectType string x[1]; // $ExpectType string x[2]; // $ExpectType string }); RecoilLoadable.isLoadable(false); // $ExpectType boolean RecoilLoadable.isLoadable(RecoilLoadable.of('x')); // $ExpectType boolean } ================================================ FILE: typescript/recoil.d.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ /** * This file is a manual translation of the flow types, which are the source of truth, so we should not introduce new terminology or behavior in this file. */ export {}; import * as React from 'react'; // state.d.ts type NodeKey = string; // node.d.ts export class DefaultValue { private __tag: 'DefaultValue'; } // recoilRoot.d.ts export type RecoilRootProps = | { initializeState?: (mutableSnapshot: MutableSnapshot) => void; override?: true; children: React.ReactNode; } | { override: false; children: React.ReactNode; }; // recoilEnv.d.ts export interface RecoilEnv { RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED: boolean; RECOIL_GKS_ENABLED: Set; } export const RecoilEnv: RecoilEnv; /** * Root component for managing Recoil state. Most Recoil hooks should be * called from a component nested in a */ export const RecoilRoot: React.FC; // Snapshot.d.ts declare const SnapshotID_OPAQUE: unique symbol; export interface SnapshotID { readonly [SnapshotID_OPAQUE]: true; } interface ComponentInfo { name: string; } interface RecoilStateInfo { loadable?: Loadable; isActive: boolean; isSet: boolean; isModified: boolean; // TODO report modified selectors type: 'atom' | 'selector'; deps: Iterable>; subscribers: { nodes: Iterable>; components: Iterable; }; } export class Snapshot { getID(): SnapshotID; getLoadable(recoilValue: RecoilValue): Loadable; getPromise(recoilValue: RecoilValue): Promise; getNodes_UNSTABLE(opts?: { isModified?: boolean; isInitialized?: boolean; }): Iterable>; getInfo_UNSTABLE(recoilValue: RecoilValue): RecoilStateInfo; map(cb: (mutableSnapshot: MutableSnapshot) => void): Snapshot; asyncMap( cb: (mutableSnapshot: MutableSnapshot) => Promise, ): Promise; retain(): () => void; isRetained(): boolean; } export class MutableSnapshot extends Snapshot { set: SetRecoilState; reset: ResetRecoilState; } declare const WrappedValue_OPAQUE: unique symbol; export interface WrappedValue { readonly [WrappedValue_OPAQUE]: true; } // Effect is called the first time a node is used with a export type AtomEffect = (param: { node: RecoilState; storeID: StoreID; parentStoreID_UNSTABLE?: StoreID; trigger: 'set' | 'get'; // Call synchronously to initialize value or async to change it later setSelf: ( param: | T | DefaultValue | Promise | WrappedValue | (( param: T | DefaultValue, ) => T | DefaultValue | Promise | WrappedValue), ) => void; resetSelf: () => void; // Subscribe callbacks to events. // Atom effect observers are called before global transaction observers onSet: ( param: (newValue: T, oldValue: T | DefaultValue, isReset: boolean) => void, ) => void; // Accessors to read other atoms/selectors getPromise: (recoilValue: RecoilValue) => Promise; getLoadable: (recoilValue: RecoilValue) => Loadable; getInfo_UNSTABLE: (recoilValue: RecoilValue) => RecoilStateInfo; }) => void | (() => void); // atom.d.ts interface AtomOptionsWithoutDefault { key: NodeKey; effects?: ReadonlyArray>; effects_UNSTABLE?: ReadonlyArray>; dangerouslyAllowMutability?: boolean; } interface AtomOptionsWithDefault extends AtomOptionsWithoutDefault { default: RecoilValue | Promise | Loadable | WrappedValue | T; } export type AtomOptions = | AtomOptionsWithoutDefault | AtomOptionsWithDefault; /** * Creates an atom, which represents a piece of writeable state */ export function atom(options: AtomOptions): RecoilState; export namespace atom { function value(value: T): WrappedValue; } export type GetRecoilValue = (recoilVal: RecoilValue) => T; export type SetterOrUpdater = ( valOrUpdater: ((currVal: T) => T) | T, ) => void; export type Resetter = () => void; export interface TransactionInterface_UNSTABLE { get(a: RecoilValue): T; set(s: RecoilState, u: ((currVal: T) => T) | T): void; reset(s: RecoilState): void; } export interface CallbackInterface { set: ( recoilVal: RecoilState, valOrUpdater: ((currVal: T) => T) | T, ) => void; reset: (recoilVal: RecoilState) => void; // eslint-disable-line @typescript-eslint/no-explicit-any refresh: (recoilValue: RecoilValue) => void; snapshot: Snapshot; gotoSnapshot: (snapshot: Snapshot) => void; transact_UNSTABLE: (cb: (i: TransactionInterface_UNSTABLE) => void) => void; } // selector.d.ts export interface SelectorCallbackInterface extends CallbackInterface { node: RecoilState; // TODO This isn't properly typed } export type GetCallback = , Return>( fn: (interface: SelectorCallbackInterface) => (...args: Args) => Return, ) => (...args: Args) => Return; export type SetRecoilState = ( recoilVal: RecoilState, newVal: T | DefaultValue | ((prevValue: T) => T | DefaultValue), ) => void; export type ResetRecoilState = (recoilVal: RecoilState) => void; // eslint-disable-line @typescript-eslint/no-explicit-any // export type EqualityPolicy = 'reference' | 'value'; TODO: removing while we discuss long term API export type EvictionPolicy = 'lru' | 'keep-all' | 'most-recent'; // TODO: removing while we discuss long term API // export type CachePolicy = // | {eviction: 'lru', maxSize: number, equality?: EqualityPolicy} // | {eviction: 'none', equality?: EqualityPolicy} // | {eviction?: undefined, equality: EqualityPolicy}; // TODO: removing while we discuss long term API // export interface CachePolicyWithoutEviction { // equality: EqualityPolicy; // } export type CachePolicyWithoutEquality = | {eviction: 'lru'; maxSize: number} | {eviction: 'keep-all'} | {eviction: 'most-recent'}; export interface ReadOnlySelectorOptions { key: string; get: (opts: { get: GetRecoilValue; getCallback: GetCallback; }) => Promise | RecoilValue | Loadable | WrappedValue | T; dangerouslyAllowMutability?: boolean; cachePolicy_UNSTABLE?: CachePolicyWithoutEquality; // TODO: using the more restrictive CachePolicyWithoutEquality while we discuss long term API } export interface ReadWriteSelectorOptions extends ReadOnlySelectorOptions { set: ( opts: { set: SetRecoilState; get: GetRecoilValue; reset: ResetRecoilState; }, newValue: T | DefaultValue, ) => void; } /** * Creates a selector which represents derived state. */ export function selector( options: ReadWriteSelectorOptions, ): RecoilState; export function selector( options: ReadOnlySelectorOptions, ): RecoilValueReadOnly; export namespace selector { function value(value: T): WrappedValue; } // hooks.d.ts /** * Returns the value of an atom or selector (readonly or writeable) and * subscribes the components to future updates of that state. */ export function useRecoilValue(recoilValue: RecoilValue): T; /** * Returns a Loadable representing the status of the given Recoil state * and subscribes the component to future updates of that state. Useful * for working with async selectors. */ export function useRecoilValueLoadable( recoilValue: RecoilValue, ): Loadable; /** * Returns a tuple where the first element is the value of the recoil state * and the second is a setter to update that state. Subscribes component * to updates of the given state. */ export function useRecoilState( recoilState: RecoilState, ): [T, SetterOrUpdater]; /** * Returns a tuple where the first element is a Loadable and the second * element is a setter function to update the given state. Subscribes * component to updates of the given state. */ export function useRecoilStateLoadable( recoilState: RecoilState, ): [Loadable, SetterOrUpdater]; /** * Returns a setter function for updating Recoil state. Does not subscribe * the component to the given state. */ export function useSetRecoilState( recoilState: RecoilState, ): SetterOrUpdater; /** * Returns a function that will reset the given state to its default value. */ export function useResetRecoilState(recoilState: RecoilState): Resetter; // eslint-disable-line @typescript-eslint/no-explicit-any /** * Returns current info about an atom */ export function useGetRecoilValueInfo_UNSTABLE(): ( recoilValue: RecoilValue, ) => RecoilStateInfo; /** * Experimental version of hooks for useTransition() support */ export function useRecoilValue_TRANSITION_SUPPORT_UNSTABLE( recoilValue: RecoilValue, ): T; export function useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE( recoilValue: RecoilValue, ): Loadable; export function useRecoilState_TRANSITION_SUPPORT_UNSTABLE( recoilState: RecoilState, ): [T, SetterOrUpdater]; /** * Returns a function that will run the callback that was passed when * calling this hook. Useful for accessing Recoil state in response to * events. */ export function useRecoilCallback, Return>( fn: (interface: CallbackInterface) => (...args: Args) => Return, deps?: ReadonlyArray, ): (...args: Args) => Return; /** * Returns a function that executes an atomic transaction for updating Recoil state. */ export function useRecoilTransaction_UNSTABLE< Args extends ReadonlyArray, >( fn: (interface: TransactionInterface_UNSTABLE) => (...args: Args) => void, deps?: ReadonlyArray, ): (...args: Args) => void; export function useRecoilTransactionObserver_UNSTABLE( callback: (opts: {snapshot: Snapshot; previousSnapshot: Snapshot}) => void, ): void; /** * Updates Recoil state to match the provided snapshot. */ export function useGotoRecoilSnapshot(): (snapshot: Snapshot) => void; /** * Returns a snapshot of the current Recoil state and subscribes the component * to re-render when any state is updated. */ export function useRecoilSnapshot(): Snapshot; // useRecoilRefresher.d.ts /** * Clears the cache for a selector causing it to be reevaluated. */ export function useRecoilRefresher_UNSTABLE( recoilValue: RecoilValue, ): () => void; // useRecoilBridgeAcrossReactRoots.d.ts export const RecoilBridge: React.FC<{children: React.ReactNode}>; /** * Returns a component that acts like a but shares the same store * as the current . */ export function useRecoilBridgeAcrossReactRoots_UNSTABLE(): typeof RecoilBridge; // useRecoilStoreID declare const StoreID_OPAQUE: unique symbol; export interface StoreID { readonly [StoreID_OPAQUE]: true; } /** * Returns an ID for the currently active state store of the host */ export function useRecoilStoreID(): StoreID; // loadable.d.ts interface BaseLoadable { getValue: () => T; toPromise: () => Promise; valueOrThrow: () => T; errorOrThrow: () => any; promiseOrThrow: () => Promise; is: (other: Loadable) => boolean; map: (map: (from: T) => Loadable | Promise | S) => Loadable; } interface ValueLoadable extends BaseLoadable { state: 'hasValue'; contents: T; valueMaybe: () => T; errorMaybe: () => undefined; promiseMaybe: () => undefined; } interface LoadingLoadable extends BaseLoadable { state: 'loading'; contents: Promise; valueMaybe: () => undefined; errorMaybe: () => undefined; promiseMaybe: () => Promise; } interface ErrorLoadable extends BaseLoadable { state: 'hasError'; contents: any; valueMaybe: () => undefined; errorMaybe: () => any; promiseMaybe: () => undefined; } export type Loadable = | ValueLoadable | LoadingLoadable | ErrorLoadable; // recoilValue.d.ts declare class AbstractRecoilValue { __tag: [T]; __cTag: (t: T) => void; // for contravariance key: NodeKey; constructor(newKey: NodeKey); toJSON(): {key: string}; } declare class AbstractRecoilValueReadonly { __tag: [T]; key: NodeKey; constructor(newKey: NodeKey); toJSON(): {key: string}; } export class RecoilState extends AbstractRecoilValue {} export class RecoilValueReadOnly extends AbstractRecoilValueReadonly {} export type RecoilValue = RecoilValueReadOnly | RecoilState; /** * Returns true if the parameter is a Recoil atom or selector. */ export function isRecoilValue(val: unknown): val is RecoilValue; // eslint-disable-line @typescript-eslint/no-explicit-any /** Utilities */ // bigint not supported yet type Primitive = undefined | null | boolean | number | symbol | string; interface HasToJSON { toJSON(): SerializableParam; } export type SerializableParam = | Primitive | HasToJSON | ReadonlyArray | ReadonlySet | ReadonlyMap | Readonly<{[key: string]: SerializableParam}>; interface AtomFamilyOptionsWithoutDefault { key: NodeKey; dangerouslyAllowMutability?: boolean; effects?: | ReadonlyArray> | ((param: P) => ReadonlyArray>); effects_UNSTABLE?: | ReadonlyArray> | ((param: P) => ReadonlyArray>); // cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction; TODO: removing while we discuss long term API } interface AtomFamilyOptionsWithDefault extends AtomFamilyOptionsWithoutDefault { default: | RecoilValue | Promise | Loadable | WrappedValue | T | (( param: P, ) => T | RecoilValue | Promise | Loadable | WrappedValue); } export type AtomFamilyOptions = | AtomFamilyOptionsWithDefault | AtomFamilyOptionsWithoutDefault; /** * Returns a function which returns a memoized atom for each unique parameter value. */ export function atomFamily( options: AtomFamilyOptions, ): (param: P) => RecoilState; export interface ReadOnlySelectorFamilyOptions { key: string; get: ( param: P, ) => (opts: { get: GetRecoilValue; getCallback: GetCallback; }) => Promise | RecoilValue | Loadable | WrappedValue | T; // cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction; TODO: removing while we discuss long term API cachePolicy_UNSTABLE?: CachePolicyWithoutEquality; // TODO: using the more restrictive CachePolicyWithoutEquality while we discuss long term API dangerouslyAllowMutability?: boolean; } export interface ReadWriteSelectorFamilyOptions< T, P extends SerializableParam, > { key: string; get: ( param: P, ) => (opts: { get: GetRecoilValue; getCallback: GetCallback; }) => Promise | Loadable | WrappedValue | RecoilValue | T; set: ( param: P, ) => ( opts: {set: SetRecoilState; get: GetRecoilValue; reset: ResetRecoilState}, newValue: T | DefaultValue, ) => void; // cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction; TODO: removing while we discuss long term API cachePolicy_UNSTABLE?: CachePolicyWithoutEquality; // TODO: using the more restrictive CachePolicyWithoutEquality while we discuss long term API dangerouslyAllowMutability?: boolean; } /** * Returns a function which returns a memoized atom for each unique parameter value. */ export function selectorFamily( options: ReadWriteSelectorFamilyOptions, ): (param: P) => RecoilState; /** * Returns a function which returns a memoized atom for each unique parameter value. */ export function selectorFamily( options: ReadOnlySelectorFamilyOptions, ): (param: P) => RecoilValueReadOnly; /** * Returns a selector that always has a constant value. */ export function constSelector( constant: T, ): RecoilValueReadOnly; /** * Returns a selector which is always in the provided error state. */ export function errorSelector(message: string): RecoilValueReadOnly; /** * Casts a selector to be a read-only selector */ export function readOnlySelector( atom: RecoilValue, ): RecoilValueReadOnly; /** * Returns a selector that has the value of the provided atom or selector as a Loadable. * This means you can use noWait() to avoid entering an error or suspense state in * order to manually handle those cases. */ export function noWait( state: RecoilValue, ): RecoilValueReadOnly>; /* eslint-disable @typescript-eslint/no-explicit-any */ export type UnwrapRecoilValue = T extends RecoilValue ? R : never; export type UnwrapRecoilValues< T extends Array> | {[key: string]: RecoilValue}, > = { [P in keyof T]: UnwrapRecoilValue; }; export type UnwrapRecoilValueLoadables< T extends Array> | {[key: string]: RecoilValue}, > = { [P in keyof T]: Loadable>; }; export function waitForNone< RecoilValues extends Array> | [RecoilValue], >( param: RecoilValues, ): RecoilValueReadOnly>; export function waitForNone< RecoilValues extends {[key: string]: RecoilValue}, >( param: RecoilValues, ): RecoilValueReadOnly>; export function waitForAny< RecoilValues extends Array> | [RecoilValue], >( param: RecoilValues, ): RecoilValueReadOnly>; export function waitForAny< RecoilValues extends {[key: string]: RecoilValue}, >( param: RecoilValues, ): RecoilValueReadOnly>; export function waitForAll< RecoilValues extends Array> | [RecoilValue], >(param: RecoilValues): RecoilValueReadOnly>; export function waitForAll< RecoilValues extends {[key: string]: RecoilValue}, >(param: RecoilValues): RecoilValueReadOnly>; export function waitForAllSettled< RecoilValues extends Array> | [RecoilValue], >( param: RecoilValues, ): RecoilValueReadOnly>; export function waitForAllSettled< RecoilValues extends {[key: string]: RecoilValue}, >( param: RecoilValues, ): RecoilValueReadOnly>; export type UnwrapLoadable = T extends Loadable ? R : T extends Promise ? P : T; export type UnwrapLoadables = { [P in keyof T]: UnwrapLoadable; }; /* eslint-disable @typescript-eslint/no-unused-vars */ export namespace RecoilLoadable { /** * Factory to make a Loadable object. If a Promise is provided the Loadable will * be in a 'loading' state until the Promise is either resolved or rejected. */ function of(x: T | Promise | Loadable): Loadable; /** * Factory to make a Loadable object in an error state. */ function error(x: any): ErrorLoadable; /** * Factory to make a loading Loadable which never resolves. */ function loading(): LoadingLoadable; /** * Factory to make a Loadable which is resolved when all of the Loadables provided * to it are resolved or any one has an error. The value is an array of the values * of all of the provided Loadables. This is comparable to Promise.all() for Loadables. * Similar to Promise.all(), inputs may be Loadables, Promises, or literal values. */ function all]>( inputs: Inputs, ): Loadable>; function all( inputs: Inputs, ): Loadable>; /** * Returns true if the provided parameter is a Loadable type. */ function isLoadable(x: any): x is Loadable; } /* eslint-enable @typescript-eslint/no-unused-vars */ /* eslint-enable @typescript-eslint/no-explicit-any */ /** * Factory to produce a Recoil snapshot object with all atoms in the default state. */ export function snapshot_UNSTABLE( initializeState?: (mutableSnapshot: MutableSnapshot) => void, ): Snapshot; ================================================ FILE: typescript/refine-test.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ import { CheckerReturnType, bool, stringLiterals, string, number, mixed, nullable, voidable, date, custom, asType, match, union, or, object, dict, array, tuple, map, set, writableObject, writableArray, writableDict, optional, } from 'refine'; // turn of lint for unused test vars /* eslint-disable @typescript-eslint/no-unused-vars */ /** * * utility type tests * */ { const checker = object({a: number()}); type MyType = CheckerReturnType; const x: MyType = { a: 1, }; const y: MyType = { a: 'test', // $ExpectError }; } /** * * primitive tests * */ { const rboolean = bool()({}); if (rboolean.type === 'success') { const v: boolean = rboolean.value; } const rstring = string()({}); if (rstring.type === 'success') { const v: string = rstring.value; } const rnumber = number()({}); if (rnumber.type === 'success') { const v: number = rnumber.value; } const runknown = mixed()({}); if (runknown.type === 'success') { const v: unknown = runknown.value; } const rliterals = stringLiterals<'a' | 'b'>({a: 'a', b: 'b'})({}); if (rliterals.type === 'success') { const v: 'a' | 'b' = rliterals.value; } const rsorn = or(string(), number())({}); if (rsorn.type === 'success') { const v: string | number = rsorn.value; } const rsunionn = union(string(), number(), bool())({}); if (rsunionn.type === 'success') { const v: string | number | boolean = rsunionn.value; } const rvoidablestring = voidable(string())({}); if (rvoidablestring.type === 'success') { const v: undefined | string = rvoidablestring.value; const x: string = rvoidablestring.value; // $ExpectError const z: string | null = rvoidablestring.value; // $ExpectError } const rnullable = nullable(string())({}); if (rnullable.type === 'success') { const v: null | string | undefined = rnullable.value; const x: string = rnullable.value; // $ExpectError const z: string | undefined = rnullable.value; // $ExpectError } const rdate = date()({}); if (rdate.type === 'success') { const v: Date = rdate.value; } } /** * * collection tests * */ { const rarray = array(string())({}); if (rarray.type === 'success') { const s: string = rarray.value[0]; rarray.value.push('test'); // $ExpectError } const rwarray = writableArray(string())({}); if (rwarray.type === 'success') { const s: string = rwarray.value[0]; rwarray.value.push('test'); } } { const check = object({a: optional(string()), b: string()}); type ObjectWithOptional = CheckerReturnType; const r1: ObjectWithOptional = { b: 'test', }; const r2: ObjectWithOptional = { b: 'test', a: 'test', }; const result = check({}); if (result.type === 'success') { result.value.b.includes('test'); } else { result.message.includes(''); } const rwobject = writableObject({c: number()})({}); if (rwobject.type === 'success') { rwobject.value.c = 1; const v: {c: number} = rwobject.value; } } { const rdict = dict(string())({}); if (rdict.type === 'success') { const v: {readonly [key: string]: string} = rdict.value; } const rwdict = writableDict(number())({}); if (rwdict.type === 'success') { rwdict.value.key = 1; const v: {[key: string]: number} = rwdict.value; } } { const rtuple = tuple(string(), number())({}); if (rtuple.type === 'success') { const v: readonly [string, number] = rtuple.value; } } { const rmap = map(array(string()), dict(number()))({}); if (rmap.type === 'success') { const v: ReadonlyMap< readonly string[], Readonly<{[key: string]: number}> > = rmap.value; } } { const rset = set(date())({}); if (rset.type === 'success') { const v: ReadonlySet = rset.value; } } /** * * utilities * */ { const rasnum = asType(string(), s => parseInt(s, 10))({}); if (rasnum.type === 'success') { const v: number = rasnum.value; } const rmatch = match( asType(number(), n => n.toString()), asType(bool(), b => b.toString()), string(), )({}); if (rmatch.type === 'success') { const v: string = rmatch.value; } class Custom {} const isCustomClass = custom(value => value instanceof Custom ? value : null, ); const rcustomclass = isCustomClass({}); if (rcustomclass.type === 'success') { const v: Custom = rcustomclass.value; } } ================================================ FILE: typescript/refine.d.ts ================================================ // Minimum TypeScript Version: 3.9 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @oncall recoil */ type CheckerResult = V extends Checker ? Result : V extends OptionalPropertyChecker ? OptionalResult : never; export type CheckerReturnType = V extends Checker ? Result : never; /** * This file is a manual translation of the flow types, which are the source of truth, so we should not introduce new terminology or behavior in this file. */ export class Path { constructor(parent?: Path | null, field?: string); extend(field: string): Path; toString(): string; } /** * the result of failing to match a value to its expected type */ export type CheckFailure = Readonly<{ type: 'failure'; message: string; path: Path; }>; /** * the result of successfully matching a value to its expected type */ export type CheckSuccess = Readonly<{ type: 'success'; value: V; // if using `nullable` with the `nullWithWarningWhenInvalid` option, // failures will be appended here warnings: ReadonlyArray; }>; /** * the result of checking whether a type matches an expected value */ export type CheckResult = CheckSuccess | CheckFailure; /** * a function which checks if a given mixed value matches a type V, * returning the value if it does, otherwise a failure message. */ export type Checker = (value: unknown, path?: Path) => CheckResult; /** * wrap value in an object signifying successful checking */ export function success( value: V, warnings: ReadonlyArray, ): CheckSuccess; /** * indicate typecheck failed */ export function failure(message: string, path: Path): CheckFailure; /** * utility function for composing checkers */ export function compose( checker: Checker, next: (success: CheckSuccess, path: Path) => CheckResult, ): Checker; /** * function to assert that a given value matches a checker */ export type AssertionFunction = (value: unknown) => V; /** * function to coerce a given value to a checker type, returning null if invalid */ export type CoercionFunction = (value: unknown) => V | null | undefined; /** * create a function to assert a value matches a checker, throwing otherwise * * For example, * * ``` * const assert = assertion(array(number())); * const value: Array = assert([1,2]); * * try { * // should throw with `Refine.js assertion failed: ...` * const invalid = assert('test'); * } catch { * } * ``` */ export function assertion( checker: Checker, errorMessage?: string, ): AssertionFunction; /** * create a CoercionFunction given a checker. * * Allows for null-coercing a value to a given type using a checker. Optionally * provide a callback which receives the full check * result object (e.g. for logging). * * Example: * * ```javascript * import {coercion, record, string} from 'refine'; * import MyLogger from './MyLogger'; * * const Person = record({ * name: string(), * hobby: string(), * }); * * const coerce = coercion(Person, result => MyLogger.log(result)); * * declare value: mixed; * * // ?Person * const person = coerce(value); * ``` */ export function coercion( checker: Checker, onResult?: (checker: CheckResult) => void, ): CoercionFunction; /** * checker to assert if a mixed value is an array of * values determined by a provided checker */ export function array(valueChecker: Checker): Checker>; /** * checker to assert if a mixed value is a tuple of values * determined by provided checkers. Extra entries are ignored. * * Example: * ```jsx * const checker = tuple( number(), string() ); * ``` * * Example with optional trailing entry: * ```jsx * const checker = tuple( number(), voidable(string())); * ``` */ export function tuple( ...checkers: Checkers ): Checker}>>; /** * checker to assert if a mixed value is a string-keyed dict of * values determined by a provided checker */ export function dict( valueChecker: Checker, ): Checker>; // expose opaque version of optional property as public api, // forcing consistent usage of built-in `optional` to define optional properties declare const __opaque: unique symbol; export interface OptionalPropertyChecker { readonly [__opaque]: T; } /** * checker which can only be used with `object` or `writablObject`. Marks a * field as optional, skipping the key in the result if it doesn't * exist in the input. * * @example * ```jsx * import {object, string, optional} from 'refine'; * * const checker = object({a: string(), b: optional(string())}); * assert(checker({a: 1}).type === 'success'); * ``` */ export function optional(checker: Checker): OptionalPropertyChecker; type CheckerObject = Readonly<{ [key: string]: Checker | OptionalPropertyChecker; }>; type CheckersToValues = { [K in keyof Checkers]: CheckerResult; }; type WhereValue = Pick< Checkers, { [Key in keyof Checkers]: Checkers[Key] extends Condition ? Key : never; }[keyof Checkers] >; type RequiredCheckerProperties = WhereValue< Checkers, Checker >; type OptionalCheckerProperties = Partial< WhereValue> >; type ObjectCheckerResult = CheckersToValues< RequiredCheckerProperties & OptionalCheckerProperties >; /** * checker to assert if a mixed value is a fixed-property object, * with key-value pairs determined by a provided object of checkers. * Any extra properties in the input object values are ignored. * Class instances are not supported, use the custom() checker for those. * * Example: * ```jsx * const myObject = object({ * name: string(), * job: object({ * years: number(), * title: string(), * }), * }); * ``` * * Properties can be optional using `voidable()` or have default values * using `withDefault()`: * ```jsx * const customer = object({ * name: string(), * reference: voidable(string()), * method: withDefault(string(), 'email'), * }); * ``` */ export function object( checkers: Checkers, ): Checker>>; /** * checker to assert if a mixed value is a Set type */ export function set(checker: Checker): Checker>; /** * checker to assert if a mixed value is a Map. */ export function map( keyChecker: Checker, valueChecker: Checker, ): Checker>; /** * identical to `array()` except the resulting value is a writable flow type. */ export function writableArray(valueChecker: Checker): Checker; /** * identical to `dict()` except the resulting value is a writable flow type. */ export function writableDict( valueChecker: Checker, ): Checker<{[key: string]: V}>; /** * identical to `object()` except the resulting value is a writable flow type. */ export function writableObject( checkers: Checkers, ): Checker>; /** * function which takes a json string, parses it, * and matches it with a checker (returning null if no match) */ export type JSONParser = (input: string | null | undefined) => T; /** * creates a JSON parser which will error if the resulting value is invalid */ export function jsonParserEnforced( checker: Checker, suffix?: string, ): JSONParser; /** * convienience function to wrap a checker in a function * for easy JSON string parsing. */ export function jsonParser( checker: Checker, ): JSONParser; /** * a mixed (i.e. untyped) value */ export function mixed(): Checker; /** * checker to assert if a mixed value matches a literal value */ export function literal( literalValue: T, ): Checker; /** * boolean value checker */ export function bool(): Checker; /** * checker to assert if a mixed value is a number */ export function number(): Checker; /** * Checker to assert if a mixed value is a string. * * Provide an optional RegExp template to match string against. */ export function string(regex?: RegExp): Checker; /** * checker to assert if a mixed value matches a union of string literals. * Legal values are provided as keys in an object and may be translated by * providing values in the object. * * For example: * ```jsx * ``` */ export function stringLiterals(enumValues: { readonly [key: string]: T; }): Checker; /** * checker to assert if a mixed value is a Date object */ export function date(): Checker; /** * Cast the type of a value after passing a given checker * * For example: * * ```javascript * import {string, asType} from 'refine'; * * opaque type ID = string; * * const IDChecker: Checker = asType(string(), s => (s: ID)); * ``` */ export function asType( checker: Checker
, cast: (input: A) => B, ): Checker; /** * checker which asserts the value matches * at least one of the two provided checkers */ export function or( aChecker: Checker, bChecker: Checker, ): Checker; type ElementType = T extends unknown[] ? T[number] : T; /** * checker which asserts the value matches * at least one of the provided checkers */ export function union>>( ...checkers: Checkers ): Checker}>>; /** * Provide a set of checkers to check in sequence to use the first match. * This is similar to union(), but all checkers must have the same type. * * This can be helpful for supporting backward compatibility. For example the * following loads a string type, but can also convert from a number as the * previous version or pull from an object as an even older version: * * ```jsx * const backwardCompatibilityChecker: Checker = match( * string(), * asType(number(), num => `${num}`), * asType(record({num: number()}), obj => `${obj.num}`), * ); * ``` */ export function match(...checkers: ReadonlyArray>): Checker; /** * wraps a given checker, making the valid value nullable * * By default, a value passed to nullable must match the checker spec exactly * when it is not null, or it will fail. * * passing the `nullWithWarningWhenInvalid` enables gracefully handling invalid * values that are less important -- if the provided checker is invalid, * the new checker will return null. * * For example: * * ```javascript * import {nullable, record, string} from 'refine'; * * const Options = record({ * // this must be a non-null string, * // or Options is not valid * filename: string(), * * // if this field is not a string, * // it will be null and Options will pass the checker * description: nullable(string(), { * nullWithWarningWhenInvalid: true, * }) * }) * * const result = Options({filename: 'test', description: 1}); * * invariant(result.type === 'success'); * invariant(result.value.description === null); * invariant(result.warnings.length === 1); // there will be a warning * ``` */ export function nullable( checker: Checker, options?: Readonly<{ // if this is true, the checker will not fail // validation if an invalid value is provided, instead // returning null and including a warning as to the invalid type. nullWithWarningWhenInvalid?: boolean; }>, ): Checker; /** * wraps a given checker, making the valid value voidable * * By default, a value passed to voidable must match the checker spec exactly * when it is not undefined, or it will fail. * * passing the `undefinedWithWarningWhenInvalid` enables gracefully handling invalid * values that are less important -- if the provided checker is invalid, * the new checker will return undefined. * * For example: * * ```javascript * import {voidable, record, string} from 'refine'; * * const Options = record({ * // this must be a string, or Options is not valid * filename: string(), * * // this must be a string or undefined, * // or Options is not valid * displayName: voidable(string()), * * // if this field is not a string, * // it will be undefined and Options will pass the checker * description: voidable(string(), { * undefinedWithWarningWhenInvalid: true, * }) * }) * * const result = Options({filename: 'test', description: 1}); * * invariant(result.type === 'success'); * invariant(result.value.description === undefined); * invariant(result.warnings.length === 1); // there will be a warning * ``` */ export function voidable( checker: Checker, options?: Readonly<{ // if this is true, the checker will not fail // validation if an invalid value is provided, instead // returning undefined and including a warning as to the invalid type. undefinedWithWarningWhenInvalid?: boolean; }>, ): Checker; /** * a checker that provides a withDefault value if the provided value is nullable. * * For example: * ```jsx * const objPropertyWithDefault = record({ * foo: withDefault(number(), 123), * }); * ``` * Both `{}` and `{num: 123}` will refine to `{num: 123}` */ export function withDefault(checker: Checker, fallback: T): Checker; /** * wraps a checker with a logical constraint. * * Predicate function can return either a boolean result or * a tuple with a result and message * * For example: * * ```javascript * import {number, constraint} from 'refine'; * * const evenNumber = constraint( * number(), * n => n % 2 === 0 * ); * * const passes = evenNumber(2); * // passes.type === 'success'; * * const fails = evenNumber(1); * // fails.type === 'failure'; * ``` */ export function constraint( checker: Checker, predicate: (value: T) => boolean | [boolean, string], ): Checker; /** * wrapper to allow for passing a lazy checker value. This enables * recursive types by allowing for passing in the returned value of * another checker. For example: * * ```javascript * const user = record({ * id: number(), * name: string(), * friends: array(lazy(() => user)) * }); * ``` * * Example of array with arbitrary nesting depth: * ```jsx * const entry = or(number(), array(lazy(() => entry))); * const nestedArray = array(entry); * ``` */ export function lazy(getChecker: () => Checker): Checker; /** * helper to create a custom checker from a provided function. * If the function returns a non-nullable value, the checker succeeds. * * ```jsx * const myClassChecker = custom(x => x instanceof MyClass ? x : null); * ``` * * Nullable custom types can be created by composing with `nullable()` or * `voidable()` checkers: * * ```jsx * const maybeMyClassChecker = * nullable(custom(x => x instanceof MyClass ? x : null)); * ``` */ export function custom( checkValue: (value: unknown) => null | T, failureMessage?: string, ): Checker; export {}; ================================================ FILE: typescript/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "lib": ["es6", "dom"], "noImplicitAny": true, "noImplicitThis": true, "strictFunctionTypes": true, "strictNullChecks": true, "types": [], "noEmit": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { "recoil": ["./recoil.d.ts"], "@recoiljs/refine": ["./refine.d.ts"] } }, "files": [ "recoil.d.ts", "recoil-test.ts", "refine.d.ts", "refine-test.ts", "recoil-sync.d.ts", "recoil-sync-test.ts", "recoil-relay.d.ts" ] } ================================================ FILE: typescript/tslint.json ================================================ { "extends": "dtslint/dtslint.json" }