Skip to content

Commit b47ba40

Browse files
authored
feat: Add checkContentChanged() and reactively listen to it in hooks (#1071)
* feat: add content change checks * install one listener per instance, not per hook
1 parent 9f29037 commit b47ba40

13 files changed

Lines changed: 139 additions & 0 deletions

File tree

example/__tests__/MMKV.harness.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,17 @@ describe('MMKV Configuration & Multiple Instances', () => {
416416

417417
storage.clearAll();
418418
});
419+
420+
it('should check for external content changes', () => {
421+
const storage = createMMKV({ id: 'content-change-check-test' });
422+
423+
storage.checkContentChanged();
424+
425+
storage.set('content-change-check', 'value');
426+
expect(storage.getString('content-change-check')).toStrictEqual('value');
427+
428+
storage.clearAll();
429+
});
419430
});
420431

421432
describe('Instance Management', () => {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ void HybridMMKV::trim() {
229229
instance->clearMemoryCache();
230230
}
231231

232+
void HybridMMKV::checkContentChanged() {
233+
instance->checkContentChanged();
234+
}
235+
232236
Listener HybridMMKV::addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) {
233237
// Add listener
234238
auto mmkvID = instance->mmapID();

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 checkContentChanged() 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/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace margelo::nitro::mmkv {
3333
prototype.registerHybridMethod("encrypt", &HybridMMKVSpec::encrypt);
3434
prototype.registerHybridMethod("decrypt", &HybridMMKVSpec::decrypt);
3535
prototype.registerHybridMethod("trim", &HybridMMKVSpec::trim);
36+
prototype.registerHybridMethod("checkContentChanged", &HybridMMKVSpec::checkContentChanged);
3637
prototype.registerHybridMethod("addOnValueChangedListener", &HybridMMKVSpec::addOnValueChangedListener);
3738
prototype.registerHybridMethod("importAllFrom", &HybridMMKVSpec::importAllFrom);
3839
});

packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ namespace margelo::nitro::mmkv {
8080
virtual void encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) = 0;
8181
virtual void decrypt() = 0;
8282
virtual void trim() = 0;
83+
virtual void checkContentChanged() = 0;
8384
virtual Listener addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) = 0;
8485
virtual double importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) = 0;
8586

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { AppState } from 'react-native'
2+
import type { AppStateStatus, NativeEventSubscription } from 'react-native'
3+
import { createMockMMKV } from '../createMMKV/createMockMMKV'
4+
import { addContentChangedListener } from '../addContentChangedListener/addContentChangedListener'
5+
6+
function mockAppStateChangeListener(): {
7+
emit: (state: AppStateStatus) => void
8+
} {
9+
const listeners = new Set<(state: AppStateStatus) => void>()
10+
11+
jest
12+
.spyOn(AppState, 'addEventListener')
13+
.mockImplementation((type, listener): NativeEventSubscription => {
14+
if (type === 'change') {
15+
listeners.add(listener as (state: AppStateStatus) => void)
16+
}
17+
18+
return {
19+
remove: () => {
20+
listeners.delete(listener as (state: AppStateStatus) => void)
21+
},
22+
} as NativeEventSubscription
23+
})
24+
25+
return {
26+
emit: (state) => {
27+
listeners.forEach((listener) => listener(state))
28+
},
29+
}
30+
}
31+
32+
afterEach(() => {
33+
jest.clearAllMocks()
34+
jest.restoreAllMocks()
35+
})
36+
37+
test('content changed listener checks content when app becomes active', () => {
38+
const appState = mockAppStateChangeListener()
39+
const storage = createMockMMKV({ id: 'content-changed-listener-test' })
40+
const checkContentChanged = jest.spyOn(storage, 'checkContentChanged')
41+
42+
addContentChangedListener(storage)
43+
44+
appState.emit('background')
45+
expect(checkContentChanged).not.toHaveBeenCalled()
46+
47+
appState.emit('active')
48+
expect(checkContentChanged).toHaveBeenCalledTimes(1)
49+
})
50+
51+
test('content changed listener registers once per instance', () => {
52+
mockAppStateChangeListener()
53+
const storage = createMockMMKV({
54+
id: 'content-changed-listener-once-test',
55+
})
56+
57+
addContentChangedListener(storage)
58+
addContentChangedListener(storage)
59+
60+
expect(AppState.addEventListener).toHaveBeenCalledTimes(1)
61+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { MMKV } from '../specs/MMKV.nitro'
2+
3+
export function addContentChangedListener(_mmkv: MMKV): void {
4+
// no-op in mocked environments
5+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { AppState } from 'react-native'
2+
import type { AppStateStatus, NativeEventSubscription } from 'react-native'
3+
import type { MMKV } from '../specs/MMKV.nitro'
4+
5+
const registeredInstances = new WeakSet<MMKV>()
6+
7+
export function addContentChangedListener(mmkv: MMKV): void {
8+
if (registeredInstances.has(mmkv)) {
9+
return
10+
}
11+
registeredInstances.add(mmkv)
12+
13+
if (global.WeakRef != null && global.FinalizationRegistry != null) {
14+
const weakMmkv = new WeakRef(mmkv)
15+
const listener = AppState.addEventListener(
16+
'change',
17+
(state: AppStateStatus) => {
18+
if (state === 'active') {
19+
weakMmkv.deref()?.checkContentChanged()
20+
}
21+
}
22+
)
23+
const finalization = new FinalizationRegistry(
24+
(l: NativeEventSubscription) => {
25+
l.remove()
26+
}
27+
)
28+
finalization.register(mmkv, listener)
29+
} else {
30+
AppState.addEventListener('change', (state: AppStateStatus) => {
31+
if (state === 'active') {
32+
mmkv.checkContentChanged()
33+
}
34+
})
35+
}
36+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { MMKV } from '../specs/MMKV.nitro'
2+
3+
export function addContentChangedListener(_mmkv: MMKV): void {
4+
// no-op on Web
5+
}

packages/react-native-mmkv/src/createMMKV/createMMKV.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { MMKV } from '../specs/MMKV.nitro'
22
import type { Configuration } from '../specs/MMKVFactory.nitro'
33
import { Platform } from 'react-native'
44
import { addMemoryWarningListener } from '../addMemoryWarningListener/addMemoryWarningListener'
5+
import { addContentChangedListener } from '../addContentChangedListener/addContentChangedListener'
56
import { isTest } from '../isTest'
67
import { createMockMMKV } from './createMockMMKV'
78
import { getMMKVFactory, getPlatformContext } from '../getMMKVFactory'
@@ -33,5 +34,7 @@ export function createMMKV(configuration?: Configuration): MMKV {
3334
const mmkv = factory.createMMKV(config)
3435
// Add a hook that trims the storage when we get a memory warning
3536
addMemoryWarningListener(mmkv)
37+
// Check for updates from app extensions/App Clips/background services when the app becomes active
38+
addContentChangedListener(mmkv)
3639
return mmkv
3740
}

0 commit comments

Comments
 (0)