├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── SponsoredByMyTextAi.png ├── main.dart └── pubspec.yaml ├── lib ├── src │ ├── cache.dart │ ├── weak_container.dart │ └── weak_map.dart └── weak_map.dart ├── pubspec.yaml └── test ├── cache_test.dart ├── weak_container_test.dart └── weak_map_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | .idea_modules/ 18 | out/ 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | .idea/**/contentModel.xml 27 | .idea/**/workspace.xml 28 | .idea/**/tasks.xml 29 | .idea/**/usage.statistics.xml 30 | .idea/**/dictionaries 31 | .idea/**/shelf 32 | **/.idea/workspace.xml 33 | .idea/**/gradle.xml 34 | .idea/**/libraries/ 35 | .idea/**/modules.xml 36 | 37 | # The .vscode folder contains launch configuration and tasks you configure in 38 | # VS Code which you may wish to be included in version control, so this line 39 | # is commented out by default. 40 | #.vscode/ 41 | 42 | # Flutter/Dart/Pub related 43 | **/doc/api/ 44 | .dart_tool/ 45 | .flutter-plugins 46 | .flutter-plugins-dependencies 47 | .packages 48 | .pub-cache/ 49 | .pub/ 50 | build/ 51 | **/build/ 52 | pubspec.lock 53 | **/pubspec.lock 54 | 55 | # Android related 56 | **/android/**/gradle-wrapper.jar 57 | **/android/.gradle 58 | **/android/captures/ 59 | **/android/gradlew 60 | **/android/gradlew.bat 61 | **/android/local.properties 62 | **/android/**/GeneratedPluginRegistrant.java 63 | 64 | # iOS/XCode related 65 | **/ios/**/*.mode1v3 66 | **/ios/**/*.mode2v3 67 | **/ios/**/*.moved-aside 68 | **/ios/**/*.pbxuser 69 | **/ios/**/*.perspectivev3 70 | **/ios/**/*sync/ 71 | **/ios/**/.sconsign.dblite 72 | **/ios/**/.tags* 73 | **/ios/**/.vagrant/ 74 | **/ios/**/DerivedData/ 75 | **/ios/**/Icon? 76 | **/ios/**/Pods/ 77 | **/ios/**/.symlinks/ 78 | **/ios/**/profile 79 | **/ios/**/xcuserdata 80 | **/ios/.generated/ 81 | **/ios/Flutter/App.framework 82 | **/ios/Flutter/Flutter.framework 83 | **/ios/Flutter/Flutter.podspec 84 | **/ios/Flutter/Generated.xcconfig 85 | **/ios/Flutter/app.flx 86 | **/ios/Flutter/app.zip 87 | **/ios/Flutter/flutter_assets/ 88 | **/ios/Flutter/flutter_export_environment.sh 89 | **/ios/ServiceDefinitions.json 90 | **/ios/Runner/GeneratedPluginRegistrant.* 91 | 92 | # Exceptions to above rules. 93 | !**/ios/**/default.mode1v3 94 | !**/ios/**/default.mode2v3 95 | !**/ios/**/default.pbxuser 96 | !**/ios/**/default.perspectivev3 97 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 98 | 99 | # Arquivos de cache-serializado do Android studio 3.1+. 100 | .idea/caches/build_file_checksums.ser 101 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f7a6a7906be96d2288f5d63a5a54c515a6e987fe 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.0.1 2 | 3 | * Sponsored by [MyText.ai](https://mytext.ai) 4 | 5 | [![](./example/SponsoredByMyTextAi.png)](https://mytext.ai) 6 | 7 | ## 3.0.1 8 | 9 | * Flutter 3.10.0 and Dart 3.0.0 10 | 11 | ## 2.1.0 12 | 13 | * `cache2states_3params((state1, state2) => (param1, param2, param3) => ...);` 14 | 15 | ## 2.0.4 16 | 17 | * Docs improvement. 18 | 19 | ## 2.0.2 20 | 21 | * NNBD improvement. 22 | 23 | ## 2.0.0 24 | 25 | * Cache functions with extra parameters. 26 | * Rename of cache functions to make them easier to use. 27 | 28 | Now, cache functions are: 29 | 30 | ``` 31 | cache1state((state) => () => ...); 32 | cache1state_1param((state) => (parameter) => ...); 33 | cache1state_2params((state) => (param1, param2) => ...); 34 | cache2states((state1, state2) => () => ...); 35 | cache2states_1param((state1, state2) => (parameter) => ...); 36 | cache2states_2params((state1, state2) => (param1, param2) => ...); 37 | cache3states((state1, state2, state3) => () => ...); 38 | cache1state_0params_x((state1, extra) => () => ...); 39 | cache2states_0params_x((state1, state2, extra) => () => ...); 40 | cache3states_0params_x((state1, state2, state3, extra) => () => ...); 41 | ``` 42 | 43 | ## 1.4.0-nullsafety.0 44 | 45 | * Migrating to null safety 46 | 47 | ## 1.3.2 48 | 49 | * Type parameters rename (clean-code). 50 | 51 | ## 1.3.1 52 | 53 | * cache3_0. 54 | 55 | ## 1.2.2 56 | 57 | * Dependency bump. 58 | 59 | ## 1.2.1 60 | 61 | * Cache functions. 62 | 63 | ## 1.1.2 64 | 65 | * Add is by identity. 66 | * Docs improvement. 67 | 68 | ## 1.0.6 69 | 70 | * Removed dependency on Flutter (it's now pure Dart). 71 | 72 | ## 1.0.2 73 | 74 | * Example. 75 | 76 | ## 1.0.0 77 | 78 | * WeakMap and WeakContainer. 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 by Marcelo Glasberg 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted 4 | provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of 10 | conditions and the following disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 14 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 15 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 16 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 19 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 20 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Pub popularity](https://badgen.net/pub/popularity/weak_map)](https://pub.dev/packages/weak_map) 2 | [![Pub Version](https://img.shields.io/pub/v/weak_map?style=flat-square&logo=dart)](https://pub.dev/packages/weak_map) 3 | [![GitHub stars](https://img.shields.io/github/stars/marcglasberg/weak_map?style=social)](https://github.com/marcglasberg/weak_map) 4 | ![GitHub repo size](https://img.shields.io/github/repo-size/marcglasberg/weak_map?style=flat-square) 5 | ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) 6 | [![Developed by Marcelo Glasberg](https://img.shields.io/badge/Developed%20by%20Marcelo%20Glasberg-blue.svg)](https://glasberg.dev/) 7 | [![Glasberg.dev on pub.dev](https://img.shields.io/pub/publisher/weak_map.svg)](https://pub.dev/publishers/glasberg.dev/packages) 8 | [![Platforms](https://badgen.net/pub/flutter-platform/weak_map)](https://pub.dev/packages/weak_map) 9 | 10 | #### Sponsor 11 | 12 | [![](./example/SponsoredByMyTextAi.png)](https://mytext.ai) 13 | 14 | # weak_map 15 | 16 | This package contains the classes: 17 | 18 | * **WeakMap** 19 | * **WeakContainer** 20 | 21 | And also the functions: 22 | 23 | * **cache1state** 24 | * **cache1state_1param** 25 | * **cache1state_2params** 26 | * **cache2states** 27 | * **cache2states_1param** 28 | * **cache2states_2params** 29 | * **cache3states** 30 | * **cache1state_0params_x** 31 | * **cache2states_0params_x** 32 | * **cache3states_0params_x** 33 | 34 | ### Why is this package useful? 35 | 36 | 1. Dart doesn't allow for real 37 | weak-references, but this package allows you to go as close as possible 38 | (internally it uses the 39 | Expando class). The Dart engine stores a value in memory while it is reachable (and can 40 | potentially be used). Usually, keys in a map are considered reachable and kept in memory while 41 | the map itself is in memory. This means if we put an object into a map or into a variable, then 42 | while the map is alive, the object will be alive as well, even if there are no other references 43 | to it. It occupies memory and may not be garbage collected. WeakMap and WeakContainer are 44 | fundamentally different in this aspect. They don't prevent garbage-collection of key objects. 45 | 46 | 2. Caches that keep the result of expensive processes calculated over immutable data can also 47 | benefit from weak-maps. I here provide functions similar to the ones of the 48 | reselect package, but better. This can be used 49 | with Redux or with any other calculations over immutable data. 50 | 51 |
52 | 53 | ## WeakMap 54 | 55 | A WeakMap lets you garbage-collect its keys. Please note the **keys** can be garbage-collected, not 56 | their corresponding values. 57 | 58 | This means if you use some object as a key to a map-entry, this alone will not prevent Dart to 59 | garbage-collect this object. 60 | 61 | In other words, after all other references to that object have been destroyed, its entry 62 | (key and value) may be removed automatically from the map at any moment. To create a map: 63 | 64 | ``` 65 | var map = WeakMap(); 66 | ``` 67 | 68 | To add and retrieve a value: 69 | 70 | ``` 71 | map["John"] = 42; 72 | var age = map["John"]; 73 | ``` 74 | 75 | The following map methods work as expected: 76 | 77 | ``` 78 | map.remove("John") 79 | map.clear() 80 | map.contains("John")) 81 | ``` 82 | 83 | However, adding some `null` value to the map is the same as removing the key: 84 | 85 | ``` 86 | map["John"] = null; // Same as map.remove("John") 87 | ``` 88 | 89 | **Notes:** 90 | 91 | 1. The keys are compared using object **identity**, and not object equivalence (operator `==`). 92 | 93 | 2. If you use null, a number, a boolean, a String, or a const type as the map key, it will act like 94 | a regular map, because these types are never garbage-collected. All other types of object may be 95 | garbage-collected. 96 | 97 | 3. To retrieve a value added to the map, you can use the equivalent syntaxes `var y = map[x]` 98 | or `var y = map.get(x)`. 99 | 100 | 4. Doing `map[x] = y` is equivalent to `map.add(key: x, value: y)`, but the object is later 101 | retrieved by **identity**. 102 | 103 |
104 | 105 | ## WeakContainer 106 | 107 | As previously explained, Dart doesn't have real weak-references. But you can check that some object 108 | is the same you had before. 109 | 110 | To create a weak-container: 111 | 112 | ``` 113 | var obj = Object(); 114 | var ref = WeakContainer(obj); 115 | var someObj = Random().nextBool() ? obj : Object(); 116 | print(ref.contains(someObj)); // True or false. 117 | ``` 118 | 119 | This will print `true` if `someObj` is the same as the original `obj`, and will print `false` if 120 | it's a different object, compared by identity. If all references to the original `obj` have been 121 | destroyed, the weak-container will **not** prevent `obj` to be garbage-collected. 122 | 123 |
124 | 125 | ### Why doesn't Dart allow for real weak-references, anyway? 126 | 127 | Because the creators of Dart don't want the GC (garbage-collector) to be "visible". 128 | 129 | Expandos are not equivalent to weak-references (meaning the Java `WeakReference` behavior). A weak 130 | reference is one that doesn't keep the referenced object alive, so the weak reference value may 131 | change to `null` at any time in the program. This makes the GC visible in the program. 132 | 133 | Expandos are maps (from key to value) which won't keep the key alive. There is no way to distinguish 134 | an Expando that garbage collects the entry when the key dies, and one that doesn't, because you 135 | don't have the key to do the lookup anymore. 136 | 137 | Basically, it means that an expando keeps a value alive as long as you have a reference to both the 138 | expando and the key, and after that, you can't check if the entry is there or not. With expandos, 139 | the GC need not be part of the language specification. It's just an optimization that 140 | implementations (are expected to) do to release memory that isn't needed anymore. Disabling the GC 141 | will not change the behavior of programs unless they run out of memory. 142 | 143 |
144 | 145 | ## Cache 146 | 147 | Suppose you have some **immutable** information, which we call "state", and some parameters. We want 148 | to perform some expensive process (calculation, selection filtering etc) over the state, and we want 149 | to cache the result. 150 | 151 | For example, suppose you want to filter an **immutable list of millions of users**, to create a new 152 | list with only the names that start with some text. You could filter the users list to remove all 153 | other names, like this: 154 | 155 | ``` 156 | List filter(String text) => users.where((user)=>user.name.startsWith(text)).toList(); 157 | ``` 158 | 159 | This is an expensive process, so you may want to cache the filtered list. 160 | 161 | In this example, we have a single state and a single parameter, so we're going to use 162 | the `cache1state_1param` method: 163 | 164 | ``` 165 | static List filter(Users users, String text) 166 | => _filter(users)(text); 167 | 168 | static final _filter = cache1state_1param( 169 | (Users users) 170 | => (String text) 171 | => users.where((user)=>user.name.startsWith(text)).toList()); 172 | ``` 173 | 174 | The above code will calculate the filtered list only once, and then return it when the `filter` 175 | function is called again with the same `users` and `text`. 176 | 177 | If the function is called with a **different** `users` and/or `text`, it will recalculate and cache 178 | the new result. 179 | 180 | However, it treats the state and the parameter differently. If you call the function while keeping 181 | the **same state** and changing only the parameter, it will cache all the results, one for each 182 | parameter. 183 | 184 | However, as soon as you call the function with a **changed state**, it will delete all of its 185 | previous cached information, since it understands that they are no longer useful. 186 | 187 | And even if you don't call that function ever again, it will delete the cached information if it 188 | detects that the state is no longer used in other parts of the program. In other words, it keeps the 189 | cached information in a weak-map, so that the cache will not hold to old information and have a 190 | negative impact in memory usage. 191 | 192 | Some functions, marked with an "x", also let you pass some extra information which is not used in 193 | any way to decide whether the cache should be used/recalculated/evicted. 194 | 195 | For the moment, the following 10 methods are provided, which combine 1, 2 or 3 states with 0, 1 or 2 196 | parameters, and possibly some extra information: 197 | 198 | ``` 199 | cache1state((state) => () => ...); 200 | cache1state_1param((state) => (parameter) => ...); 201 | cache1state_2params((state) => (parameter1, parameter2) => ...); 202 | cache2states((state1, state2) => () => ...); 203 | cache2states_1param((state1, state2) => (parameter) => ...); 204 | cache2states_2params((state1, state2) => (parameter1, parameter2) => ...); 205 | cache3states((state1, state2, state3) => () => ...); 206 | cache1state_0params_x((state1, extra) => () => ...); 207 | cache2states_0params_x((state1, state2, extra) => () => ...); 208 | cache3states_0params_x((state1, state2, state3, extra) => () => ...); 209 | ``` 210 | 211 | I have created only those above, because for my own usage I never required more than that. Please, 212 | open an issue 213 | to ask for more variations in case you feel the need. 214 | 215 | **Note:** These cache functions are similar to the "createSelector" functions found in the 216 | reselect package. The differences are: First, here 217 | you can keep any number of cached results for each function, one for each time the function is 218 | called with the same state and different parameters. Meanwhile, the reselect package only keeps a 219 | single cached result per function. Second, here it discards the cached information when the state 220 | changes or is no longer used in other parts of the program. Meanwhile, the reselect package will 221 | always keep the states and cached results in memory. 222 | 223 |
224 | 225 | *** 226 | 227 | ## By Marcelo Glasberg 228 | 229 | _glasberg.dev_ 230 |
231 | _github.com/marcglasberg_ 232 |
233 | _linkedin.com/in/marcglasberg/_ 234 |
235 | _twitter.com/glasbergmarcelo_ 236 |
237 | 238 | _stackoverflow.com/users/3411681/marcg_ 239 |
240 | _medium.com/@marcglasberg_ 241 |
242 | 243 | *My article in the official Flutter documentation*: 244 | 245 | * Understanding 246 | constraints 247 | 248 | *The Flutter packages I've authored:* 249 | 250 | * async_redux 251 | * provider_for_redux 252 | * i18n_extension 253 | * align_positioned 254 | * network_to_file_image 255 | * image_pixels 256 | * matrix4_transform 257 | * back_button_interceptor 258 | * indexed_list_view 259 | * animated_size_and_fade 260 | * assorted_layout_widgets 261 | * weak_map 262 | * themed 263 | * bdd_framework 264 | * 265 | tiktoken_tokenizer_gpt4o_o1 266 | 267 | *My Medium Articles:* 268 | 269 | * 270 | Async Redux: Flutter’s non-boilerplate version of Redux 271 | (versions: 272 | Português) 273 | * 274 | i18n_extension 275 | (versions: 276 | Português) 277 | * 278 | Flutter: The Advanced Layout Rule Even Beginners Must Know 279 | (versions: русский) 280 | * 281 | The New Way to create Themes in your Flutter App 282 | 283 | [![](./example/SponsoredByMyTextAi.png)](https://mytext.ai) 284 | -------------------------------------------------------------------------------- /example/SponsoredByMyTextAi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcglasberg/weak_map/7acc88d56b52cffb41cdc170850e216160d2b211/example/SponsoredByMyTextAi.png -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:weak_map/weak_map.dart'; 2 | 3 | void main() { 4 | var map = WeakMap(); 5 | 6 | // 1) 7 | print('\nmap["A"] = 1'); 8 | map["A"] = 1; 9 | print('A = ${map["A"]}'); // A = 1 10 | print('A exists in map = ${map.contains("A")}'); // A = true 11 | 12 | // 2) 13 | print('\nDoes not set B'); 14 | print('B = ${map["B"]}'); // B = null 15 | print('B exists in map = ${map.contains("B")}'); // A = false 16 | 17 | // 3) 18 | print('\nmap["A"] = null'); 19 | map["A"] = null; 20 | print('A = ${map["A"]}'); // A = null 21 | // Adding some null value to the map is the same as removing the key. 22 | print('A exists in map = ${map.contains("A")}'); // A = false 23 | 24 | // 4) 25 | print('\nvalue = WeakContainer("X")'); 26 | var value = WeakContainer("X"); 27 | print('Contains X = ${value.contains("X")}'); // Contains X = true 28 | print('Contains Y = ${value.contains("Y")}'); // Contains Y = false 29 | } 30 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | publish_to: "none" 3 | version: 1.0.0 4 | 5 | environment: 6 | sdk: '>=2.19.0 <4.0.0' 7 | 8 | dependencies: 9 | weak_map: 10 | path: ../ 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/src/cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:weak_map/weak_map.dart'; 4 | 5 | typedef R1_0 = Result Function(); 6 | typedef F1_0 = R1_0 Function(State1); 7 | 8 | /// Cache for 1 immutable state, and no parameters. 9 | F1_0 cache1state(F1_0 f) { 10 | WeakContainer? _s1; 11 | late WeakMap weakMap; 12 | 13 | return (State1 s1) { 14 | return () { 15 | if (_s1 == null || !_s1!.contains(s1)) { 16 | weakMap = WeakMap(); 17 | _s1 = WeakContainer(s1); 18 | var result = f(s1)(); 19 | weakMap[s1] = result; 20 | return result; 21 | } 22 | // 23 | else { 24 | return weakMap[s1] as Result; 25 | } 26 | }; 27 | }; 28 | } 29 | 30 | typedef R1_1 = Result Function(Param1); 31 | typedef F1_1 = R1_1 Function(State1); 32 | 33 | /// Cache for 1 immutable state, and 1 parameter. 34 | F1_1 cache1state_1param( 35 | F1_1 f, 36 | ) { 37 | WeakContainer? _s1; 38 | late WeakMap> weakMap; 39 | 40 | return (State1 s1) { 41 | return (Param1 p1) { 42 | if (_s1 == null || !_s1!.contains(s1)) { 43 | weakMap = WeakMap(); 44 | Map map = HashMap(); 45 | weakMap[s1] = map; 46 | _s1 = WeakContainer(s1); 47 | var result = f(s1)(p1); 48 | map[p1] = result; 49 | return result; 50 | } 51 | // 52 | else { 53 | Map map = weakMap[s1]!; 54 | if (!map.containsKey(p1)) { 55 | var result = f(s1)(p1); 56 | map[p1] = result; 57 | return result; 58 | } 59 | // 60 | else 61 | return map[p1] as Result; 62 | } 63 | }; 64 | }; 65 | } 66 | 67 | typedef R1_2 = // 68 | Result Function(Param1, Param2); 69 | 70 | typedef F1_2 = // 71 | R1_2 Function(State1); 72 | 73 | /// Cache for 1 immutable state, and 2 parameters. 74 | F1_2 cache1state_2params( 75 | F1_2 f) { 76 | WeakContainer? _s1; 77 | late WeakMap, Result>> weakMap; 78 | 79 | return (State1 s1) { 80 | return (Param1 p1, Param2 p2) { 81 | var parP = _Pair(p1, p2); 82 | if (_s1 == null || !_s1!.contains(s1)) { 83 | weakMap = WeakMap(); 84 | Map<_Pair, Result> map = HashMap(); 85 | weakMap[s1] = map; 86 | _s1 = WeakContainer(s1); 87 | var result = f(s1)(p1, p2); 88 | map[parP] = result; 89 | return result; 90 | } 91 | // 92 | else { 93 | Map<_Pair, Result> map = weakMap[s1]!; 94 | if (!map.containsKey(parP)) { 95 | var result = f(s1)(p1, p2); 96 | map[parP] = result; 97 | return result; 98 | } 99 | return map[parP] as Result; 100 | } 101 | }; 102 | }; 103 | } 104 | 105 | typedef R2_0 = Result Function(); 106 | typedef F2_0 = R2_0 Function(State1, State2); 107 | 108 | /// Cache for 2 immutable states, and no parameters. 109 | F2_0 cache2states( 110 | F2_0 f, 111 | ) { 112 | WeakContainer? _s1, _s2; 113 | late WeakMap> weakMap1; 114 | WeakMap weakMap2; 115 | 116 | return (State1 s1, State2 s2) { 117 | return () { 118 | if (_s1 == null || 119 | _s2 == null || // 120 | !_s1!.contains(s1) || 121 | !_s2!.contains(s2)) { 122 | _s1 = WeakContainer(s1); 123 | _s2 = WeakContainer(s2); 124 | 125 | var result = f(s1, s2)(); 126 | weakMap1 = WeakMap(); 127 | weakMap2 = WeakMap(); 128 | weakMap2[s2] = result; 129 | weakMap1[s1] = weakMap2; 130 | return result; 131 | } 132 | // 133 | else { 134 | return weakMap1[s1]![s2] as Result; 135 | } 136 | }; 137 | }; 138 | } 139 | 140 | typedef R2_1 = Result Function(Param1); 141 | 142 | typedef F2_1 = R2_1 Function( 143 | State1, 144 | State2, 145 | ); 146 | 147 | /// Cache for 2 immutable states, and 1 parameter. 148 | F2_1 cache2states_1param( 149 | F2_1 f) { 150 | WeakContainer? _s1, _s2; 151 | late WeakMap>> weakMap1; 152 | WeakMap> weakMap2; 153 | 154 | return (State1 s1, State2 s2) { 155 | return (Param1 p1) { 156 | if (_s1 == null || 157 | _s2 == null || // 158 | !_s1!.contains(s1) || 159 | !_s2!.contains(s2)) { 160 | _s1 = WeakContainer(s1); 161 | _s2 = WeakContainer(s2); 162 | 163 | var result = f(s1, s2)(p1); 164 | weakMap1 = WeakMap(); 165 | weakMap2 = WeakMap(); 166 | Map map = HashMap(); 167 | map[p1] = result; 168 | weakMap2[s2] = map; 169 | weakMap1[s1] = weakMap2; 170 | return result; 171 | } 172 | // 173 | else { 174 | Map map = weakMap1[s1]![s2]!; 175 | if (!map.containsKey(p1)) { 176 | var result = f(s1, s2)(p1); 177 | map[p1] = result; 178 | return result; 179 | } 180 | 181 | return map[p1] as Result; 182 | } 183 | }; 184 | }; 185 | } 186 | 187 | typedef R2_2 = // 188 | Result Function(Param1, Param2); 189 | 190 | typedef F2_2 = // 191 | R2_2 Function(State1, State2); 192 | 193 | /// Cache for 2 immutable states, and 2 parameters. 194 | F2_2 // 195 | cache2states_2params( 196 | F2_2 f) { 197 | WeakContainer? _s1, _s2; 198 | late WeakMap, Result>>> weakMap1; 199 | WeakMap, Result>> weakMap2; 200 | 201 | return (State1 s1, State2 s2) { 202 | return (Param1 p1, Param2 p2) { 203 | var pair = _Pair(p1, p2); 204 | if (_s1 == null || !_s1!.contains(s1) || !_s2!.contains(s2)) { 205 | _s1 = WeakContainer(s1); 206 | _s2 = WeakContainer(s2); 207 | 208 | var result = f(s1, s2)(p1, p2); 209 | weakMap1 = WeakMap(); 210 | weakMap2 = WeakMap(); 211 | Map<_Pair, Result> map = HashMap(); 212 | map[pair] = result; 213 | weakMap2[s2] = map; 214 | weakMap1[s1] = weakMap2; 215 | return result; 216 | } 217 | // 218 | else { 219 | Map<_Pair, Result> map = weakMap1[s1]![s2]!; 220 | if (!map.containsKey(pair)) { 221 | var result = f(s1, s2)(p1, p2); 222 | map[pair] = result; 223 | return result; 224 | } 225 | 226 | return map[pair] as Result; 227 | } 228 | }; 229 | }; 230 | } 231 | 232 | typedef R2_3 = // 233 | Result Function(Param1, Param2, Param3); 234 | 235 | typedef F2_3 = // 236 | R2_3 Function(State1, State2); 237 | 238 | /// Cache for 2 immutable states, and 3 parameters. 239 | F2_3 // 240 | cache2states_3params( 241 | F2_3 f) { 242 | WeakContainer? _s1, _s2; 243 | late WeakMap, Result>>> weakMap1; 244 | WeakMap, Result>> weakMap2; 245 | 246 | return (State1 s1, State2 s2) { 247 | return (Param1 p1, Param2 p2, Param3 p3) { 248 | var triad = _Triad(p1, p2, p3); 249 | if (_s1 == null || !_s1!.contains(s1) || !_s2!.contains(s2)) { 250 | _s1 = WeakContainer(s1); 251 | _s2 = WeakContainer(s2); 252 | 253 | var result = f(s1, s2)(p1, p2, p3); 254 | weakMap1 = WeakMap(); 255 | weakMap2 = WeakMap(); 256 | Map<_Triad, Result> map = HashMap(); 257 | map[triad] = result; 258 | weakMap2[s2] = map; 259 | weakMap1[s1] = weakMap2; 260 | return result; 261 | } 262 | // 263 | else { 264 | Map<_Triad, Result> map = weakMap1[s1]![s2]!; 265 | if (!map.containsKey(triad)) { 266 | var result = f(s1, s2)(p1, p2, p3); 267 | map[triad] = result; 268 | return result; 269 | } 270 | 271 | return map[triad] as Result; 272 | } 273 | }; 274 | }; 275 | } 276 | 277 | typedef R3_0 = // 278 | Result Function(); 279 | 280 | typedef F3_0 = // 281 | R3_0 Function(State1, State2, State3); 282 | 283 | /// Cache for 3 immutable states, and no parameters. 284 | F3_0 cache3states( 285 | F3_0 f) { 286 | WeakContainer? _s1, _s2, _s3; 287 | late WeakMap>> weakMap1; 288 | WeakMap> weakMap2; 289 | WeakMap weakMap3; 290 | 291 | return (State1 s1, State2 s2, State3 s3) { 292 | return () { 293 | if (_s1 == null || 294 | _s2 == null || 295 | _s3 == null || // 296 | !_s1!.contains(s1) || 297 | !_s2!.contains(s2) || 298 | !_s3!.contains(s3)) { 299 | _s1 = WeakContainer(s1); 300 | _s2 = WeakContainer(s2); 301 | _s3 = WeakContainer(s3); 302 | 303 | var result = f(s1, s2, s3)(); 304 | weakMap1 = WeakMap(); 305 | weakMap2 = WeakMap(); 306 | weakMap3 = WeakMap(); 307 | weakMap3[s3] = result; 308 | weakMap2[s2] = weakMap3; 309 | weakMap1[s1] = weakMap2; 310 | return result; 311 | } 312 | // 313 | else { 314 | return weakMap1[s1]![s2]![s3] as Result; 315 | } 316 | }; 317 | }; 318 | } 319 | 320 | typedef F1_0_x = R1_0 Function(State1, Extra); 321 | 322 | /// Cache for 1 immutable state, no parameters, and some extra information. 323 | /// Note: The extra information is not used in any way to decide whether 324 | /// the cache should be used/recalculated/evicted. 325 | F1_0_x cache1state_0params_x( 326 | F1_0_x f, 327 | ) { 328 | WeakContainer? _s1; 329 | late WeakMap weakMap; 330 | 331 | return (State1 state1, Extra extra) { 332 | return () { 333 | if (_s1 == null || !_s1!.contains(state1)) { 334 | weakMap = WeakMap(); 335 | _s1 = WeakContainer(state1); 336 | var result = f(state1, extra)(); 337 | weakMap[state1] = result; 338 | return result; 339 | } 340 | // 341 | else { 342 | return weakMap[state1] as Result; 343 | } 344 | }; 345 | }; 346 | } 347 | 348 | typedef F2_0_x = R2_0 Function(State1, State2, Extra); 349 | 350 | /// Cache for 2 immutable states, no parameters, and some extra information. 351 | /// Note: The extra information is not used in any way to decide whether 352 | /// the cache should be used/recalculated/evicted. 353 | F2_0_x cache2states_0params_x( 354 | F2_0_x f, 355 | ) { 356 | WeakContainer? _s1, _s2; 357 | late WeakMap> weakMap1; 358 | late WeakMap weakMap2; 359 | 360 | return (State1 state1, State2 state2, Extra extra) { 361 | return () { 362 | if (_s1 == null || 363 | _s2 == null || // 364 | !_s1!.contains(state1) || 365 | !_s2!.contains(state2)) { 366 | _s1 = WeakContainer(state1); 367 | _s2 = WeakContainer(state2); 368 | 369 | var result = f(state1, state2, extra)(); 370 | weakMap1 = WeakMap(); 371 | weakMap2 = WeakMap(); 372 | weakMap2[state2] = result; 373 | weakMap1[state1] = weakMap2; 374 | return result; 375 | } 376 | // 377 | else { 378 | return weakMap1[state1]![state2] as Result; 379 | } 380 | }; 381 | }; 382 | } 383 | 384 | typedef F3_0_x = R3_0 Function( 385 | State1, State2, State3, Extra); 386 | 387 | /// Cache for 3 immutable states, no parameters, and some extra information. 388 | /// Note: The extra information is not used in any way to decide whether 389 | /// the cache should be used/recalculated/evicted. 390 | F3_0_x 391 | cache3states_0params_x( 392 | F3_0_x f, 393 | ) { 394 | WeakContainer? _s1, _s2, _s3; 395 | late WeakMap>> weakMap1; 396 | late WeakMap> weakMap2; 397 | late WeakMap weakMap3; 398 | 399 | return (State1 state1, State2 state2, State3 state3, Extra extra) { 400 | return () { 401 | if (_s1 == null || 402 | _s2 == null || 403 | _s3 == null || // 404 | !_s1!.contains(state1) || 405 | !_s2!.contains(state2) || 406 | !_s3!.contains(state3)) { 407 | _s1 = WeakContainer(state1); 408 | _s2 = WeakContainer(state2); 409 | _s3 = WeakContainer(state3); 410 | 411 | var result = f(state1, state2, state3, extra)(); 412 | weakMap1 = WeakMap(); 413 | weakMap2 = WeakMap(); 414 | weakMap3 = WeakMap(); 415 | weakMap3[state3] = result; 416 | weakMap2[state2] = weakMap3; 417 | weakMap1[state1] = weakMap2; 418 | return result; 419 | } 420 | // 421 | else { 422 | return weakMap1[state1]![state2]![state3] as Result; 423 | } 424 | }; 425 | }; 426 | } 427 | 428 | class _Pair { 429 | final X x; 430 | final Y y; 431 | 432 | _Pair(this.x, this.y); 433 | 434 | @override 435 | bool operator ==(Object other) => 436 | identical(this, other) || 437 | other is _Pair && 438 | runtimeType == other.runtimeType // 439 | && 440 | x == other.x && 441 | y == other.y; 442 | 443 | @override 444 | int get hashCode => Object.hash(x, y); 445 | } 446 | 447 | class _Triad { 448 | final X x; 449 | final Y y; 450 | final Z z; 451 | 452 | _Triad(this.x, this.y, this.z); 453 | 454 | @override 455 | bool operator ==(Object other) => 456 | identical(this, other) || 457 | other is _Triad && 458 | runtimeType == other.runtimeType // 459 | && 460 | x == other.x && 461 | y == other.y && 462 | z == other.z; 463 | 464 | @override 465 | int get hashCode => Object.hash(x, y, z); 466 | } 467 | -------------------------------------------------------------------------------- /lib/src/weak_container.dart: -------------------------------------------------------------------------------- 1 | /// A weak-container is similar, but not exactly, a weak-reference to some object. 2 | /// Dart doesn't have real weak-references, so the best you can do here is to check 3 | /// that some object is the same you had before. 4 | /// 5 | /// To create a weak-container: 6 | /// ``` 7 | /// var obj = Object(); 8 | /// var ref = WeakContainer(obj); 9 | /// var someObj = Random().nextBool() ? obj : Object(); 10 | /// print(ref.contains(someObj)); // True or false. 11 | /// ``` 12 | /// This will print `true` if `someObj` is the same as the original `obj`, 13 | /// and will print `false` if it's a different object, compared by identity. 14 | /// 15 | /// If all references to the original `obj` have been destroyed, 16 | /// the weak-container will NOT prevent `obj` to be garbage-collected. 17 | /// 18 | class WeakContainer { 19 | Expando? _expando; 20 | Object? _value; 21 | bool _isNull; 22 | 23 | WeakContainer(Object? value) 24 | : _expando = _allowedInExpando(value) ? _createExpando(value!) : null, 25 | _value = _allowedInExpando(value) ? null : value, 26 | _isNull = (value == null); 27 | 28 | bool contains(Object? value) { 29 | if (value == null) { 30 | return _isNull; 31 | } else { 32 | if (_value == value) { 33 | return true; 34 | } else { 35 | return (_expando != null && // 36 | _allowedInExpando(value) && 37 | _expando![value] == true); 38 | } 39 | } 40 | } 41 | 42 | void clear() { 43 | _value = null; 44 | _expando = null; 45 | _isNull = false; 46 | } 47 | 48 | static Expando _createExpando(Object value) { 49 | assert(_allowedInExpando(value)); 50 | var expando = Expando(); 51 | expando[value] = true; 52 | return expando; 53 | } 54 | 55 | static bool _allowedInExpando(Object? value) => 56 | value is! String && value is! num && value is! bool && value != null; 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/weak_map.dart: -------------------------------------------------------------------------------- 1 | /// A WeakMap lets you garbage-collect its keys. 2 | /// Please note: The **[key]** can be garbage-collected, not the [value]. 3 | /// 4 | /// This means if you use some object as a key to a map-entry, this alone 5 | /// will not prevent Dart to garbage-collect this object. In other words, 6 | /// after all other references to that object have been destroyed, its entry 7 | /// (key and value) may be removed automatically from the map at any moment. 8 | /// 9 | /// To create a map: 10 | /// ``` 11 | /// var map = WeakMap(); 12 | /// ``` 13 | /// 14 | /// To add and retrieve a value: 15 | /// ``` 16 | /// map["John"] = 42; 17 | /// var age = map["John"]; 18 | /// ``` 19 | /// 20 | /// The following map methods work as expected: 21 | /// ``` 22 | /// map.remove("John") 23 | /// map.clear() 24 | /// map.contains("John")) 25 | /// ``` 26 | /// 27 | /// However, adding some null value to the map is the same as removing the key: 28 | /// ``` 29 | /// map["John"] = null; // Same as map.remove("John") 30 | /// ``` 31 | /// 32 | /// Notes: 33 | /// 34 | /// 1. If you use null, a number, a boolean, a String, or a const type as the 35 | /// map key, it will act like a regular map, because these types are never 36 | /// garbage-collected. All other types of object may be garbage-collected. 37 | /// 38 | /// 2. To retrieve a value added to the map, you can use the equivalent 39 | /// syntax `var y = map[x]` or `var y = map.get(x)`. 40 | /// 41 | class WeakMap { 42 | final Map _map; 43 | Expando _expando; 44 | 45 | WeakMap() 46 | : _map = {}, 47 | _expando = Expando(); 48 | 49 | static bool _allowedInExpando(Object? value) => 50 | value is! String && value is! num && value is! bool && value != null; 51 | 52 | void operator []=(K key, V value) => add(key: key, value: value); 53 | 54 | /// Returns the value associated with this key, or null if the key doesn't exist in the map. 55 | /// Note this can't differentiate between the key not existing and the value being null. 56 | /// 57 | /// This is the same as using the [get] method. 58 | /// 59 | V? operator [](K key) => get(key); 60 | 61 | void add({required K key, required V value}) { 62 | if (_allowedInExpando(key)) { 63 | _expando[key!] = value; 64 | } else { 65 | _map[key] = value; 66 | } 67 | } 68 | 69 | bool contains(K key) => get(key) != null; 70 | 71 | /// Returns the value associated with this key, or null if the key doesn't exist in the map. 72 | /// Note this can't differentiate between the key not existing and the value being null. 73 | /// 74 | /// This is the same as using the [] operator. 75 | /// 76 | V? get(K key) => _map.containsKey(key) 77 | ? // 78 | _map[key] 79 | : (_allowedInExpando(key) ? _expando[key!] as V : null); 80 | 81 | /// Returns the value associated with this key. 82 | /// It will throw if the key doesn't exist in the map. 83 | /// Note this may only return null if V is nullable. 84 | /// 85 | /// This is the same as using the [] operator. 86 | /// 87 | V getOrThrow(K key) { 88 | if (_map.containsKey(key)) 89 | return _map[key] as V; 90 | else { 91 | if (_allowedInExpando(key)) 92 | return _expando[key!] as V; 93 | else 94 | throw StateError("No value for key."); 95 | } 96 | } 97 | 98 | void remove(K key) { 99 | _map.remove(key); 100 | 101 | if (_allowedInExpando(key)) { 102 | _expando[key!] = null; 103 | } 104 | } 105 | 106 | void clear() { 107 | _map.clear(); 108 | _expando = Expando(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/weak_map.dart: -------------------------------------------------------------------------------- 1 | library weak_map; 2 | 3 | export "src/weak_map.dart"; 4 | export "src/weak_container.dart"; 5 | export "src/cache.dart"; 6 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: weak_map 2 | description: WeakMap is a map where the keys are weakly referenced. WeakContainer lets you check if an object is the same you had before. Cache functions for memoization with weak-references. 3 | version: 4.0.1 4 | # author: Marcelo Glasberg 5 | homepage: https://github.com/marcglasberg/weak_map 6 | topics: 7 | - memory 8 | - memory-management 9 | - cache 10 | - collections 11 | - data-structures 12 | 13 | environment: 14 | sdk: '>=2.19.0 <4.0.0' 15 | 16 | dev_dependencies: 17 | test: ^1.17.12 18 | -------------------------------------------------------------------------------- /test/cache_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:weak_map/weak_map.dart'; 3 | 4 | void main() { 5 | var stateNames = List.unmodifiable(["Juan", "Anna", "Bill", "Zack", "Arnold", "Amanda"]); 6 | 7 | test('Test 1 state with 0 parameters.', () { 8 | // 9 | 10 | // This is some function we want to cache, for some specific `limit`. 11 | List func(int? limit) { 12 | return (limit == null) ? [] : stateNames.take(limit).toList(); 13 | } 14 | 15 | // This is the cache. 16 | var _funcCached = cache1state, int?>((int? limit) => () => func(limit)); 17 | 18 | // And this is the cached func. 19 | List funcCached(int? limit) => _funcCached(limit)(); 20 | 21 | List memoA1 = funcCached(1); 22 | List memoA2 = funcCached(1); 23 | expect(memoA1, ["Juan"]); 24 | expect(memoA2, ["Juan"]); 25 | expect(identical(memoA1, memoA2), isTrue); 26 | 27 | var memoB1 = _funcCached(2)(); 28 | var memoB2 = _funcCached(2)(); 29 | expect(memoB1, ["Juan", "Anna"]); 30 | expect(memoB2, ["Juan", "Anna"]); 31 | expect(identical(memoB1, memoB2), isTrue); 32 | 33 | var memoC1 = _funcCached(null)(); 34 | var memoC2 = _funcCached(null)(); 35 | expect(memoC1, []); 36 | expect(memoC2, []); 37 | expect(identical(memoC1, memoC2), isTrue); 38 | }); 39 | 40 | test('Test 1 state with 0 parameters, caching null parameter.', () { 41 | // 42 | 43 | // This is some function we want to cache, for some specific `limit`. 44 | List? func(int? limit) { 45 | return (limit == null) ? null : stateNames.take(limit).toList(); 46 | } 47 | 48 | // This is the cache. 49 | var _funcCached = cache1state?, int?>((int? limit) => () => func(limit)); 50 | 51 | // And this is the cached func. 52 | List? funcCached(int? limit) => _funcCached(limit)(); 53 | 54 | List? memoA1 = funcCached(1); 55 | List? memoA2 = funcCached(1); 56 | expect(memoA1, ["Juan"]); 57 | expect(memoA2, ["Juan"]); 58 | expect(identical(memoA1, memoA2), isTrue); 59 | 60 | var memoB1 = _funcCached(2)(); 61 | var memoB2 = _funcCached(2)(); 62 | expect(memoB1, ["Juan", "Anna"]); 63 | expect(memoB2, ["Juan", "Anna"]); 64 | expect(identical(memoB1, memoB2), isTrue); 65 | 66 | var memoC1 = _funcCached(null)(); 67 | var memoC2 = _funcCached(null)(); 68 | expect(memoC1, isNull); 69 | expect(memoC2, isNull); 70 | expect(identical(memoC1, memoC2), isTrue); 71 | }); 72 | 73 | test('Test results are forgotten when the state changes (1 state with 0 parameters).', () { 74 | // 75 | var selector = cache1state((int? limit) => () { 76 | return limit == null ? [] : stateNames.take(limit).toList(); 77 | }); 78 | 79 | var memoA1 = selector(1)(); 80 | var memoA2 = selector(1)(); 81 | expect(memoA1, ["Juan"]); 82 | expect(identical(memoA1, memoA2), isTrue); 83 | 84 | // Another state with another parameter. 85 | selector(2)(); 86 | 87 | // Try reading the previous state, with the same parameter as before. 88 | var memoA5 = selector(1)(); 89 | expect(memoA5, ["Juan"]); 90 | expect(identical(memoA5, memoA1), isFalse); 91 | }); 92 | 93 | test('Also works when changing to null (1 state with 0 parameters).', () { 94 | // 95 | var selector = cache1state((int? limit) => () { 96 | return limit == null ? [] : stateNames.take(limit).toList(); 97 | }); 98 | 99 | var memoA1 = selector(1)(); 100 | var memoA2 = selector(1)(); 101 | expect(memoA1, ["Juan"]); 102 | expect(identical(memoA1, memoA2), isTrue); 103 | 104 | // Another state with another parameter. 105 | selector(null)(); 106 | 107 | // Try reading the previous state, with the same parameter as before. 108 | var memoA5 = selector(1)(); 109 | expect(memoA5, ["Juan"]); 110 | expect(identical(memoA5, memoA1), isFalse); 111 | }); 112 | 113 | test('Also works when changing from null (1 state with 0 parameters).', () { 114 | // 115 | var selector = cache1state((int? limit) => () { 116 | return limit == null ? [] : stateNames.take(limit).toList(); 117 | }); 118 | 119 | var memoA1 = selector(null)(); 120 | var memoA2 = selector(null)(); 121 | expect(memoA1, []); 122 | expect(identical(memoA1, memoA2), isTrue); 123 | 124 | // Another state with another parameter. 125 | selector(1)(); 126 | 127 | // Try reading the previous state, with the same parameter as before. 128 | var memoA5 = selector(null)(); 129 | expect(memoA5, []); 130 | expect(identical(memoA5, memoA1), isFalse); 131 | }); 132 | 133 | test('Test 1 state with 1 parameter.', () { 134 | // 135 | var selector = cache1state_1param((List state) => (String startString) { 136 | return state.where((str) => str.startsWith(startString)).toList(); 137 | }); 138 | 139 | var memoA1 = selector(stateNames)("A"); 140 | var memoA2 = selector(stateNames)("A"); 141 | expect(memoA1, ["Anna", "Arnold", "Amanda"]); 142 | expect(identical(memoA1, memoA2), isTrue); 143 | 144 | selector(stateNames)("B"); 145 | 146 | var memoA3 = selector(stateNames)("A"); 147 | expect(memoA3, ["Anna", "Arnold", "Amanda"]); 148 | expect(identical(memoA1, memoA3), isTrue); 149 | }); 150 | 151 | test('Test results are forgotten when the state changes (1 state with 1 parameter).', () { 152 | // 153 | var selector = cache1state_1param((List state) => (String startString) { 154 | return state.where((str) => str.startsWith(startString)).toList(); 155 | }); 156 | 157 | var memoA1 = selector(stateNames)("A"); 158 | var memoA2 = selector(stateNames)("A"); 159 | expect(memoA1, ["Anna", "Arnold", "Amanda"]); 160 | expect(identical(memoA1, memoA2), isTrue); 161 | 162 | // Another state with another parameter. 163 | selector(List.of(stateNames))("B"); 164 | selector(stateNames)("B"); 165 | 166 | // Try reading the previous state, with the same parameter as before. 167 | var memoA5 = selector(stateNames)("A"); 168 | expect(memoA5, ["Anna", "Arnold", "Amanda"]); 169 | expect(identical(memoA5, memoA1), isFalse); 170 | }); 171 | 172 | test('Test 1 state with 2 parameters.', () { 173 | // 174 | var selector = 175 | cache1state_2params((List state) => (String startString, String endString) { 176 | return state 177 | .where((str) => str.startsWith(startString) && str.endsWith(endString)) 178 | .toList(); 179 | }); 180 | 181 | // TODO: MARCELO: Refazer este teste. 182 | // Originalmente era: expect(identical("a", otherA), isFalse); 183 | // With no other changes except 184 | // flutter channel stable 1.22.5 => flutter channel beta 1.24.0-10.2.pre 185 | // (Dart version 2.10.4) => (Dart version 2.12.0 (build 2.12.0-29.10.beta)) 186 | // suddenly this isTrue 187 | String otherA = "a" ""; // Concatenate. 188 | expect(identical("a", otherA), isTrue); 189 | 190 | var memoA1 = selector(stateNames)("A", "a"); 191 | var memoA2 = selector(stateNames)("A", otherA); 192 | expect(memoA1, ["Anna", "Amanda"]); 193 | expect(identical(memoA1, memoA2), isTrue); 194 | 195 | var memoB1 = selector(stateNames)("A", "d"); 196 | var memoB2 = selector(stateNames)("A", "d"); 197 | expect(memoB1, ["Arnold"]); 198 | expect(identical(memoB1, memoB2), isTrue); 199 | 200 | var memoA3 = selector(stateNames)("A", "a"); 201 | expect(memoA1, ["Anna", "Amanda"]); 202 | expect(identical(memoA1, memoA3), isTrue); 203 | }); 204 | 205 | test('Test results are forgotten when the state changes (1 state with 2 parameters).', () { 206 | // 207 | var selector = 208 | cache1state_2params((List state) => (String startString, String endString) { 209 | return state 210 | .where((str) => str.startsWith(startString) && str.endsWith(endString)) 211 | .toList(); 212 | }); 213 | 214 | var memoA1 = selector(stateNames)("A", "a"); 215 | var memoA2 = selector(stateNames)("A", "a"); 216 | expect(memoA1, ["Anna", "Amanda"]); 217 | expect(identical(memoA1, memoA2), isTrue); 218 | 219 | // Another state with another parameter. 220 | selector(List.of(stateNames))("B", "l"); 221 | selector(stateNames)("B", "l"); 222 | 223 | // Try reading the previous state, with the same parameter as before. 224 | var memoA5 = selector(stateNames)("A", "a"); 225 | expect(memoA5, ["Anna", "Amanda"]); 226 | expect(identical(memoA5, memoA1), isFalse); 227 | }); 228 | 229 | test('Test 2 states with 0 parameters.', () { 230 | // 231 | var selector = cache2states((List names, int limit) => () { 232 | return names.where((str) => str.startsWith("A")).take(limit).toList(); 233 | }); 234 | 235 | var memoA1 = selector(stateNames, 1)(); 236 | var memoA2 = selector(stateNames, 1)(); 237 | expect(memoA1, ["Anna"]); 238 | expect(memoA2, ["Anna"]); 239 | expect(identical(memoA1, memoA2), isTrue); 240 | 241 | var memoB1 = selector(stateNames, 2)(); 242 | var memoB2 = selector(stateNames, 2)(); 243 | expect(memoB1, ["Anna", "Arnold"]); 244 | expect(identical(memoB1, memoB2), isTrue); 245 | }); 246 | 247 | test('Test results are forgotten when the state changes (2 states with 0 parameters).', () { 248 | // 249 | var selector = cache2states((List names, int limit) => () { 250 | return names.where((str) => str.startsWith("A")).take(limit).toList(); 251 | }); 252 | 253 | var memoA1 = selector(stateNames, 1)(); 254 | var memoA2 = selector(stateNames, 1)(); 255 | expect(memoA1, ["Anna"]); 256 | expect(identical(memoA1, memoA2), isTrue); 257 | 258 | // Another state with another parameter. 259 | selector(stateNames, 2)(); 260 | 261 | // Try reading the previous state, with the same parameter as before. 262 | var memoA5 = selector(stateNames, 1)(); 263 | expect(memoA5, ["Anna"]); 264 | expect(identical(memoA5, memoA1), isFalse); 265 | }); 266 | 267 | test('Test 2 states with 1 parameter.', () { 268 | // 269 | var selector = cache2states_1param((List names, int limit) => (String searchString) { 270 | return names.where((str) => str.startsWith(searchString)).take(limit).toList(); 271 | }); 272 | 273 | var memoA1 = selector(stateNames, 1)("A"); 274 | var memoA2 = selector(stateNames, 1)("A"); 275 | expect(memoA1, ["Anna"]); 276 | expect(identical(memoA1, memoA2), isTrue); 277 | 278 | var memoB1 = selector(stateNames, 2)("A"); 279 | var memoB2 = selector(stateNames, 2)("A"); 280 | expect(memoB1, ["Anna", "Arnold"]); 281 | expect(identical(memoB1, memoB2), isTrue); 282 | 283 | var memoC = selector(stateNames, 2)("B"); 284 | expect(memoC, ["Bill"]); 285 | 286 | var memoD = selector(stateNames, 2)("A"); 287 | expect(identical(memoD, memoB1), isTrue); 288 | 289 | // Has to forget, because the state changed. 290 | selector(stateNames, 1)("A"); 291 | expect(identical(memoA1, memoC), isFalse); 292 | }); 293 | 294 | test('Test results are forgotten when the state changes (2 states with 1 parameter).', () { 295 | // 296 | var selector = cache2states_1param((List names, int limit) => (String searchString) { 297 | return names.where((str) => str.startsWith(searchString)).take(limit).toList(); 298 | }); 299 | 300 | var memoA1 = selector(stateNames, 1)("A"); 301 | var memoA2 = selector(stateNames, 1)("A"); 302 | expect(memoA1, ["Anna"]); 303 | expect(identical(memoA1, memoA2), isTrue); 304 | 305 | // Another state with another parameter. 306 | selector(stateNames, 2)("B"); 307 | selector(stateNames, 1)("B"); 308 | 309 | // Try reading the previous state, with the same parameter as before. 310 | var memoA5 = selector(stateNames, 1)("A"); 311 | expect(memoA5, ["Anna"]); 312 | expect(identical(memoA5, memoA1), isFalse); 313 | }); 314 | 315 | test('Test 2 states with 2 parameters.', () { 316 | // 317 | var selector = cache2states_2params( 318 | (List names, int limit) => (String startString, String endString) { 319 | return names 320 | .where((str) => str.startsWith(startString) && str.endsWith(endString)) 321 | .take(limit) 322 | .toList(); 323 | }); 324 | 325 | var memoA1 = selector(stateNames, 1)("A", "a"); 326 | var memoA2 = selector(stateNames, 1)("A", "a"); 327 | expect(memoA1, ["Anna"]); 328 | expect(identical(memoA1, memoA2), isTrue); 329 | 330 | var memoB1 = selector(stateNames, 2)("A", "a"); 331 | var memoB2 = selector(stateNames, 2)("A", "a"); 332 | expect(memoB1, ["Anna", "Amanda"]); 333 | expect(identical(memoB1, memoB2), isTrue); 334 | }); 335 | 336 | test('Test results are forgotten when the state changes (2 states with 2 parameters).', () { 337 | // 338 | var selector = cache2states_2params( 339 | (List names, int limit) => (String startString, String endString) { 340 | return names 341 | .where((str) => str.startsWith(startString) && str.endsWith(endString)) 342 | .take(limit) 343 | .toList(); 344 | }); 345 | 346 | var memoA1 = selector(stateNames, 1)("A", "a"); 347 | var memoA2 = selector(stateNames, 1)("A", "a"); 348 | expect(memoA1, ["Anna"]); 349 | expect(identical(memoA1, memoA2), isTrue); 350 | 351 | // Another state with another parameter. 352 | selector(stateNames, 2)("B", "l"); 353 | selector(stateNames, 1)("B", "l"); 354 | 355 | // Try reading the previous state, with the same parameter as before. 356 | var memoA5 = selector(stateNames, 1)("A", "a"); 357 | expect(memoA5, ["Anna"]); 358 | expect(identical(memoA5, memoA1), isFalse); 359 | }); 360 | 361 | test( 362 | 'Changing the second or the first state, it should forget the cached value. ' 363 | '(2 states with 2 parameters)', () { 364 | // 365 | var stateNames1 = List.unmodifiable(["A1a", "A2a", "A3x", "B4a", "B5a", "B6x"]); 366 | 367 | var selector = cache2states_2params( 368 | (List names, int limit) => (String startString, String endString) { 369 | return names 370 | .where((str) => str.startsWith(startString) && str.endsWith(endString)) 371 | .take(limit) 372 | .toList(); 373 | }); 374 | 375 | var memo1 = selector(stateNames1, 1)("A", "a"); 376 | expect(memo1, ["A1a"]); 377 | 378 | var memo2 = selector(stateNames1, 2)("A", "a"); 379 | expect(memo2, ["A1a", "A2a"]); 380 | 381 | var memo3 = selector(stateNames1, 1)("A", "a"); 382 | expect(memo3, ["A1a"]); 383 | 384 | var memo4 = selector(stateNames1, 2)("A", "a"); 385 | expect(memo4, ["A1a", "A2a"]); 386 | 387 | expect(identical(memo1, memo3), isFalse); 388 | expect(identical(memo2, memo4), isFalse); 389 | 390 | // --- 391 | 392 | var stateNames2 = List.unmodifiable(["A1a", "A2a", "A3x", "B4a", "B5a", "B6x"]); 393 | 394 | var memo5 = selector(stateNames1, 1)("A", "a"); 395 | expect(memo5, ["A1a"]); 396 | 397 | var memo6 = selector(stateNames2, 1)("A", "a"); 398 | expect(memo6, ["A1a"]); 399 | 400 | var memo7 = selector(stateNames1, 1)("A", "a"); 401 | expect(memo7, ["A1a"]); 402 | 403 | var memo8 = selector(stateNames2, 1)("A", "a"); 404 | expect(memo8, ["A1a"]); 405 | 406 | expect(identical(memo5, memo7), isFalse); 407 | expect(identical(memo6, memo8), isFalse); 408 | }); 409 | 410 | test('Test 2 states with 3 parameters.', () { 411 | // 412 | var selector = cache2states_3params((List names, int limit) => 413 | (String startString, String endString, String containsString) { 414 | return names 415 | .where((str) => 416 | str.startsWith(startString) && 417 | str.endsWith(endString) && 418 | str.contains(containsString)) 419 | .take(limit) 420 | .toList(); 421 | }); 422 | 423 | expect(selector(stateNames, 1)("A", "a", "x"), []); 424 | 425 | var memoA1 = selector(stateNames, 1)("A", "a", "n"); 426 | var memoA2 = selector(stateNames, 1)("A", "a", "n"); 427 | expect(memoA1, ["Anna"]); 428 | expect(identical(memoA1, memoA2), isTrue); 429 | 430 | var memoB1 = selector(stateNames, 2)("A", "a", "n"); 431 | var memoB2 = selector(stateNames, 2)("A", "a", "n"); 432 | expect(memoB1, ["Anna", "Amanda"]); 433 | expect(identical(memoB1, memoB2), isTrue); 434 | }); 435 | 436 | test('Test results are forgotten when the state changes (2 states with 3 parameters).', () { 437 | // 438 | var selector = cache2states_3params((List names, int limit) => 439 | (String startString, String endString, String containsString) { 440 | return names 441 | .where((str) => 442 | str.startsWith(startString) && 443 | str.endsWith(endString) && 444 | str.contains(containsString)) 445 | .take(limit) 446 | .toList(); 447 | }); 448 | 449 | var memoA1 = selector(stateNames, 1)("A", "a", "n"); 450 | var memoA2 = selector(stateNames, 1)("A", "a", "n"); 451 | expect(memoA1, ["Anna"]); 452 | expect(identical(memoA1, memoA2), isTrue); 453 | 454 | // Same state with another parameter. 455 | // Then try reading the previous state, with the same parameter as before. 456 | // Result is the same instance. 457 | var memoA3 = selector(stateNames, 1)("A", "a", "x"); 458 | var memoA4 = selector(stateNames, 1)("A", "a", "n"); 459 | var memoA5 = selector(stateNames, 1)("A", "a", "n"); 460 | var memoA6 = selector(stateNames, 1)("A", "a", "x"); 461 | expect(memoA3, []); 462 | expect(memoA4, ["Anna"]); 463 | expect(memoA5, ["Anna"]); 464 | expect(memoA6, []); 465 | expect(identical(memoA4, memoA5), isTrue); 466 | expect(identical(memoA3, memoA6), isTrue); 467 | 468 | // --- 469 | 470 | // Another state with the same parameter. 471 | selector(stateNames, 2)("A", "a", "n"); 472 | 473 | // Try reading the previous state, with the same parameter as before. 474 | var memoA7 = selector(stateNames, 1)("A", "a", "x"); 475 | var memoA8 = selector(stateNames, 1)("A", "a", "n"); 476 | expect(memoA7, []); 477 | expect(memoA8, ["Anna"]); 478 | expect(identical(memoA7, memoA3), isFalse); 479 | expect(identical(memoA8, memoA4), isFalse); 480 | }); 481 | 482 | test( 483 | 'Changing the second or the first state, it should forget the cached value. ' 484 | '(2 states with 3 parameters)', () { 485 | // 486 | var stateNames1 = List.unmodifiable(["A1na", "A2na", "A3nx", "B4na", "B5na", "B6nx"]); 487 | 488 | var selector = cache2states_3params((List names, int limit) => 489 | (String startString, String endString, String containsString) { 490 | return names 491 | .where((str) => 492 | str.startsWith(startString) && 493 | str.endsWith(endString) && 494 | str.contains(containsString)) 495 | .take(limit) 496 | .toList(); 497 | }); 498 | 499 | var memo1 = selector(stateNames1, 1)("A", "a", "n"); 500 | expect(memo1, ["A1na"]); 501 | 502 | var memo2 = selector(stateNames1, 2)("A", "a", "n"); 503 | expect(memo2, ["A1na", "A2na"]); 504 | 505 | var memo3 = selector(stateNames1, 1)("A", "a", "n"); 506 | expect(memo3, ["A1na"]); 507 | 508 | var memo4 = selector(stateNames1, 2)("A", "a", "n"); 509 | expect(memo4, ["A1na", "A2na"]); 510 | 511 | expect(identical(memo1, memo3), isFalse); 512 | expect(identical(memo2, memo4), isFalse); 513 | 514 | // --- 515 | 516 | var stateNames2 = List.unmodifiable(["A1na", "A2na", "A3nx", "B4na", "B5na", "B6nx"]); 517 | 518 | var memo5 = selector(stateNames1, 1)("A", "a", "n"); 519 | expect(memo5, ["A1na"]); 520 | 521 | var memo6 = selector(stateNames2, 1)("A", "a", "n"); 522 | expect(memo6, ["A1na"]); 523 | 524 | var memo7 = selector(stateNames1, 1)("A", "a", "n"); 525 | expect(memo7, ["A1na"]); 526 | 527 | var memo8 = selector(stateNames2, 1)("A", "a", "n"); 528 | expect(memo8, ["A1na"]); 529 | 530 | expect(identical(memo5, memo7), isFalse); 531 | expect(identical(memo6, memo8), isFalse); 532 | }); 533 | 534 | test('Test 3 states with 0 parameters.', () { 535 | // 536 | var selector = cache3states(( 537 | List names, 538 | int limit, 539 | String startsWith, 540 | ) => 541 | () { 542 | return names.where((str) => str.startsWith(startsWith)).take(limit).toList(); 543 | }); 544 | 545 | // Same states return the same object as the result. 546 | var memoA1 = selector(stateNames, 1, "A")(); 547 | var memoA2 = selector(stateNames, 1, "A")(); 548 | expect(memoA1, ["Anna"]); 549 | expect(memoA2, ["Anna"]); 550 | expect(identical(memoA1, memoA2), isTrue); 551 | 552 | // --- 553 | 554 | // Change first state (even if it's equal), deletes the cache. 555 | var memoA = selector(stateNames, 1, "A")(); 556 | var memoB = selector(stateNames.toList(), 1, "A")(); 557 | expect(memoB, ["Anna"]); 558 | expect(identical(memoA, memoB), isFalse); 559 | 560 | // --- 561 | 562 | // Change first state, deletes the cache. 563 | memoA = selector(stateNames, 1, "A")(); 564 | selector(stateNames.toList(), 1, "A")(); 565 | memoB = selector(stateNames, 1, "A")(); 566 | expect(memoB, ["Anna"]); 567 | expect(identical(memoA, memoB), isFalse); 568 | 569 | // Change second state, deletes the cache. 570 | memoA = selector(stateNames, 1, "A")(); 571 | selector(stateNames, 2, "A")(); 572 | memoB = selector(stateNames, 1, "A")(); 573 | expect(memoB, ["Anna"]); 574 | expect(identical(memoA, memoB), isFalse); 575 | 576 | // Change third state, deletes the cache. 577 | memoA = selector(stateNames, 1, "A")(); 578 | selector(stateNames, 1, "B")(); 579 | memoB = selector(stateNames, 1, "A")(); 580 | expect(memoB, ["Anna"]); 581 | expect(identical(memoA, memoB), isFalse); 582 | }); 583 | } 584 | -------------------------------------------------------------------------------- /test/weak_container_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:weak_map/weak_map.dart'; 3 | 4 | void main() { 5 | test("Contains.", () { 6 | expect(WeakContainer(1).contains(1), true); 7 | expect(WeakContainer(1).contains(2), false); 8 | expect(WeakContainer(1).contains("A"), false); 9 | expect(WeakContainer(true).contains(null), false); 10 | 11 | expect(WeakContainer("A").contains("A"), true); 12 | expect(WeakContainer("A").contains("B"), false); 13 | expect(WeakContainer("A").contains(1), false); 14 | expect(WeakContainer(true).contains(null), false); 15 | 16 | expect(WeakContainer(true).contains(true), true); 17 | expect(WeakContainer(true).contains(false), false); 18 | expect(WeakContainer(true).contains("A"), false); 19 | expect(WeakContainer(true).contains(null), false); 20 | 21 | expect(WeakContainer(null).contains(null), true); 22 | expect(WeakContainer(null).contains(true), false); 23 | expect(WeakContainer(null).contains(false), false); 24 | expect(WeakContainer(null).contains("A"), false); 25 | expect(WeakContainer(null).contains(1), false); 26 | 27 | var obj1 = Object(); 28 | var obj2 = Object(); 29 | expect(WeakContainer(obj1).contains(obj1), true); 30 | expect(WeakContainer(obj1).contains(obj2), false); 31 | expect(WeakContainer(obj1).contains(1), false); 32 | expect(WeakContainer(obj1).contains("A"), false); 33 | expect(WeakContainer(obj1).contains(null), false); 34 | expect(WeakContainer(1).contains(obj1), false); 35 | expect(WeakContainer("A").contains(obj1), false); 36 | expect(WeakContainer(true).contains(obj1), false); 37 | expect(WeakContainer(false).contains(obj1), false); 38 | expect(WeakContainer(null).contains(obj1), false); 39 | }); 40 | 41 | test("Clear.", () { 42 | var container = WeakContainer(1); 43 | expect(container.contains(1), true); 44 | container.clear(); 45 | expect(container.contains(1), false); 46 | }); 47 | 48 | test("Using null in the map.", () { 49 | var map = WeakMap(); 50 | 51 | var obj1 = Object(); 52 | expect(map[null], null); 53 | expect(map.contains(null), false); 54 | 55 | map[null] = obj1; 56 | expect(map[null], obj1); 57 | expect(map.contains(null), true); 58 | 59 | map[null] = null; 60 | expect(map[null], null); 61 | expect(map.contains(null), false); 62 | }); 63 | 64 | test("Using null in the map.", () async { 65 | var map = WeakMap(); 66 | 67 | var obj1 = Object(); 68 | expect(map[null], null); 69 | expect(map.contains(null), false); 70 | 71 | map[null] = obj1; 72 | expect(map[null], obj1); 73 | expect(map.contains(null), true); 74 | 75 | map[null] = null; 76 | expect(map[null], null); 77 | expect(map.contains(null), false); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/weak_map_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:weak_map/weak_map.dart'; 3 | 4 | void main() { 5 | test("Add number/String/boolean to map.", () { 6 | var map = WeakMap(); 7 | 8 | var obj1 = Object(); 9 | var obj2 = Object(); 10 | var obj3 = Object(); 11 | var obj4 = Object(); 12 | var obj5 = Object(); 13 | expect(obj1, isNot(obj2)); 14 | expect(obj2, isNot(obj3)); 15 | expect(obj3, isNot(obj4)); 16 | expect(obj4, isNot(obj5)); 17 | 18 | map[1] = obj1; 19 | map["A"] = obj2; 20 | map[true] = obj3; 21 | map[false] = obj4; 22 | map[null] = obj5; 23 | 24 | expect(map[1], obj1); 25 | expect(map["A"], obj2); 26 | expect(map[true], obj3); 27 | expect(map[false], obj4); 28 | expect(map[null], obj5); 29 | }); 30 | 31 | test("Using null as the map value.", () { 32 | var map = WeakMap(); 33 | 34 | map["A"] = null; 35 | expect(map.contains("A"), false); 36 | 37 | map["A"] = Object(); 38 | expect(map.contains("A"), true); 39 | 40 | map["A"] = null; 41 | expect(map.contains("A"), false); 42 | 43 | map["A"] = Object(); 44 | map.remove("A"); 45 | expect(map.contains("A"), false); 46 | }); 47 | 48 | test("Using null as the map key.", () { 49 | var map = WeakMap(); 50 | 51 | var obj1 = Object(); 52 | expect(map[null], null); 53 | expect(map.contains(null), false); 54 | 55 | map[null] = obj1; 56 | expect(map[null], obj1); 57 | expect(map.contains(null), true); 58 | 59 | map[null] = null; 60 | expect(map[null], null); 61 | expect(map.contains(null), false); 62 | }); 63 | 64 | test("Using null in the map.", () async { 65 | var map = WeakMap(); 66 | 67 | var obj1 = Object(); 68 | expect(map[null], null); 69 | expect(map.contains(null), false); 70 | 71 | map[null] = obj1; 72 | expect(map[null], obj1); 73 | expect(map.contains(null), true); 74 | 75 | map[null] = null; 76 | expect(map[null], null); 77 | expect(map.contains(null), false); 78 | }); 79 | 80 | test("getOrThrow", () async { 81 | // 82 | var map = WeakMap(); 83 | map[1] = 1; 84 | map["A"] = 2; 85 | map[true] = 3; 86 | var obj1 = Object(); 87 | map[obj1] = 4; 88 | var obj2 = Object(); 89 | map[obj2] = 5; 90 | 91 | expect(map[1], 1); 92 | expect(map.get(1), 1); 93 | expect(map.getOrThrow(1), 1); 94 | 95 | expect(map["A"], 2); 96 | expect(map.get("A"), 2); 97 | expect(map.getOrThrow("A"), 2); 98 | 99 | expect(map[true], 3); 100 | expect(map.get(true), 3); 101 | expect(map.getOrThrow(true), 3); 102 | 103 | expect(map[obj1], 4); 104 | expect(map.get(obj1), 4); 105 | expect(map.getOrThrow(obj1), 4); 106 | 107 | expect(map[obj2], 5); 108 | expect(map.get(obj2), 5); 109 | expect(map.getOrThrow(obj2), 5); 110 | 111 | expect(map[123], null); 112 | expect(map.get(123), null); 113 | expect(() => map.getOrThrow(123), throwsStateError); 114 | }); 115 | } 116 | --------------------------------------------------------------------------------