react-native-mmkv 4.1.2 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/NitroMmkv.podspec +9 -2
  2. package/android/CMakeLists.txt +5 -0
  3. package/android/build.gradle +8 -3
  4. package/android/src/main/cpp/cpp-adapter.cpp +2 -1
  5. package/cpp/HybridMMKV.cpp +94 -58
  6. package/cpp/HybridMMKV.hpp +5 -0
  7. package/cpp/HybridMMKVFactory.cpp +1 -5
  8. package/cpp/MMKVTypes.hpp +12 -0
  9. package/cpp/ManagedMMBuffer.hpp +1 -1
  10. package/lib/__tests__/hooks.test.js +30 -1
  11. package/lib/createMMKV/createMMKV.web.js +17 -1
  12. package/lib/createMMKV/createMockMMKV.js +15 -1
  13. package/lib/hooks/createMMKVHook.js +9 -18
  14. package/lib/specs/MMKV.nitro.d.ts +46 -1
  15. package/lib/specs/MMKVFactory.nitro.d.ts +30 -1
  16. package/nitrogen/generated/android/NitroMmkvOnLoad.cpp +35 -25
  17. package/nitrogen/generated/android/NitroMmkvOnLoad.hpp +13 -4
  18. package/nitrogen/generated/android/c++/JHybridMMKVPlatformContextSpec.cpp +20 -26
  19. package/nitrogen/generated/android/c++/JHybridMMKVPlatformContextSpec.hpp +19 -22
  20. package/nitrogen/generated/android/kotlin/com/margelo/nitro/mmkv/HybridMMKVPlatformContextSpec.kt +15 -18
  21. package/nitrogen/generated/ios/NitroMmkv-Swift-Cxx-Bridge.hpp +1 -1
  22. package/nitrogen/generated/ios/swift/HybridMMKVPlatformContextSpec.swift +0 -1
  23. package/nitrogen/generated/ios/swift/HybridMMKVPlatformContextSpec_cxx.swift +0 -1
  24. package/nitrogen/generated/shared/c++/Configuration.hpp +13 -2
  25. package/nitrogen/generated/shared/c++/EncryptionType.hpp +76 -0
  26. package/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp +5 -0
  27. package/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp +8 -0
  28. package/package.json +3 -3
  29. package/src/__tests__/hooks.test.tsx +37 -0
  30. package/src/createMMKV/createMMKV.web.ts +17 -1
  31. package/src/createMMKV/createMockMMKV.ts +15 -1
  32. package/src/hooks/createMMKVHook.ts +16 -19
  33. package/src/specs/MMKV.nitro.ts +46 -1
  34. package/src/specs/MMKVFactory.nitro.ts +31 -1
package/NitroMmkv.podspec CHANGED
@@ -24,13 +24,20 @@ Pod::Spec.new do |s|
24
24
 
25
25
  # Add MMKV Core dependency
26
26
  s.compiler_flags = '-x objective-c++'
27
- s.dependency 'MMKVCore', '2.2.4'
27
+ s.dependency 'MMKVCore', '2.4.0'
28
+
29
+ # Optionally configure MMKV log level via Podfile ($MMKVLogLevel) or env var (MMKV_LOG_LEVEL)
30
+ mmkv_log_level = $MMKVLogLevel || ENV['MMKV_LOG_LEVEL']
31
+ gcc_preprocessor_defs = "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES"
32
+ if mmkv_log_level != nil && mmkv_log_level.to_s != ""
33
+ gcc_preprocessor_defs += " MMKV_LOG_LEVEL=#{mmkv_log_level}"
34
+ end
28
35
 
29
36
  # TODO: Remove when no one uses RN 0.79 anymore
30
37
  # Add support for React Native 0.79 or below
31
38
  s.pod_target_xcconfig = {
32
39
  "HEADER_SEARCH_PATHS" => ["${PODS_ROOT}/RCT-Folly"],
33
- "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES",
40
+ "GCC_PREPROCESSOR_DEFINITIONS" => gcc_preprocessor_defs,
34
41
  "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1"
35
42
  }
