Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ import { deleteMMKV } from 'react-native-mmkv'
const wasDeleted = deleteMMKV('my-instance')
```

Instances of a deleted storage that are still alive become unusable: using them throws. Create a new one with `createMMKV(...)`.

### Log Level

By default, MMKV logs at `Debug` level in debug builds and `Warning` level in release builds. You can override this at build time to control the verbosity of MMKV's native logs.
Expand Down
34 changes: 34 additions & 0 deletions example/__tests__/MMKV.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,40 @@ describe('MMKV Configuration & Multiple Instances', () => {
expect(storage2.getBoolean('key3')).toStrictEqual(true);
});

it('should make live instances unusable after deleteMMKV()', (context) => {
context.skip(
Platform.OS === 'web',
'deleteMMKV does not invalidate instances on web',
);
const storage = createMMKV({ id: 'deleted-live-instance' });
const sameFile = createMMKV({ id: 'deleted-live-instance' });
const other = createMMKV({ id: 'deleted-live-instance-other' });
storage.set('key', 'value');
other.set('key', 'value');

expect(deleteMMKV('deleted-live-instance')).toStrictEqual(true);

// Every instance of the deleted file throws instead of using freed memory
expect(() => storage.getString('key')).toThrow();
expect(() => sameFile.set('key', 'value')).toThrow();
expect(() => other.importAllFrom(storage)).toThrow();
// The AppState and memory warning listeners still call these
expect(() => storage.checkContentChanged()).not.toThrow();
expect(() => storage.trim()).not.toThrow();
expect(storage.id).toStrictEqual('deleted-live-instance');
// Other files are not affected
expect(other.getString('key')).toStrictEqual('value');

// The id can be used again
const recreated = createMMKV({ id: 'deleted-live-instance' });
expect(recreated.getString('key')).toBeUndefined();
recreated.set('key', 'new');
expect(recreated.getString('key')).toStrictEqual('new');

recreated.clearAll();
other.clearAll();
});

it('should handle instance properties correctly', () => {
const storage = createMMKV({ id: 'properties-test' });

Expand Down
86 changes: 82 additions & 4 deletions packages/react-native-mmkv/cpp/HybridMMKV.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@

namespace margelo::nitro::mmkv {

HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) {
std::mutex HybridMMKV::_liveInstancesMutex;
std::unordered_set<HybridMMKV*> HybridMMKV::_liveInstances;
Comment on lines +16 to +17

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to avoid keeping track of such static maps. That's the whole point of object-oriented programming to not do that.


HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG), _id(config.id), _rootPath(config.path.value_or("")) {
MMKVMode mmkvMode = getMMKVMode(config);
if (config.readOnly.value_or(false)) {
mmkvMode = mmkvMode | MMKVMode::MMKV_READ_ONLY;
Expand All @@ -36,7 +39,7 @@ HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) {
Logger::log(LogLevel::Info, TAG, "Creating MMKV instance \"%s\"... (Path: %s, Encrypted: %s)", config.id.c_str(), rootPath.c_str(),
hasEncryptionKey ? "true" : "false");

instance = MMKV::mmkvWithID(config.id, mmkvConfig);
MMKV* instance = MMKV::mmkvWithID(config.id, mmkvConfig);

if (instance == nullptr) [[unlikely]] {
// Check if instanceId is invalid
Expand Down Expand Up @@ -65,13 +68,61 @@ HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) {

throw std::runtime_error("Failed to create MMKV instance!");
}

_instance = instance;
std::lock_guard lock(_liveInstancesMutex);
_liveInstances.insert(this);
}

HybridMMKV::~HybridMMKV() {
std::lock_guard lock(_liveInstancesMutex);
_liveInstances.erase(this);
}

void HybridMMKV::invalidateInstances(const std::string& id, const std::string& rootPath) {
auto resolvedRoot = [](const std::string& path) -> const std::string& { return path.empty() ? MMKV::getRootDir() : path; };
const std::string& root = resolvedRoot(rootPath);

std::lock_guard lock(_liveInstancesMutex);
// 1. Find the native instance of that file. MMKV caches one per file, so every
// HybridMMKV of the file shares it (even one created with an equivalent path).
MMKV* deleted = nullptr;
for (HybridMMKV* hybrid : _liveInstances) {
if (hybrid->_id == id && resolvedRoot(hybrid->_rootPath) == root) {
deleted = hybrid->_instance.load();
if (deleted != nullptr) {
break;
}
}
}
if (deleted == nullptr) {
// No live instance of this file.
return;
}
// 2. Unlink every HybridMMKV that uses it.
for (HybridMMKV* hybrid : _liveInstances) {
MMKV* expected = deleted;
hybrid->_instance.compare_exchange_strong(expected, nullptr);
}
}

MMKV* HybridMMKV::getInstance() const {
MMKV* instance = _instance.load();
if (instance == nullptr) [[unlikely]] {
throw std::runtime_error("The MMKV instance \"" + _id +
"\" has been deleted with `deleteMMKV(...)`! Create a new one with `createMMKV(...)`.");
}
return instance;
}

std::string HybridMMKV::getId() {
return instance->mmapID();
MMKV* instance = _instance.load();
// Still readable after `deleteMMKV(...)`, e.g. for logging.
return instance != nullptr ? instance->mmapID() : _id;
}

double HybridMMKV::getLength() {
MMKV* instance = getInstance();
return instance->count();
}

Expand All @@ -80,18 +131,22 @@ double HybridMMKV::getSize() {
}

double HybridMMKV::getByteSize() {
MMKV* instance = getInstance();
return instance->actualSize();
}

size_t HybridMMKV::getExternalMemorySize() noexcept {
MMKV* instance = _instance.load();
return instance != nullptr ? instance->actualSize() : 0;
}

bool HybridMMKV::getIsReadOnly() {
MMKV* instance = getInstance();
return instance->isReadOnly();
}

bool HybridMMKV::getIsEncrypted() {
MMKV* instance = getInstance();
return instance->isEncryptionEnabled();
}

Expand All @@ -104,6 +159,7 @@ template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

void HybridMMKV::set(const std::string& key, const std::variant<bool, std::shared_ptr<ArrayBuffer>, std::string, double>& value) {
MMKV* instance = getInstance();
if (key.empty()) [[unlikely]] {
throw std::runtime_error("Cannot set a value for an empty key!");
}
Expand Down Expand Up @@ -136,6 +192,7 @@ void HybridMMKV::set(const std::string& key, const std::variant<bool, std::share
}

std::optional<bool> HybridMMKV::getBoolean(const std::string& key) {
MMKV* instance = getInstance();
bool hasValue;
bool result = instance->getBool(key, /* defaultValue */ false, &hasValue);
if (hasValue) {
Expand All @@ -146,6 +203,7 @@ std::optional<bool> HybridMMKV::getBoolean(const std::string& key) {
}

std::optional<std::string> HybridMMKV::getString(const std::string& key) {
MMKV* instance = getInstance();
std::string result;
bool hasValue = instance->getString(key, result, /* inplaceModification */ true);
if (hasValue) {
Expand All @@ -156,6 +214,7 @@ std::optional<std::string> HybridMMKV::getString(const std::string& key) {
}

std::optional<double> HybridMMKV::getNumber(const std::string& key) {
MMKV* instance = getInstance();
bool hasValue;
double result = instance->getDouble(key, /* defaultValue */ 0.0, &hasValue);
if (hasValue) {
Expand All @@ -166,6 +225,7 @@ std::optional<double> HybridMMKV::getNumber(const std::string& key) {
}

std::optional<std::shared_ptr<ArrayBuffer>> HybridMMKV::getBuffer(const std::string& key) {
MMKV* instance = getInstance();
MMBuffer result;
bool hasValue = instance->getBytes(key, result);
if (hasValue) {
Expand All @@ -176,10 +236,12 @@ std::optional<std::shared_ptr<ArrayBuffer>> HybridMMKV::getBuffer(const std::str
}

bool HybridMMKV::contains(const std::string& key) {
MMKV* instance = getInstance();
return instance->containsKey(key);
}

bool HybridMMKV::remove(const std::string& key) {
MMKV* instance = getInstance();
bool wasRemoved = instance->removeValueForKey(key);
if (wasRemoved) {
// Notify on changed
Expand All @@ -189,10 +251,12 @@ bool HybridMMKV::remove(const std::string& key) {
}

std::vector<std::string> HybridMMKV::getAllKeys() {
MMKV* instance = getInstance();
return instance->allKeys();
}

void HybridMMKV::clearAll() {
MMKV* instance = getInstance();
auto keysBefore = getAllKeys();
instance->clearAll();
for (const auto& key : keysBefore) {
Expand All @@ -210,6 +274,7 @@ void HybridMMKV::recrypt(const std::optional<std::string>& key) {
}

void HybridMMKV::encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) {
MMKV* instance = getInstance();
bool isAes256Encryption = encryptionType == EncryptionType::AES_256;
bool successful = instance->reKey(key, isAes256Encryption);
if (!successful) {
Expand All @@ -218,22 +283,34 @@ void HybridMMKV::encrypt(const std::string& key, std::optional<EncryptionType> e
}

void HybridMMKV::decrypt() {
MMKV* instance = getInstance();
bool successful = instance->reKey("");
if (!successful) [[unlikely]] {
throw std::runtime_error("Failed to decrypt MMKV instance!");
}
}

void HybridMMKV::trim() {
MMKV* instance = _instance.load();
if (instance == nullptr) {
// Deleted with `deleteMMKV(...)`: nothing to trim. The memory warning listener calls this.
return;
}
instance->trim();
instance->clearMemoryCache();
}

void HybridMMKV::checkContentChanged() {
MMKV* instance = _instance.load();
if (instance == nullptr) {
// Deleted with `deleteMMKV(...)`: nothing to check. The AppState listener calls this.
return;
}
instance->checkContentChanged();
}

Listener HybridMMKV::addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) {
MMKV* instance = getInstance();
// Add listener
auto mmkvID = instance->mmapID();
auto listenerID = MMKVValueChangedListenerRegistry::addListener(mmkvID, onValueChanged);
Expand Down Expand Up @@ -272,12 +349,13 @@ std::optional<MMKVRecoverStrategic> HybridMMKV::getRecoveryStrategy(const Config
}

double HybridMMKV::importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) {
MMKV* instance = getInstance();
auto hybridMMKV = std::dynamic_pointer_cast<HybridMMKV>(other);
if (hybridMMKV == nullptr) [[unlikely]] {
throw std::runtime_error("The given `MMKV` instance is not of type `HybridMMKV`!");
}

size_t importedCount = instance->importFrom(hybridMMKV->instance);
size_t importedCount = instance->importFrom(hybridMMKV->getInstance());
return static_cast<double>(importedCount);
}

Expand Down
25 changes: 24 additions & 1 deletion packages/react-native-mmkv/cpp/HybridMMKV.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,24 @@
#include "Configuration.hpp"
#include "HybridMMKVSpec.hpp"
#include "MMKVTypes.hpp"
#include <atomic>
#include <mutex>
#include <unordered_set>

namespace margelo::nitro::mmkv {

class HybridMMKV final : public HybridMMKVSpec {
public:
explicit HybridMMKV(const Configuration& configuration);
~HybridMMKV() override;

public:
/**
* Makes every live instance of the MMKV file `id` in `rootPath` (MMKV's root
* directory if empty) unusable. Call it before deleting the file:
* `MMKV::removeStorage(...)` destroys the native instance they all point to.
*/
static void invalidateInstances(const std::string& id, const std::string& rootPath = "");

public:
// Properties
Expand Down Expand Up @@ -53,7 +65,18 @@ class HybridMMKV final : public HybridMMKVSpec {
static std::optional<MMKVRecoverStrategic> getRecoveryStrategy(const Configuration& config);

private:
MMKV* instance;
/**
* The native instance, or throws if it was deleted with `deleteMMKV(...)`.
*/
MMKV* getInstance() const;

private:
std::atomic<MMKV*> _instance;
std::string _id;
std::string _rootPath;

static std::mutex _liveInstancesMutex;
static std::unordered_set<HybridMMKV*> _liveInstances;
};

} // namespace margelo::nitro::mmkv
3 changes: 3 additions & 0 deletions packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ std::shared_ptr<HybridMMKVSpec> HybridMMKVFactory::createMMKV(const Configuratio
}

bool HybridMMKVFactory::deleteMMKV(const std::string& id) {
// MMKV destroys the file's native instance: instances still alive in JS
// must not keep pointing to it.
HybridMMKV::invalidateInstances(id);
return MMKV::removeStorage(id);
}

Expand Down