diff --git a/docs/LISTENERS.md b/docs/LISTENERS.md index df190e4a..6ea46daa 100644 --- a/docs/LISTENERS.md +++ b/docs/LISTENERS.md @@ -13,6 +13,38 @@ const listener = storage.addOnValueChangedListener((changedKey) => { }) ``` +### Listen to native changes from another process. + +On native platforms, MMKV can detect changes from another process, such as an app +extension, App Clip, or background service. These changes are routed through +`addOnValueChangedListener(...)` as well. + +MMKV does not report the exact changed key for external changes, so React Native +MMKV conservatively notifies listeners for all currently known keys and all +previously known keys. This ensures value hooks and `useMMKVKeys()` refresh for +adds, updates, and deletes. + +```ts +const storage = createMMKV({ + id: 'shared-storage', + mode: 'multi-process', +}) + +const listener = storage.addOnValueChangedListener((changedKey) => { + const value = storage.getString(changedKey) + console.log(`"${changedKey}" might have changed: ${value}`) +}) +``` + +You can also force MMKV to check for external changes: + +```ts +storage.checkExternalContentChanged() +``` + +The built-in `useMMKV*` value hooks and `useMMKVKeys()` automatically refresh +when this native event fires. + Don't forget to remove the listener when no longer needed. For example, when the user logs out: ```ts diff --git a/example/__tests__/MMKV.harness.ts b/example/__tests__/MMKV.harness.ts index 59718dca..ceeae24a 100644 --- a/example/__tests__/MMKV.harness.ts +++ b/example/__tests__/MMKV.harness.ts @@ -1161,6 +1161,17 @@ describe('MMKV Listeners & Observers', () => { listener.remove(); }); }); + + describe('External Content Change Checks', () => { + it('should allow manually checking for external content changes', async () => { + storage.checkExternalContentChanged(); + storage.set('external-api-test', 'value'); + + await waitForNextTick(); + + expect(storage.getString('external-api-test')).toStrictEqual('value'); + }); + }); }); describe('Deleting instances and checking if they exist', () => { diff --git a/packages/react-native-mmkv/cpp/HybridMMKV.cpp b/packages/react-native-mmkv/cpp/HybridMMKV.cpp index 1086ea5c..340c0e3a 100644 --- a/packages/react-native-mmkv/cpp/HybridMMKV.cpp +++ b/packages/react-native-mmkv/cpp/HybridMMKV.cpp @@ -6,8 +6,8 @@ // #include "HybridMMKV.hpp" +#include "MMKVListenerRegistry.hpp" #include "MMKVTypes.hpp" -#include "MMKVValueChangedListenerRegistry.hpp" #include "ManagedMMBuffer.hpp" #include @@ -64,6 +64,8 @@ HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) { throw std::runtime_error("Failed to create MMKV instance!"); } + + MMKVListenerRegistry::registerInstance(instance); } std::string HybridMMKV::getId() { @@ -127,7 +129,7 @@ void HybridMMKV::set(const std::string& key, const std::variantmmapID(), key); + MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key); } std::optional HybridMMKV::getBoolean(const std::string& key) { @@ -178,7 +180,7 @@ bool HybridMMKV::remove(const std::string& key) { bool wasRemoved = instance->removeValueForKey(key); if (wasRemoved) { // Notify on changed - MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key); + MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key); } return wasRemoved; } @@ -192,7 +194,7 @@ void HybridMMKV::clearAll() { instance->clearAll(); for (const auto& key : keysBefore) { // Notify on changed - MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key); + MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key); } } @@ -224,14 +226,18 @@ void HybridMMKV::trim() { instance->clearMemoryCache(); } +void HybridMMKV::checkExternalContentChanged() { + instance->checkContentChanged(); +} + Listener HybridMMKV::addOnValueChangedListener(const std::function& onValueChanged) { // Add listener auto mmkvID = instance->mmapID(); - auto listenerID = MMKVValueChangedListenerRegistry::addListener(mmkvID, onValueChanged); + auto listenerID = MMKVListenerRegistry::addValueChangedListener(mmkvID, onValueChanged); return Listener([=]() { // remove() - MMKVValueChangedListenerRegistry::removeListener(mmkvID, listenerID); + MMKVListenerRegistry::removeValueChangedListener(mmkvID, listenerID); }); } diff --git a/packages/react-native-mmkv/cpp/HybridMMKV.hpp b/packages/react-native-mmkv/cpp/HybridMMKV.hpp index 4c8053a1..1b00aa95 100644 --- a/packages/react-native-mmkv/cpp/HybridMMKV.hpp +++ b/packages/react-native-mmkv/cpp/HybridMMKV.hpp @@ -41,6 +41,7 @@ class HybridMMKV final : public HybridMMKVSpec { void encrypt(const std::string& key, std::optional encryptionType) override; void decrypt() override; void trim() override; + void checkExternalContentChanged() override; Listener addOnValueChangedListener(const std::function& onValueChanged) override; double importAllFrom(const std::shared_ptr& other) override; diff --git a/packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp b/packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp index e93be0b7..11bdbb20 100644 --- a/packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp +++ b/packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp @@ -7,6 +7,8 @@ #include "HybridMMKVFactory.hpp" #include "HybridMMKV.hpp" +#include "MMKVGlobalHandler.hpp" +#include "MMKVListenerRegistry.hpp" #include "MMKVTypes.hpp" namespace margelo::nitro::mmkv { @@ -19,7 +21,7 @@ void HybridMMKVFactory::initializeMMKV(const std::string& rootPath) { Logger::log(LogLevel::Info, TAG, "Initializing MMKV with rootPath=%s", rootPath.c_str()); MMKVLogLevel logLevel = static_cast(MMKV_LOG_LEVEL); - MMKV::initializeMMKV(rootPath, logLevel); + MMKV::initializeMMKV(rootPath, logLevel, &MMKVGlobalHandler::shared()); } std::shared_ptr HybridMMKVFactory::createMMKV(const Configuration& configuration) { @@ -27,6 +29,7 @@ std::shared_ptr HybridMMKVFactory::createMMKV(const Configuratio } bool HybridMMKVFactory::deleteMMKV(const std::string& id) { + MMKVListenerRegistry::unregisterInstance(id); return MMKV::removeStorage(id); } diff --git a/packages/react-native-mmkv/cpp/MMKVGlobalHandler.cpp b/packages/react-native-mmkv/cpp/MMKVGlobalHandler.cpp new file mode 100644 index 00000000..afdc9752 --- /dev/null +++ b/packages/react-native-mmkv/cpp/MMKVGlobalHandler.cpp @@ -0,0 +1,73 @@ +// +// MMKVGlobalHandler.cpp +// react-native-mmkv +// +// Created by Marc Rousavy on 03.06.2026. +// + +#include "MMKVGlobalHandler.hpp" +#include "MMKVListenerRegistry.hpp" +#include + +namespace margelo::nitro::mmkv { + +namespace { + + LogLevel getNitroLogLevel(::mmkv::MMKVLogLevel level) { + switch (level) { + case ::mmkv::MMKVLogDebug: + return LogLevel::Debug; + case ::mmkv::MMKVLogInfo: + return LogLevel::Info; + case ::mmkv::MMKVLogWarning: + return LogLevel::Warning; + case ::mmkv::MMKVLogError: + case ::mmkv::MMKVLogNone: + return LogLevel::Error; + } + return LogLevel::Error; + } + +} // namespace + +MMKVGlobalHandler& MMKVGlobalHandler::shared() { + static MMKVGlobalHandler handler; + return handler; +} + +void MMKVGlobalHandler::mmkvLog(::mmkv::MMKVLogLevel level, const char* file, int line, const char* function, ::mmkv::MMKVLog_t message) { + auto logMessage = getLogMessage(message); + Logger::log(getNitroLogLevel(level), "MMKV", "%s:%d %s: %s", file != nullptr ? file : "", line, function != nullptr ? function : "", + logMessage.c_str()); +} + +::mmkv::MMKVRecoverStrategic MMKVGlobalHandler::onMMKVCRCCheckFail(const std::string& /* mmapID */) { + return ::mmkv::OnErrorDiscard; +} + +::mmkv::MMKVRecoverStrategic MMKVGlobalHandler::onMMKVFileLengthError(const std::string& /* mmapID */) { + return ::mmkv::OnErrorDiscard; +} + +void MMKVGlobalHandler::onContentChangedByOuterProcess(const std::string& mmapID) { + MMKVListenerRegistry::notifyOnExternalContentChanged(mmapID); +} + +void MMKVGlobalHandler::onMMKVContentLoadSuccessfully(const std::string& /* mmapID */) { + // no-op +} + +std::string MMKVGlobalHandler::getLogMessage(::mmkv::MMKVLog_t message) { +#if defined(__ANDROID__) + return message; +#elif defined(__OBJC__) + if (message == nullptr) { + return ""; + } + return std::string([message UTF8String]); +#else + return ""; +#endif +} + +} // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/cpp/MMKVGlobalHandler.hpp b/packages/react-native-mmkv/cpp/MMKVGlobalHandler.hpp new file mode 100644 index 00000000..d39ce21b --- /dev/null +++ b/packages/react-native-mmkv/cpp/MMKVGlobalHandler.hpp @@ -0,0 +1,32 @@ +// +// MMKVGlobalHandler.hpp +// react-native-mmkv +// +// Created by Marc Rousavy on 03.06.2026. +// + +#pragma once + +#include "MMKVTypes.hpp" + +namespace margelo::nitro::mmkv { + +class MMKVGlobalHandler final : public ::mmkv::MMKVHandler { +public: + static MMKVGlobalHandler& shared(); + +public: + void mmkvLog(::mmkv::MMKVLogLevel level, const char* file, int line, const char* function, ::mmkv::MMKVLog_t message) override; + ::mmkv::MMKVRecoverStrategic onMMKVCRCCheckFail(const std::string& mmapID) override; + ::mmkv::MMKVRecoverStrategic onMMKVFileLengthError(const std::string& mmapID) override; + void onContentChangedByOuterProcess(const std::string& mmapID) override; + void onMMKVContentLoadSuccessfully(const std::string& mmapID) override; + +private: + MMKVGlobalHandler() = default; + +private: + static std::string getLogMessage(::mmkv::MMKVLog_t message); +}; + +} // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/cpp/MMKVListenerRegistry.cpp b/packages/react-native-mmkv/cpp/MMKVListenerRegistry.cpp new file mode 100644 index 00000000..b41569f0 --- /dev/null +++ b/packages/react-native-mmkv/cpp/MMKVListenerRegistry.cpp @@ -0,0 +1,164 @@ +// +// MMKVListenerRegistry.cpp +// react-native-mmkv +// +// Created by Marc Rousavy on 21.08.2025. +// + +#include "MMKVListenerRegistry.hpp" +#include +#include + +namespace margelo::nitro::mmkv { + +// static members +std::atomic MMKVListenerRegistry::_listenersCounter = 0; +std::mutex MMKVListenerRegistry::_mutex; +std::unordered_map MMKVListenerRegistry::_instances; +std::unordered_map> MMKVListenerRegistry::_valueChangedListeners; + +void MMKVListenerRegistry::registerInstance(MMKV* instance) { + auto mmkvID = instance->mmapID(); + auto keys = getAllKeys(instance); + + std::lock_guard lock(_mutex); + auto entry = _instances.find(mmkvID); + if (entry == _instances.end()) { + _instances.emplace(mmkvID, MMKVInstanceCache{.instance = instance, .knownKeys = std::move(keys)}); + } else { + entry->second.instance = instance; + } +} + +void MMKVListenerRegistry::unregisterInstance(const std::string& mmkvID) { + std::lock_guard lock(_mutex); + _instances.erase(mmkvID); + _valueChangedListeners.erase(mmkvID); +} + +ListenerID MMKVListenerRegistry::addValueChangedListener(const std::string& mmkvID, + const std::function& callback) { + auto id = _listenersCounter.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(_mutex); + + auto& listeners = _valueChangedListeners[mmkvID]; + listeners.push_back(ValueChangedListenerSubscription{ + .id = id, + .callback = callback, + }); + return id; +} + +void MMKVListenerRegistry::removeValueChangedListener(const std::string& mmkvID, ListenerID id) { + std::lock_guard lock(_mutex); + + auto entry = _valueChangedListeners.find(mmkvID); + if (entry == _valueChangedListeners.end()) { + return; + } + + auto& listeners = entry->second; + listeners.erase( + std::remove_if(listeners.begin(), listeners.end(), [id](const ValueChangedListenerSubscription& e) { return e.id == id; }), + listeners.end()); + if (listeners.empty()) { + _valueChangedListeners.erase(entry); + } +} + +void MMKVListenerRegistry::notifyOnValueChanged(const std::string& mmkvID, const std::string& key) { + MMKV* instance = nullptr; + { + std::lock_guard lock(_mutex); + auto instanceEntry = _instances.find(mmkvID); + if (instanceEntry != _instances.end()) { + instance = instanceEntry->second.instance; + } + } + + std::unordered_set keys; + if (instance != nullptr) { + keys = getAllKeys(instance); + } + + std::vector> callbacks; + { + std::lock_guard lock(_mutex); + auto instanceEntry = _instances.find(mmkvID); + if (instanceEntry != _instances.end() && instance != nullptr) { + instanceEntry->second.knownKeys = std::move(keys); + } + + auto entry = _valueChangedListeners.find(mmkvID); + if (entry == _valueChangedListeners.end()) { + return; + } + + callbacks.reserve(entry->second.size()); + for (const auto& listener : entry->second) { + callbacks.push_back(listener.callback); + } + } + + for (const auto& callback : callbacks) { + callback(key); + } +} + +void MMKVListenerRegistry::notifyOnExternalContentChanged(const std::string& mmkvID) { + MMKV* instance = nullptr; + std::unordered_set affectedKeys; + { + std::lock_guard lock(_mutex); + auto instanceEntry = _instances.find(mmkvID); + if (instanceEntry == _instances.end()) { + return; + } + + instance = instanceEntry->second.instance; + affectedKeys = instanceEntry->second.knownKeys; + } + + auto currentKeys = getAllKeys(instance); + affectedKeys.insert(currentKeys.begin(), currentKeys.end()); + + std::vector> callbacks; + std::vector keys; + { + std::lock_guard lock(_mutex); + auto instanceEntry = _instances.find(mmkvID); + if (instanceEntry == _instances.end()) { + return; + } + + instanceEntry->second.knownKeys = std::move(currentKeys); + + auto entry = _valueChangedListeners.find(mmkvID); + if (entry == _valueChangedListeners.end()) { + return; + } + + callbacks.reserve(entry->second.size()); + for (const auto& listener : entry->second) { + callbacks.push_back(listener.callback); + } + + keys.reserve(affectedKeys.size()); + for (const auto& key : affectedKeys) { + keys.push_back(key); + } + } + + for (const auto& callback : callbacks) { + for (const auto& key : keys) { + callback(key); + } + } +} + +std::unordered_set MMKVListenerRegistry::getAllKeys(MMKV* instance) { + auto keys = instance->allKeys(); + return std::unordered_set(keys.begin(), keys.end()); +} + +} // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/cpp/MMKVListenerRegistry.hpp b/packages/react-native-mmkv/cpp/MMKVListenerRegistry.hpp new file mode 100644 index 00000000..b4e9907a --- /dev/null +++ b/packages/react-native-mmkv/cpp/MMKVListenerRegistry.hpp @@ -0,0 +1,65 @@ +// +// MMKVListenerRegistry.hpp +// react-native-mmkv +// +// Created by Marc Rousavy on 21.08.2025. +// + +#pragma once + +#include "MMKVTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace margelo::nitro::mmkv { + +using ListenerID = size_t; +using MMKVID = std::string; + +struct ValueChangedListenerSubscription { + ListenerID id; + std::function callback; +}; + +struct MMKVInstanceCache { + MMKV* instance; + std::unordered_set knownKeys; +}; + +/** + * Listeners are tracked across instances - so we need an extra static class for + * the registry. + */ +class MMKVListenerRegistry final { +public: + MMKVListenerRegistry() = delete; + ~MMKVListenerRegistry() = delete; + +public: + static void registerInstance(MMKV* instance); + static void unregisterInstance(const std::string& mmkvID); + +public: + static ListenerID addValueChangedListener(const std::string& mmkvID, const std::function& callback); + static void removeValueChangedListener(const std::string& mmkvID, ListenerID id); + +public: + static void notifyOnValueChanged(const std::string& mmkvID, const std::string& key); + static void notifyOnExternalContentChanged(const std::string& mmkvID); + +private: + static std::atomic _listenersCounter; + static std::mutex _mutex; + static std::unordered_map _instances; + static std::unordered_map> _valueChangedListeners; + +private: + static std::unordered_set getAllKeys(MMKV* instance); +}; + +} // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/cpp/MMKVTypes.hpp b/packages/react-native-mmkv/cpp/MMKVTypes.hpp index 9a53a2ea..c4059759 100644 --- a/packages/react-native-mmkv/cpp/MMKVTypes.hpp +++ b/packages/react-native-mmkv/cpp/MMKVTypes.hpp @@ -17,6 +17,8 @@ namespace mmkv { using MMKV = ::MMKV; using MMKVMode = ::MMKVMode; using MMKVLogLevel = ::MMKVLogLevel; +using MMKVLog_t = ::MMKVLog_t; +using MMKVRecoverStrategic = ::MMKVRecoverStrategic; // Constants - bring into mmkv namespace constexpr auto MMKVLogDebug = ::MMKVLogDebug; @@ -24,6 +26,8 @@ constexpr auto MMKVLogInfo = ::MMKVLogInfo; constexpr auto MMKVLogWarning = ::MMKVLogWarning; constexpr auto MMKVLogError = ::MMKVLogError; constexpr auto MMKVLogNone = ::MMKVLogNone; +constexpr auto OnErrorDiscard = ::OnErrorDiscard; +constexpr auto OnErrorRecover = ::OnErrorRecover; constexpr auto MMKV_SINGLE_PROCESS = ::MMKV_SINGLE_PROCESS; constexpr auto MMKV_MULTI_PROCESS = ::MMKV_MULTI_PROCESS; @@ -33,6 +37,9 @@ constexpr auto MMKV_READ_ONLY = ::MMKVMode::MMKV_READ_ONLY; #else #include // iOS already has everything in mmkv:: namespace +namespace mmkv { +using MMKVLog_t = ::MMKVLog_t; +} // namespace mmkv #endif /** diff --git a/packages/react-native-mmkv/cpp/MMKVValueChangedListenerRegistry.cpp b/packages/react-native-mmkv/cpp/MMKVValueChangedListenerRegistry.cpp deleted file mode 100644 index 1f1b21a4..00000000 --- a/packages/react-native-mmkv/cpp/MMKVValueChangedListenerRegistry.cpp +++ /dev/null @@ -1,58 +0,0 @@ -// -// MMKVValueChangedListenerRegistry.cpp -// react-native-mmkv -// -// Created by Marc Rousavy on 21.08.2025. -// - -#include "MMKVValueChangedListenerRegistry.hpp" - -namespace margelo::nitro::mmkv { - -// static members -std::atomic MMKVValueChangedListenerRegistry::_listenersCounter = 0; -std::unordered_map> MMKVValueChangedListenerRegistry::_listeners; - -ListenerID MMKVValueChangedListenerRegistry::addListener(const std::string& mmkvID, - const std::function& callback) { - // 1. Get (or create) the listeners array for these MMKV instances - auto& listeners = _listeners[mmkvID]; - // 2. Get (and increment) the listener ID counter - auto id = _listenersCounter.fetch_add(1); - // 3. Add the listener to our array - listeners.push_back(ListenerSubscription{ - .id = id, - .callback = callback, - }); - // 4. Return the ID used to unsubscribe later on - return id; -} - -void MMKVValueChangedListenerRegistry::removeListener(const std::string& mmkvID, ListenerID id) { - // 1. Get the listeners array for these MMKV instances - auto entry = _listeners.find(mmkvID); - if (entry == _listeners.end()) { - // There's no more listeners for this instance anyways. - return; - } - // 2. Remove all listeners where the ID matches. Should only be one. - auto& listeners = entry->second; - listeners.erase(std::remove_if(listeners.begin(), listeners.end(), [id](const ListenerSubscription& e) { return e.id == id; }), - listeners.end()); -} - -void MMKVValueChangedListenerRegistry::notifyOnValueChanged(const std::string& mmkvID, const std::string& key) { - // 1. Get all listeners for the specific MMKV ID - auto entry = _listeners.find(mmkvID); - if (entry == _listeners.end()) { - // There are no listeners. Return - return; - } - // 2. Call each listener. - auto& listeners = entry->second; - for (const auto& listener : listeners) { - listener.callback(key); - } -} - -} // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/cpp/MMKVValueChangedListenerRegistry.hpp b/packages/react-native-mmkv/cpp/MMKVValueChangedListenerRegistry.hpp deleted file mode 100644 index 1ab1641e..00000000 --- a/packages/react-native-mmkv/cpp/MMKVValueChangedListenerRegistry.hpp +++ /dev/null @@ -1,43 +0,0 @@ -// -// MMKVValueChangedListenerRegistry.hpp -// react-native-mmkv -// -// Created by Marc Rousavy on 21.08.2025. -// - -#include "MMKVTypes.hpp" -#include -#include - -namespace margelo::nitro::mmkv { - -using ListenerID = size_t; -using MMKVID = std::string; - -struct ListenerSubscription { - ListenerID id; - std::function callback; -}; - -/** - * Listeners are tracked across instances - so we need an extra static class for - * the registry. - */ -class MMKVValueChangedListenerRegistry final { -public: - MMKVValueChangedListenerRegistry() = delete; - ~MMKVValueChangedListenerRegistry() = delete; - -public: - static ListenerID addListener(const std::string& mmkvID, const std::function& callback); - static void removeListener(const std::string& mmkvID, ListenerID id); - -public: - static void notifyOnValueChanged(const std::string& mmkvID, const std::string& key); - -private: - static std::atomic _listenersCounter; - static std::unordered_map> _listeners; -}; - -} // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp index 34ee7034..585fe8d3 100644 --- a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp @@ -33,6 +33,7 @@ namespace margelo::nitro::mmkv { prototype.registerHybridMethod("encrypt", &HybridMMKVSpec::encrypt); prototype.registerHybridMethod("decrypt", &HybridMMKVSpec::decrypt); prototype.registerHybridMethod("trim", &HybridMMKVSpec::trim); + prototype.registerHybridMethod("checkExternalContentChanged", &HybridMMKVSpec::checkExternalContentChanged); prototype.registerHybridMethod("addOnValueChangedListener", &HybridMMKVSpec::addOnValueChangedListener); prototype.registerHybridMethod("importAllFrom", &HybridMMKVSpec::importAllFrom); }); diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp index df38323e..14245080 100644 --- a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp @@ -80,6 +80,7 @@ namespace margelo::nitro::mmkv { virtual void encrypt(const std::string& key, std::optional encryptionType) = 0; virtual void decrypt() = 0; virtual void trim() = 0; + virtual void checkExternalContentChanged() = 0; virtual Listener addOnValueChangedListener(const std::function& onValueChanged) = 0; virtual double importAllFrom(const std::shared_ptr& other) = 0; diff --git a/packages/react-native-mmkv/src/addMemoryWarningListener/addMemoryWarningListener.ts b/packages/react-native-mmkv/src/addMemoryWarningListener/addMemoryWarningListener.ts index d149614e..a1c67b8e 100644 --- a/packages/react-native-mmkv/src/addMemoryWarningListener/addMemoryWarningListener.ts +++ b/packages/react-native-mmkv/src/addMemoryWarningListener/addMemoryWarningListener.ts @@ -1,29 +1,50 @@ import { AppState } from 'react-native' -import type { NativeEventSubscription } from 'react-native' +import type { AppStateStatus, NativeEventSubscription } from 'react-native' import type { MMKV } from '../specs/MMKV.nitro' export function addMemoryWarningListener(mmkv: MMKV): void { + const checkExternalContentChanged = ( + appState: AppStateStatus, + instance: MMKV + ) => { + if (appState === 'active') { + instance.checkExternalContentChanged() + } + } + if (global.WeakRef != null && global.FinalizationRegistry != null) { // 1. Weakify MMKV so we can safely use it inside the memoryWarning event listener const weakMmkv = new WeakRef(mmkv) - const listener = AppState.addEventListener('memoryWarning', () => { - // 0. Everytime we receive a memoryWarning, we try to trim the MMKV instance (if it is still valid) - weakMmkv.deref()?.trim() + const memoryWarningListener = AppState.addEventListener( + 'memoryWarning', + () => { + // 0. Everytime we receive a memoryWarning, we try to trim the MMKV instance (if it is still valid) + weakMmkv.deref()?.trim() + } + ) + const appStateListener = AppState.addEventListener('change', (appState) => { + const instance = weakMmkv.deref() + if (instance != null) { + checkExternalContentChanged(appState, instance) + } }) // 2. Add a listener to when the MMKV instance is deleted const finalization = new FinalizationRegistry( - (l: NativeEventSubscription) => { - // 3. When MMKV is deleted, this listener will be called with the memoryWarning listener. - l.remove() + (listeners: NativeEventSubscription[]) => { + // 3. When MMKV is deleted, this listener will be called with the AppState listeners. + listeners.forEach((l) => l.remove()) } ) // 2.1. Bind the listener to the actual MMKV instance. - finalization.register(mmkv, listener) + finalization.register(mmkv, [memoryWarningListener, appStateListener]) } else { // WeakRef/FinalizationRegistry is not implemented in this engine. // Just add the listener, even if it retains MMKV strong forever. AppState.addEventListener('memoryWarning', () => { mmkv.trim() }) + AppState.addEventListener('change', (appState) => { + checkExternalContentChanged(appState, mmkv) + }) } } diff --git a/packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts b/packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts index 24e17e6d..aefddb25 100644 --- a/packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts +++ b/packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts @@ -126,6 +126,9 @@ export function createMMKV( trim: () => { // no-op }, + checkExternalContentChanged: () => { + // no-op + }, dispose: () => {}, equals: () => false, name: 'MMKV', diff --git a/packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts b/packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts index 33c9ace7..a188954a 100644 --- a/packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts +++ b/packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts @@ -80,6 +80,9 @@ export function createMockMMKV( trim: () => { // no-op }, + checkExternalContentChanged: () => { + // no-op + }, name: 'MMKV', dispose: () => {}, equals: () => { diff --git a/packages/react-native-mmkv/src/specs/MMKV.nitro.ts b/packages/react-native-mmkv/src/specs/MMKV.nitro.ts index f97eaaa1..17a1e6c7 100644 --- a/packages/react-native-mmkv/src/specs/MMKV.nitro.ts +++ b/packages/react-native-mmkv/src/specs/MMKV.nitro.ts @@ -133,6 +133,14 @@ export interface MMKV extends HybridObject<{ ios: 'c++'; android: 'c++' }> { * In most applications, this is not needed at all. */ trim(): void + /** + * Checks whether this {@linkcode MMKV} instance was changed by another process, + * such as an app extension, App Clip, or background service. + * + * This is only needed if you want to manually force MMKV's native outer-process + * content check. Web implementations do nothing. + */ + checkExternalContentChanged(): void /** * Adds a value changed listener. The Listener will be called whenever any value * in this storage instance changes (set or delete). @@ -140,7 +148,6 @@ export interface MMKV extends HybridObject<{ ios: 'c++'; android: 'c++' }> { * To unsubscribe from value changes, call `remove()` on the Listener. */ addOnValueChangedListener(onValueChanged: (key: string) => void): Listener - /** * Imports all keys and values from the * given other {@linkcode MMKV} instance.