Skip to content

Commit 46e1adf

Browse files
committed
feat: bridge external MMKV content changes
1 parent 1ea920e commit 46e1adf

18 files changed

Lines changed: 446 additions & 117 deletions

docs/LISTENERS.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,38 @@ const listener = storage.addOnValueChangedListener((changedKey) => {
1313
})
1414
```
1515

16+
### Listen to native changes from another process.
17+
18+
On native platforms, MMKV can detect changes from another process, such as an app
19+
extension, App Clip, or background service. These changes are routed through
20+
`addOnValueChangedListener(...)` as well.
21+
22+
MMKV does not report the exact changed key for external changes, so React Native
23+
MMKV conservatively notifies listeners for all currently known keys and all
24+
previously known keys. This ensures value hooks and `useMMKVKeys()` refresh for
25+
adds, updates, and deletes.
26+
27+
```ts
28+
const storage = createMMKV({
29+
id: 'shared-storage',
30+
mode: 'multi-process',
31+
})
32+
33+
const listener = storage.addOnValueChangedListener((changedKey) => {
34+
const value = storage.getString(changedKey)
35+
console.log(`"${changedKey}" might have changed: ${value}`)
36+
})
37+
```
38+
39+
You can also force MMKV to check for external changes:
40+
41+
```ts
42+
storage.checkExternalContentChanged()
43+
```
44+
45+
The built-in `useMMKV*` value hooks and `useMMKVKeys()` automatically refresh
46+
when this native event fires.
47+
1648
Don't forget to remove the listener when no longer needed. For example, when the user logs out:
1749

1850
```ts

example/__tests__/MMKV.harness.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,17 @@ describe('MMKV Listeners & Observers', () => {
11611161
listener.remove();
11621162
});
11631163
});
1164+
1165+
describe('External Content Change Checks', () => {
1166+
it('should allow manually checking for external content changes', async () => {
1167+
storage.checkExternalContentChanged();
1168+
storage.set('external-api-test', 'value');
1169+
1170+
await waitForNextTick();
1171+
1172+
expect(storage.getString('external-api-test')).toStrictEqual('value');
1173+
});
1174+
});
11641175
});
11651176

11661177
describe('Deleting instances and checking if they exist', () => {

packages/react-native-mmkv/cpp/HybridMMKV.cpp

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
//
77

88
#include "HybridMMKV.hpp"
9+
#include "MMKVListenerRegistry.hpp"
910
#include "MMKVTypes.hpp"
10-
#include "MMKVValueChangedListenerRegistry.hpp"
1111
#include "ManagedMMBuffer.hpp"
1212
#include <NitroModules/NitroLogger.hpp>
1313

@@ -64,6 +64,8 @@ HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) {
6464

6565
throw std::runtime_error("Failed to create MMKV instance!");
6666
}
67+
68+
MMKVListenerRegistry::registerInstance(instance);
6769
}
6870

6971
std::string HybridMMKV::getId() {
@@ -127,7 +129,7 @@ void HybridMMKV::set(const std::string& key, const std::variant<bool, std::share
127129
}
128130

129131
// Notify on changed
130-
MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
132+
MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
131133
}
132134

133135
std::optional<bool> HybridMMKV::getBoolean(const std::string& key) {
@@ -178,7 +180,7 @@ bool HybridMMKV::remove(const std::string& key) {
178180
bool wasRemoved = instance->removeValueForKey(key);
179181
if (wasRemoved) {
180182
// Notify on changed
181-
MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
183+
MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
182184
}
183185
return wasRemoved;
184186
}
@@ -192,7 +194,7 @@ void HybridMMKV::clearAll() {
192194
instance->clearAll();
193195
for (const auto& key : keysBefore) {
194196
// Notify on changed
195-
MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
197+
MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
196198
}
197199
}
198200

@@ -224,14 +226,18 @@ void HybridMMKV::trim() {
224226
instance->clearMemoryCache();
225227
}
226228

229+
void HybridMMKV::checkExternalContentChanged() {
230+
instance->checkContentChanged();
231+
}
232+
227233
Listener HybridMMKV::addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) {
228234
// Add listener
229235
auto mmkvID = instance->mmapID();
230-
auto listenerID = MMKVValueChangedListenerRegistry::addListener(mmkvID, onValueChanged);
236+
auto listenerID = MMKVListenerRegistry::addValueChangedListener(mmkvID, onValueChanged);
231237

232238
return Listener([=]() {
233239
// remove()
234-
MMKVValueChangedListenerRegistry::removeListener(mmkvID, listenerID);
240+
MMKVListenerRegistry::removeValueChangedListener(mmkvID, listenerID);
235241
});
236242
}
237243

packages/react-native-mmkv/cpp/HybridMMKV.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class HybridMMKV final : public HybridMMKVSpec {
4141
void encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) override;
4242
void decrypt() override;
4343
void trim() override;
44+
void checkExternalContentChanged() override;
4445
Listener addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) override;
4546
double importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) override;
4647

packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
#include "HybridMMKVFactory.hpp"
99
#include "HybridMMKV.hpp"
10+
#include "MMKVGlobalHandler.hpp"
11+
#include "MMKVListenerRegistry.hpp"
1012
#include "MMKVTypes.hpp"
1113

