Skip to content
Merged
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
11 changes: 11 additions & 0 deletions example/__tests__/MMKV.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,17 @@ describe('MMKV Configuration & Multiple Instances', () => {

storage.clearAll();
});

it('should check for external content changes', () => {
const storage = createMMKV({ id: 'content-change-check-test' });

storage.checkContentChanged();

storage.set('content-change-check', 'value');
expect(storage.getString('content-change-check')).toStrictEqual('value');

storage.clearAll();
});
});

describe('Instance Management', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native-mmkv/cpp/HybridMMKV.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ void HybridMMKV::trim() {
instance->clearMemoryCache();
}

void HybridMMKV::checkContentChanged() {
instance->checkContentChanged();
}

Listener HybridMMKV::addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) {
// Add listener
auto mmkvID = instance->mmapID();
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-mmkv/cpp/HybridMMKV.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class HybridMMKV final : public HybridMMKVSpec {
void encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) override;
void decrypt() override;
void trim() override;
void checkContentChanged() override;
Listener addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) override;
double importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) override;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("checkContentChanged", &HybridMMKVSpec::checkContentChanged);
prototype.registerHybridMethod("addOnValueChangedListener", &HybridMMKVSpec::addOnValueChangedListener);
prototype.registerHybridMethod("importAllFrom", &HybridMMKVSpec::importAllFrom);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ namespace margelo::nitro::mmkv {
virtual void encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) = 0;
virtual void decrypt() = 0;
virtual void trim() = 0;
virtual void checkContentChanged() = 0;
virtual Listener addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) = 0;
virtual double importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) = 0;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { AppState } from 'react-native'
import type { AppStateStatus, NativeEventSubscription } from 'react-native'
import { createMockMMKV } from '../createMMKV/createMockMMKV'
import { addContentChangedListener } from '../addContentChangedListener/addContentChangedListener'

function mockAppStateChangeListener(): {
emit: (state: AppStateStatus) => void
} {
const listeners = new Set<(state: AppStateStatus) => void>()

jest
.spyOn(AppState, 'addEventListener')
.mockImplementation((type, listener): NativeEventSubscription => {
if (type === 'change') {
listeners.add(listener as (state: AppStateStatus) => void)
}

return {
remove: () => {
listeners.delete(listener as (state: AppStateStatus) => void)
},
} as NativeEventSubscription
})

return {
emit: (state) => {
listeners.forEach((listener) => listener(state))
},
}
}

afterEach(() => {
jest.clearAllMocks()
jest.restoreAllMocks()
})

test('content changed listener checks content when app becomes active', () => {
const appState = mockAppStateChangeListener()
const storage = createMockMMKV({ id: 'content-changed-listener-test' })
const checkContentChanged = jest.spyOn(storage, 'checkContentChanged')

addContentChangedListener(storage)

appState.emit('background')
expect(checkContentChanged).not.toHaveBeenCalled()

appState.emit('active')
expect(checkContentChanged).toHaveBeenCalledTimes(1)
})

test('content changed listener registers once per instance', () => {
mockAppStateChangeListener()
const storage = createMockMMKV({
id: 'content-changed-listener-once-test',
})

addContentChangedListener(storage)
addContentChangedListener(storage)

expect(AppState.addEventListener).toHaveBeenCalledTimes(1)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { MMKV } from '../specs/MMKV.nitro'

export function addContentChangedListener(_mmkv: MMKV): void {
// no-op in mocked environments
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AppState } from 'react-native'
import type { AppStateStatus, NativeEventSubscription } from 'react-native'
import type { MMKV } from '../specs/MMKV.nitro'

const registeredInstances = new WeakSet<MMKV>()

export function addContentChangedListener(mmkv: MMKV): void {
if (registeredInstances.has(mmkv)) {
return
}
registeredInstances.add(mmkv)

if (global.WeakRef != null && global.FinalizationRegistry != null) {
const weakMmkv = new WeakRef(mmkv)
const listener = AppState.addEventListener(
'change',
(state: AppStateStatus) => {
if (state === 'active') {
weakMmkv.deref()?.checkContentChanged()
}
}
)
const finalization = new FinalizationRegistry(
(l: NativeEventSubscription) => {
l.remove()
}
)
finalization.register(mmkv, listener)
} else {
AppState.addEventListener('change', (state: AppStateStatus) => {
if (state === 'active') {
mmkv.checkContentChanged()
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { MMKV } from '../specs/MMKV.nitro'

export function addContentChangedListener(_mmkv: MMKV): void {
// no-op on Web
}
3 changes: 3 additions & 0 deletions packages/react-native-mmkv/src/createMMKV/createMMKV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { MMKV } from '../specs/MMKV.nitro'
import type { Configuration } from '../specs/MMKVFactory.nitro'
import { Platform } from 'react-native'
import { addMemoryWarningListener } from '../addMemoryWarningListener/addMemoryWarningListener'
import { addContentChangedListener } from '../addContentChangedListener/addContentChangedListener'
import { isTest } from '../isTest'
import { createMockMMKV } from './createMockMMKV'
import { getMMKVFactory, getPlatformContext } from '../getMMKVFactory'
Expand Down Expand Up @@ -33,5 +34,7 @@ export function createMMKV(configuration?: Configuration): MMKV {
const mmkv = factory.createMMKV(config)
// Add a hook that trims the storage when we get a memory warning
addMemoryWarningListener(mmkv)
// Check for updates from app extensions/App Clips/background services when the app becomes active
addContentChangedListener(mmkv)
return mmkv
}
3 changes: 3 additions & 0 deletions packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ export function createMMKV(
trim: () => {
// no-op
},
checkContentChanged: () => {
// no-op
},
dispose: () => {},
equals: () => false,
name: 'MMKV',
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export function createMockMMKV(
trim: () => {
// no-op
},
checkContentChanged: () => {
// no-op
},
name: 'MMKV',
dispose: () => {},
equals: () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/react-native-mmkv/src/specs/MMKV.nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ export interface MMKV extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
* In most applications, this is not needed at all.
*/
trim(): void
/**
* Checks whether the underlying storage file changed outside this process,
* and reloads this instance's data if needed.
*/
checkContentChanged(): void
/**
* Adds a value changed listener. The Listener will be called whenever any value
* in this storage instance changes (set or delete).
Expand Down
Loading