36
43
 
@@ -5,6 +5,11 @@ set (PACKAGE_NAME NitroMmkv)
5
5
  set (CMAKE_VERBOSE_MAKEFILE ON)
6
6
  set (CMAKE_CXX_STANDARD 20)
7
7
 
8
+ # Optionally configure MMKV log level (passed from Gradle as -DMMKV_LOG_LEVEL=<0-4>)
9
+ if(DEFINED MMKV_LOG_LEVEL)
10
+ add_definitions(-DMMKV_LOG_LEVEL=${MMKV_LOG_LEVEL})
11
+ endif()
12
+
8
13
  # Find all C++ files (shared and platform specifics)
9
14
  file(GLOB_RECURSE shared_files RELATIVE ${CMAKE_SOURCE_DIR}
10
15
  "../cpp/**.cpp"
@@ -5,7 +5,7 @@ buildscript {
5
5
  }
6
6
 
7
7
  dependencies {
8
- classpath "com.android.tools.build:gradle:8.13.1"
8
+ classpath "com.android.tools.build:gradle:9.1.0"
9
9
  }
10
10
  }
11
11
 
@@ -49,7 +49,12 @@ android {
49
49
  externalNativeBuild {
50
50
  cmake {
51
51
  cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
52
- arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
52
+ def mmkvLogLevel = rootProject.hasProperty("MMKV_logLevel") ? rootProject.property("MMKV_logLevel") : null
53
+ if (mmkvLogLevel != null && mmkvLogLevel != "") {
54
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", "-DMMKV_LOG_LEVEL=${mmkvLogLevel}"
55
+ } else {
56
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
57
+ }
53
58
  abiFilters (*reactNativeArchitectures())
54
59
 
55
60
  buildTypes {
@@ -140,6 +145,6 @@ dependencies {
140
145
  implementation project(":react-native-nitro-modules")
141
146
 
142
147
  // Add a dependency on mmkv core (this ships a C++ prefab)
143
- implementation "io.github.zhongwuzw:mmkv:2.2.4"
148
+ implementation "io.github.zhongwuzw:mmkv:2.4.0"
144
149
  }
145
150
 
@@ -1,6 +1,7 @@
1
1
  #include "NitroMmkvOnLoad.hpp"
2
+ #include <fbjni/fbjni.h>
2
3
  #include <jni.h>
3
4
 
4
5
  JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5
- return margelo::nitro::mmkv::initialize(vm);
6
+ return facebook::jni::initialize(vm, []() { margelo::nitro::mmkv::registerAllNatives(); });
6
7
  }
@@ -14,40 +14,51 @@
14
14
  namespace margelo::nitro::mmkv {
15
15
 
16
16
  HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) {
17
- std::string path = config.path.has_value() ? config.path.value() : "";
18
- std::string encryptionKey = config.encryptionKey.has_value() ? config.encryptionKey.value() : "";
17
+ MMKVMode mmkvMode = getMMKVMode(config);
18
+ if (config.readOnly.value_or(false)) {
19
+ mmkvMode = mmkvMode | MMKVMode::MMKV_READ_ONLY;
20
+ }
21
+ bool useAes256Encryption = config.encryptionType.value_or(EncryptionType::AES_128) == EncryptionType::AES_256;
22
+ std::string encryptionKey = config.encryptionKey.value_or("");
23
+ std::string* encryptionKeyPtr = encryptionKey.size() > 0 ? &encryptionKey : nullptr;
24
+ std::string rootPath = config.path.value_or("");
25
+ std::string* rootPathPtr = rootPath.size() > 0 ? &rootPath : nullptr;
26
+ bool compareBeforeSet = config.compareBeforeSet.value_or(false);
27
+
28
+ MMKVConfig mmkvConfig{.mode = mmkvMode,
29
+ .aes256 = useAes256Encryption,
30
+ .cryptKey = encryptionKeyPtr,
31
+ .rootPath = rootPathPtr,
32
+ .enableCompareBeforeSet = compareBeforeSet};
33
+
19
34
  bool hasEncryptionKey = encryptionKey.size() > 0;
20
- Logger::log(LogLevel::Info, TAG, "Creating MMKV instance \"%s\"... (Path: %s, Encrypted: %s)", config.id.c_str(), path.c_str(),
35
+ Logger::log(LogLevel::Info, TAG, "Creating MMKV instance \"%s\"... (Path: %s, Encrypted: %s)", config.id.c_str(), rootPath.c_str(),
21
36
  hasEncryptionKey ? "true" : "false");
22
37
 
23
- std::string* pathPtr = path.size() > 0 ? &path : nullptr;
24
- std::string* encryptionKeyPtr = encryptionKey.size() > 0 ? &encryptionKey : nullptr;
25
- MMKVMode mode = getMMKVMode(config);
26
- if (config.readOnly.has_value() && config.readOnly.value()) {
27
- Logger::log(LogLevel::Info, TAG, "Instance is read-only!");
28
- mode = mode | ::mmkv::MMKV_READ_ONLY;
29
- }
30
-
31
- #ifdef __APPLE__
32
- instance = MMKV::mmkvWithID(config.id, mode, encryptionKeyPtr, pathPtr);
33
- #else
34
- instance = MMKV::mmkvWithID(config.id, DEFAULT_MMAP_SIZE, mode, encryptionKeyPtr, pathPtr);
35
- #endif
38
+ instance = MMKV::mmkvWithID(config.id, mmkvConfig);
36
39
 
37
40
  if (instance == nullptr) [[unlikely]] {
38
41
  // Check if instanceId is invalid
39
- if (config.id.empty()) [[unlikely]] {
42
+ if (config.id.empty()) {
40
43
  throw std::runtime_error("Failed to create MMKV instance! `id` cannot be empty!");
41
44
  }
42
45
 
43
- // Check if encryptionKey is invalid
44
- if (encryptionKey.size() > 16) [[unlikely]] {
45
- throw std::runtime_error("Failed to create MMKV instance! `encryptionKey` cannot be longer "
46
- "than 16 bytes!");
46
+ if (useAes256Encryption) {
47
+ // With AES-256, the max key length is 32 bytes.
48
+ if (encryptionKey.size() > 32) [[unlikely]] {
49
+ throw std::runtime_error("Failed to create MMKV instance! `encryptionKey` cannot be longer "
50
+ "than 32 bytes with AES-256 encryption!");
51
+ }
52
+ } else {
53
+ // With AES-128, the max key length is 16 bytes.
54
+ if (encryptionKey.size() > 16) [[unlikely]] {
55
+ throw std::runtime_error("Failed to create MMKV instance! `encryptionKey` cannot be longer "
56
+ "than 16 bytes with AES-128 encryption!");
57
+ }
47
58
  }
48
59
 
49
60
  // Check if path is maybe invalid
50
- if (path.empty()) [[unlikely]] {
61
+ if (rootPath.empty()) [[unlikely]] {
51
62
  throw std::runtime_error("Failed to create MMKV instance! `path` cannot be empty!");
52
63
  }
53
64
 
@@ -58,13 +69,27 @@ HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) {
58
69
  std::string HybridMMKV::getId() {
59
70
  return instance->mmapID();
60
71
  }
72
+
73
+ double HybridMMKV::getLength() {
74
+ return instance->count();
75
+ }
76
+
61
77
  double HybridMMKV::getSize() {
78
+ return getByteSize();
79
+ }
80
+
81
+ double HybridMMKV::getByteSize() {
62
82
  return instance->actualSize();
63
83
  }
84
+
64
85
  bool HybridMMKV::getIsReadOnly() {
65
86
  return instance->isReadOnly();
66
87
  }
67
88
 
89
+ bool HybridMMKV::getIsEncrypted() {
90
+ return instance->isEncryptionEnabled();
91
+ }
92
+
68
93
  // helper: overload pattern matching for lambdas
69
94
  template <class... Ts>
70
95
  struct overloaded : Ts... {
@@ -79,31 +104,32 @@ void HybridMMKV::set(const std::string& key, const std::variant<bool, std::share
79
104
  }
80
105
 
81
106
  // Pattern-match each potential value in std::variant
82
- bool didSet = std::visit(overloaded{[&](bool b) {
83
- // boolean
84
- return instance->set(b, key);
85
- },
86
- [&](const std::shared_ptr<ArrayBuffer>& buf) {
87
- // ArrayBuffer
88
- MMBuffer buffer(buf->data(), buf->size(), MMBufferCopyFlag::MMBufferNoCopy);
89
- return instance->set(std::move(buffer), key);
90
- },
91
- [&](const std::string& string) {
92
- // string
93
- return instance->set(string, key);
94
- },
95
- [&](double number) {
96
- // number
97
- return instance->set(number, key);
98
- }},
99
- value);
100
- if (!didSet) {
107
+ bool successful = std::visit(overloaded{[&](bool b) {
108
+ // boolean
109
+ return instance->set(b, key);
110
+ },
111
+ [&](const std::shared_ptr<ArrayBuffer>& buf) {
112
+ // ArrayBuffer
113
+ MMBuffer buffer(buf->data(), buf->size(), MMBufferCopyFlag::MMBufferNoCopy);
114
+ return instance->set(std::move(buffer), key);
115
+ },
116
+ [&](const std::string& string) {
117
+ // string
118
+ return instance->set(string, key);
119
+ },
120
+ [&](double number) {
121
+ // number
122
+ return instance->set(number, key);
123
+ }},
124
+ value);
125
+ if (!successful) [[unlikely]] {
101
126
  throw std::runtime_error("Failed to set value for key \"" + key + "\"!");
102
127
  }
103
128
 
104
129
  // Notify on changed
105
130
  MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
106
131
  }
132
+
107
133
  std::optional<bool> HybridMMKV::getBoolean(const std::string& key) {
108
134
  bool hasValue;
109
135
  bool result = instance->getBool(key, /* defaultValue */ false, &hasValue);
@@ -113,6 +139,7 @@ std::optional<bool> HybridMMKV::getBoolean(const std::string& key) {
113
139
  return std::nullopt;
114
140
  }
115
141
  }
142
+
116
143
  std::optional<std::string> HybridMMKV::getString(const std::string& key) {
117
144
  std::string result;
118
145
  bool hasValue = instance->getString(key, result, /* inplaceModification */ true);
@@ -122,6 +149,7 @@ std::optional<std::string> HybridMMKV::getString(const std::string& key) {
122
149
  return std::nullopt;
123
150
  }
124
151
  }
152
+
125
153
  std::optional<double> HybridMMKV::getNumber(const std::string& key) {
126
154
  bool hasValue;
127
155
  double result = instance->getDouble(key, /* defaultValue */ 0.0, &hasValue);
@@ -131,25 +159,21 @@ std::optional<double> HybridMMKV::getNumber(const std::string& key) {
131
159
  return std::nullopt;
132
160
  }
133
161
  }
162
+
134
163
  std::optional<std::shared_ptr<ArrayBuffer>> HybridMMKV::getBuffer(const std::string& key) {
135
164
  MMBuffer result;
136
- #ifdef __APPLE__
137
- // iOS: Convert std::string to NSString* for MMKVCore pod compatibility
138
- bool hasValue = instance->getBytes(@(key.c_str()), result);
139
- #else
140
- // Android/other platforms: Use std::string directly (converts to
141
- // std::string_view)
142
165
  bool hasValue = instance->getBytes(key, result);
143
- #endif
144
166
  if (hasValue) {
145
167
  return std::make_shared<ManagedMMBuffer>(std::move(result));
146
168
  } else {
147
169
  return std::nullopt;
148
170
  }
149
171
  }
172
+
150
173
  bool HybridMMKV::contains(const std::string& key) {
151
174
  return instance->containsKey(key);
152
175
  }
176
+
153
177
  bool HybridMMKV::remove(const std::string& key) {
154
178
  bool wasRemoved = instance->removeValueForKey(key);
155
179
  if (wasRemoved) {
@@ -158,9 +182,11 @@ bool HybridMMKV::remove(const std::string& key) {
158
182
  }
159
183
  return wasRemoved;
160
184
  }
185
+
161
186
  std::vector<std::string> HybridMMKV::getAllKeys() {
162
187
  return instance->allKeys();
163
188
  }
189
+
164
190
  void HybridMMKV::clearAll() {
165
191
  auto keysBefore = getAllKeys();
166
192
  instance->clearAll();
@@ -169,19 +195,30 @@ void HybridMMKV::clearAll() {
169
195
  MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
170
196
  }
171
197
  }
198
+
172
199
  void HybridMMKV::recrypt(const std::optional<std::string>& key) {
173
- bool successful = false;
174
200
  if (key.has_value()) {
175
- // Encrypt with the given key
176
- successful = instance->reKey(key.value());
201
+ encrypt(key.value(), std::nullopt);
177
202
  } else {
178
- // Remove the encryption key by setting it to a blank string
179
- successful = instance->reKey(std::string());
203
+ decrypt();
180
204
  }
205
+ }
206
+
207
+ void HybridMMKV::encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) {
208
+ bool isAes256Encryption = encryptionType == EncryptionType::AES_256;
209
+ bool successful = instance->reKey(key, isAes256Encryption);
181
210
  if (!successful) {
182
- throw std::runtime_error("Failed to recrypt MMKV instance!");
211
+ throw std::runtime_error("Failed to encrypt MMKV instance!");
183
212
  }
184
213
  }
214
+
215
+ void HybridMMKV::decrypt() {
216
+ bool successful = instance->reKey("");
217
+ if (!successful) [[unlikely]] {
218
+ throw std::runtime_error("Failed to decrypt MMKV instance!");
219
+ }
220
+ }
221
+
185
222
  void HybridMMKV::trim() {
186
223
  instance->trim();
187
224
  instance->clearMemoryCache();
@@ -190,7 +227,7 @@ void HybridMMKV::trim() {
190
227
  Listener HybridMMKV::addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) {
191
228
  // Add listener
192
229
  auto mmkvID = instance->mmapID();
193
- auto listenerID = MMKVValueChangedListenerRegistry::addListener(instance->mmapID(), onValueChanged);
230
+ auto listenerID = MMKVValueChangedListenerRegistry::addListener(mmkvID, onValueChanged);
194
231
 
195
232
  return Listener([=]() {
196
233
  // remove()
@@ -207,14 +244,13 @@ MMKVMode HybridMMKV::getMMKVMode(const Configuration& config) {
207
244
  return ::mmkv::MMKV_SINGLE_PROCESS;
208
245
  case Mode::MULTI_PROCESS:
209
246
  return ::mmkv::MMKV_MULTI_PROCESS;
210
- default:
211
- [[unlikely]] throw std::runtime_error("Invalid MMKV Mode value!");
212
247
  }
248
+ throw std::runtime_error("Invalid MMKV Mode value!");
213
249
  }
214
250
 
215
251
  double HybridMMKV::importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) {
216
252
  auto hybridMMKV = std::dynamic_pointer_cast<HybridMMKV>(other);
217
- if (hybridMMKV == nullptr) {
253
+ if (hybridMMKV == nullptr) [[unlikely]] {
218
254
  throw std::runtime_error("The given `MMKV` instance is not of type `HybridMMKV`!");
219
255
  }
220
256
 
@@ -21,7 +21,10 @@ public:
21
21
  // Properties
22
22
  std::string getId() override;
23
23
  double getSize() override;
24
+ double getByteSize() override;
25
+ double getLength() override;
24
26
  bool getIsReadOnly() override;
27
+ bool getIsEncrypted() override;
25
28
 
26
29
  public:
27
30
  // Methods
@@ -35,6 +38,8 @@ public:
35
38
  std::vector<std::string> getAllKeys() override;
36
39
  void clearAll() override;
37
40
  void recrypt(const std::optional<std::string>& key) override;
41
+ void encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) override;
42
+ void decrypt() override;
38
43
  void trim() override;
39
44
  Listener addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) override;
40
45
  double importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) override;
@@ -18,11 +18,7 @@ std::string HybridMMKVFactory::getDefaultMMKVInstanceId() {
18
18
  void HybridMMKVFactory::initializeMMKV(const std::string& rootPath) {
19
19
  Logger::log(LogLevel::Info, TAG, "Initializing MMKV with rootPath=%s", rootPath.c_str());
20
20
 
21
- #ifdef NITRO_DEBUG
22
- MMKVLogLevel logLevel = ::mmkv::MMKVLogDebug;
23
- #else
24
- MMKVLogLevel logLevel = ::mmkv::MMKVLogWarning;
25
- #endif
21
+ MMKVLogLevel logLevel = static_cast<MMKVLogLevel>(MMKV_LOG_LEVEL);
26
22
  MMKV::initializeMMKV(rootPath, logLevel);
27
23
  }
28
24
 
package/cpp/MMKVTypes.hpp CHANGED
@@ -48,3 +48,15 @@ constexpr auto MMKV_READ_ONLY = ::MMKVMode::MMKV_READ_ONLY;
48
48
  */
49
49
 
50
50
  using namespace mmkv;
51
+
52
+ // Default MMKV log level if not configured at build time
53
+ #ifndef MMKV_LOG_LEVEL
54
+ #ifdef NITRO_DEBUG
55
+ #define MMKV_LOG_LEVEL 0
56
+ #else
57
+ #define MMKV_LOG_LEVEL 3
58
+ #endif
59
+ #endif
60
+ #if MMKV_LOG_LEVEL < 0 || MMKV_LOG_LEVEL > 4
61
+ #error "MMKV_LOG_LEVEL must be between 0 (Debug) and 4 (None)"
62
+ #endif
@@ -15,7 +15,7 @@ namespace margelo::nitro::mmkv {
15
15
  /**
16
16
  An ArrayBuffer subclass that manages MMBuffer memory (by ownership).
17
17
  */
18
- class ManagedMMBuffer : public ArrayBuffer {
18
+ class ManagedMMBuffer final : public ArrayBuffer {
19
19
  public:
20
20
  explicit ManagedMMBuffer(MMBuffer&& buffer) : _buffer(std::move(buffer)) {}
21
21
 
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Button, Text } from 'react-native';
3
- import { act, fireEvent, render, renderHook, screen, cleanup, } from '@testing-library/react-native';
3
+ import { act, fireEvent, render, renderHook, screen, cleanup, waitFor, } from '@testing-library/react-native';
4
4
  import { createMMKV, useMMKVNumber, useMMKVString } from '..';
5
5
  const mmkv = createMMKV();
6
6
  beforeEach(() => {
@@ -67,3 +67,32 @@ test('functional updates to hooks', () => {
67
67
  '4',
68
68
  ]);
69
69
  });
70
+ test('useMMKV hook does not miss updates that happen during subscription setup', async () => {
71
+ const raceKey = 'race-key';
72
+ const raceMMKV = createMMKV();
73
+ let simulatedRaceDone = false;
74
+ const originalSubscribe = raceMMKV.addOnValueChangedListener.bind(raceMMKV);
75
+ raceMMKV.addOnValueChangedListener = ((listener) => {
76
+ if (!simulatedRaceDone) {
77
+ simulatedRaceDone = true;
78
+ raceMMKV.set(raceKey, 'updated-before-subscribe');
79
+ }
80
+ return originalSubscribe(listener);
81
+ });
82
+ const { result } = renderHook(() => useMMKVString(raceKey, raceMMKV));
83
+ await waitFor(() => {
84
+ expect(result.current[0]).toBe('updated-before-subscribe');
85
+ });
86
+ });
87
+ test('useMMKV hook stays consistent during rapid updates', async () => {
88
+ const raceKey = 'rapid-key';
89
+ const { result } = renderHook(() => useMMKVNumber(raceKey, mmkv));
90
+ act(() => {
91
+ for (let i = 1; i <= 100; i++) {
92
+ mmkv.set(raceKey, i);
93
+ }
94
+ });
95
+ await waitFor(() => {
96
+ expect(result.current[0]).toBe(100);
97
+ });
98
+ });
@@ -24,8 +24,18 @@ export function createMMKV(config = { id: 'mmkv.default' }) {
24
24
  };
25
25
  return {
26
26
  id: config.id,
27
- size: 0,
27
+ get length() {
28
+ return getLocalStorage().length;
29
+ },
30
+ get size() {
31
+ return this.byteSize;
32
+ },
33
+ get byteSize() {
34
+ // estimate - assumes UTF8
35
+ return JSON.stringify(getLocalStorage()).length;
36
+ },
28
37
  isReadOnly: false,
38
+ isEncrypted: false,
29
39
  clearAll: () => {
30
40
  const storage = getLocalStorage();
31
41
  const keys = Object.keys(storage);
@@ -90,6 +100,12 @@ export function createMMKV(config = { id: 'mmkv.default' }) {
90
100
  recrypt: () => {
91
101
  throw new Error('`recrypt(..)` is not supported on Web!');
92
102
  },
103
+ encrypt: () => {
104
+ throw new Error('`encrypt(..)` is not supported on Web!');
105
+ },
106
+ decrypt: () => {
107
+ throw new Error('`decrypt(..)` is not supported on Web!');
108
+ },
93
109
  trim: () => {
94
110
  // no-op
95
111
  },
@@ -11,10 +11,18 @@ export function createMockMMKV(config = { id: 'mmkv.default' }) {
11
11
  };
12
12
  return {
13
13
  id: config.id,
14
- get size() {
14
+ get length() {
15
15
  return storage.size;
16
16
  },
17
+ get size() {
18
+ return this.byteSize;
19
+ },
20
+ get byteSize() {
21
+ // estimate - assumes UTF8
22
+ return JSON.stringify(storage).length;
23
+ },
17
24
  isReadOnly: false,
25
+ isEncrypted: false,
18
26
  clearAll: () => {
19
27
  const keysBefore = storage.keys();
20
28
  storage.clear();
@@ -57,6 +65,12 @@ export function createMockMMKV(config = { id: 'mmkv.default' }) {
57
65
  recrypt: () => {
58
66
  console.warn('Encryption is not supported in mocked MMKV instances!');
59
67
  },
68
+ encrypt: () => {
69
+ console.warn('Encryption is not supported in mocked MMKV instances!');
70
+ },
71
+ decrypt: () => {
72
+ console.warn('Encryption is not supported in mocked MMKV instances!');
73
+ },
60
74
  trim: () => {
61
75
  // no-op
62
76
  },
@@ -1,16 +1,16 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import { useCallback, useSyncExternalStore } from 'react';
2
2
  import { getDefaultMMKVInstance } from '../createMMKV/getDefaultMMKVInstance';
3
3
  export function createMMKVHook(getter) {
4
4
  return (key, instance) => {
5
5
  const mmkv = instance ?? getDefaultMMKVInstance();
6
- const [bump, setBump] = useState(0);
7
- const value = useMemo(() => {
8
- // bump is here as an additional outside dependency, so this useMemo
9
- // re-computes the value each time bump changes, effectively acting as a hint
10
- // that the outside value (storage) has changed. setting bump refreshes this value.
11
- bump;
12
- return getter(mmkv, key);
13
- }, [mmkv, key, bump]);
6
+ const value = useSyncExternalStore(useCallback((onStoreChange) => {
7
+ const listener = mmkv.addOnValueChangedListener((changedKey) => {
8
+ if (changedKey === key) {
9
+ onStoreChange();
10
+ }
11
+ });
12
+ return () => listener.remove();
13
+ }, [key, mmkv]), useCallback(() => getter(mmkv, key), [key, mmkv]), useCallback(() => getter(mmkv, key), [key, mmkv]));
14
14
  // update value by user set
15
15
  const set = useCallback((v) => {
16
16
  const newValue = typeof v === 'function' ? v(getter(mmkv, key)) : v;
@@ -35,15 +35,6 @@ export function createMMKVHook(getter) {
35
35
  throw new Error(`MMKV: Type ${typeof newValue} is not supported!`);
36
36
  }
37
37
  }, [key, mmkv]);
38
- // update value if it changes somewhere else (second hook, same key)
39
- useEffect(() => {
40
- const listener = mmkv.addOnValueChangedListener((changedKey) => {
41
- if (changedKey === key) {
42
- setBump((b) => b + 1);
43
- }
44
- });
45
- return () => listener.remove();
46
- }, [key, mmkv]);
47
38
  return [value, set];
48
39
  };
49
40
  }
@@ -1,4 +1,5 @@
1
1
  import type { HybridObject } from 'react-native-nitro-modules';
2
+ import type { EncryptionType } from './MMKVFactory.nitro';
2
3
  export interface Listener {
3
4
  remove: () => void;
4
5
  }
@@ -10,15 +11,31 @@ export interface MMKV extends HybridObject<{
10
11
  * Get the ID of this {@linkcode MMKV} instance.
11
12
  */
12
13
  readonly id: string;
14
+ /**
15
+ * Get the current amount of key/value pairs stored in
16
+ * this storage.
17
+ */
18
+ readonly length: number;
13
19
  /**
14
20
  * Get the current total size of the storage, in bytes.
21
+ * @deprecated Use {@linkcode byteSize} instead.
15
22
  */
16
23
  readonly size: number;
17
24
  /**
18
- * Returns whether this instance is in read-only mode or not.
25
+ * Get the current total size of the storage, in bytes.
26
+ */
27
+ readonly byteSize: number;
28
+ /**
29
+ * Get whether this instance is in read-only mode or not.
19
30
  * If this is `true`, you can only use "get"-functions.
20
31
  */
21
32
  readonly isReadOnly: boolean;
33
+ /**
34
+ * Get whether this instance is encrypted, or not.
35
+ * @see {@linkcode encrypt | encrypt(...)}
36
+ * @see {@linkcode decrypt | decrypt()}
37
+ */
38
+ readonly isEncrypted: boolean;
22
39
  /**
23
40
  * Set a {@linkcode value} for the given {@linkcode key}.
24
41
  *
@@ -77,8 +94,36 @@ export interface MMKV extends HybridObject<{
77
94
  * Encryption keys can have a maximum length of 16 bytes.
78
95
  *
79
96
  * @throws an Error if the instance cannot be recrypted.
97
+ * @deprecated Use {@linkcode encrypt | encrypt(...)} or {@linkcode decrypt | decrypt()} instead.
80
98
  */
81
99
  recrypt(key: string | undefined): void;
100
+ /**
101
+ * Encrypts the data in this MMKV instance with the
102
+ * given {@linkcode key} and {@linkcode encryptionType}.
103
+ *
104
+ * If this MMKV instance is already encrypted ({@linkcode isEncrypted}),
105
+ * it will re-encrypt the data.
106
+ *
107
+ * Future attempts to open this MMKV instance will require the same
108
+ * {@linkcode key} and {@linkcode encryptionType}, otherwise reads
109
+ * will fail.
110
+ *
111
+ * The {@linkcode key} must be 16-bytes in {@linkcode EncryptionType | 'AES-128'}
112
+ * encryption (the default), or 32-bytes in {@linkcode EncryptionType | 'AES-256'}
113
+ * encryption.
114
+ *
115
+ * @param key The encryption key to use. In AES-128 this must be 16-bytes, in AES-256 it must be 32-bytes long.
116
+ * @param encryptionType The encryption type to use. Default: AES-128
117
+ */
118
+ encrypt(key: string, encryptionType?: EncryptionType): void;
119
+ /**
120
+ * Decrypts the data in this MMKV instance and removes
121
+ * the encryption key.
122
+ *
123
+ * Future attempts to open this MMKV instance must be done
124
+ * without an encryption key, as it is now plain-text.
125
+ */
126
+ decrypt(): void;
82
127
  /**
83
128
  * Trims the storage space and clears memory cache.
84
129
  *