1214
namespace margelo::nitro::mmkv {
@@ -19,14 +21,15 @@ void HybridMMKVFactory::initializeMMKV(const std::string& rootPath) {
1921
Logger::log(LogLevel::Info, TAG, "Initializing MMKV with rootPath=%s", rootPath.c_str());
2022

2123
MMKVLogLevel logLevel = static_cast<MMKVLogLevel>(MMKV_LOG_LEVEL);
22-
MMKV::initializeMMKV(rootPath, logLevel);
24+
MMKV::initializeMMKV(rootPath, logLevel, &MMKVGlobalHandler::shared());
2325
}
2426

2527
std::shared_ptr<HybridMMKVSpec> HybridMMKVFactory::createMMKV(const Configuration& configuration) {
2628
return std::make_shared<HybridMMKV>(configuration);
2729
}
2830

2931
bool HybridMMKVFactory::deleteMMKV(const std::string& id) {
32+
MMKVListenerRegistry::unregisterInstance(id);
3033
return MMKV::removeStorage(id);
3134
}
3235

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// MMKVGlobalHandler.cpp
3+
// react-native-mmkv
4+
//
5+
// Created by Marc Rousavy on 03.06.2026.
6+
//
7+
8+
#include "MMKVGlobalHandler.hpp"
9+
#include "MMKVListenerRegistry.hpp"
10+
#include <NitroModules/NitroLogger.hpp>
11+
12+
namespace margelo::nitro::mmkv {
13+
14+
namespace {
15+
16+
LogLevel getNitroLogLevel(::mmkv::MMKVLogLevel level) {
17+
switch (level) {
18+
case ::mmkv::MMKVLogDebug:
19+
return LogLevel::Debug;
20+
case ::mmkv::MMKVLogInfo:
21+
return LogLevel::Info;
22+
case ::mmkv::MMKVLogWarning:
23+
return LogLevel::Warning;
24+
case ::mmkv::MMKVLogError:
25+
case ::mmkv::MMKVLogNone:
26+
return LogLevel::Error;
27+
}
28+
return LogLevel::Error;
29+
}
30+
31+
} // namespace
32+
33+
MMKVGlobalHandler& MMKVGlobalHandler::shared() {
34+
static MMKVGlobalHandler handler;
35+
return handler;
36+
}
37+
38+
void MMKVGlobalHandler::mmkvLog(::mmkv::MMKVLogLevel level, const char* file, int line, const char* function, ::mmkv::MMKVLog_t message) {
39+
auto logMessage = getLogMessage(message);
40+
Logger::log(getNitroLogLevel(level), "MMKV", "%s:%d %s: %s", file != nullptr ? file : "", line, function != nullptr ? function : "",
41+
logMessage.c_str());
42+
}
43+
44+
::mmkv::MMKVRecoverStrategic MMKVGlobalHandler::onMMKVCRCCheckFail(const std::string& /* mmapID */) {
45+
return ::mmkv::OnErrorDiscard;
46+
}
47+
48+
::mmkv::MMKVRecoverStrategic MMKVGlobalHandler::onMMKVFileLengthError(const std::string& /* mmapID */) {
49+
return ::mmkv::OnErrorDiscard;
50+
}
51+
52+
void MMKVGlobalHandler::onContentChangedByOuterProcess(const std::string& mmapID) {
53+
MMKVListenerRegistry::notifyOnExternalContentChanged(mmapID);
54+
}
55+
56+
void MMKVGlobalHandler::onMMKVContentLoadSuccessfully(const std::string& /* mmapID */) {
57+
// no-op
58+
}
59+
60+
std::string MMKVGlobalHandler::getLogMessage(::mmkv::MMKVLog_t message) {
61+
#if defined(__ANDROID__)
62+
return message;
63+
#elif defined(__OBJC__)
64+
if (message == nullptr) {
65+
return "";
66+
}
67+
return std::string([message UTF8String]);
68+
#else
69+
return "";
70+
#endif
71+
}
72+
73+
} // namespace margelo::nitro::mmkv
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// MMKVGlobalHandler.hpp
3+
// react-native-mmkv
4+
//
5+
// Created by Marc Rousavy on 03.06.2026.
6+
//
7+
8+
#pragma once
9+
10+
#include "MMKVTypes.hpp"
11+
12+
namespace margelo::nitro::mmkv {
13+
14+
class MMKVGlobalHandler final : public ::mmkv::MMKVHandler {
15+
public:
16+
static MMKVGlobalHandler& shared();
17+
18+
public:
19+
void mmkvLog(::mmkv::MMKVLogLevel level, const char* file, int line, const char* function, ::mmkv::MMKVLog_t message) override;
20+
::mmkv::MMKVRecoverStrategic onMMKVCRCCheckFail(const std::string& mmapID) override;
21+
::mmkv::MMKVRecoverStrategic onMMKVFileLengthError(const std::string& mmapID) override;
22+
void onContentChangedByOuterProcess(const std::string& mmapID) override;
23+
void onMMKVContentLoadSuccessfully(const std::string& mmapID) override;
24+
25+
private:
26+
MMKVGlobalHandler() = default;
27+
28+
private:
29+
static std::string getLogMessage(::mmkv::MMKVLog_t message);
30+
};
31+
32+
} // namespace margelo::nitro::mmkv

0 commit comments

Comments
 (0)