Skip to content

Commit 70ea34d

Browse files
committed
feat: add content change checks
1 parent 9f29037 commit 70ea34d

13 files changed

Lines changed: 143 additions & 4 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

packages/react-native-mmkv/src/__tests__/hooks.test.tsx

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
2-
import { Button, Text } from 'react-native'
2+
import { AppState, Button, Text } from 'react-native'
3+
import type { AppStateStatus, NativeEventSubscription } from 'react-native'
34
import {
45
act,
56
fireEvent,
@@ -9,7 +10,7 @@ import {
910
cleanup,
1011
waitFor,
1112
} from '@testing-library/react-native'
12-
import { createMMKV, useMMKVNumber, useMMKVString } from '..'
13+
import { createMMKV, useMMKVKeys, useMMKVNumber, useMMKVString } from '..'
1314

1415
const mmkv = createMMKV()
1516

@@ -20,8 +21,37 @@ beforeEach(() => {
2021

2122
afterEach(() => {
2223
cleanup()
24+
jest.restoreAllMocks()
2325
})
2426

27+
function mockAppStateChangeListener(): {
28+
emit: (state: AppStateStatus) => void
29+
} {
30+
const listeners = new Set<(state: AppStateStatus) => void>()
31+
32+
jest
33+
.spyOn(AppState, 'addEventListener')
34+
.mockImplementation((type, listener): NativeEventSubscription => {
35+
if (type === 'change') {
36+
listeners.add(listener as (state: AppStateStatus) => void)
37+
}
38+
39+
return {
40+
remove: () => {
41+
listeners.delete(listener as (state: AppStateStatus) => void)
42+
},
43+
} as NativeEventSubscription
44+
})
45+
46+
return {
47+
emit: (state) => {
48+
act(() => {
49+
listeners.forEach((listener) => listener(state))
50+
})
51+
},
52+
}
53+
}
54+
2555
test('hooks update when the value is changed directly through the instance', () => {
2656
const { result } = renderHook(() => useMMKVString('string-key', mmkv))
2757

@@ -129,3 +159,40 @@ test('useMMKV hook stays consistent during rapid updates', async () => {
129159
expect(result.current[0]).toBe(100)
130160
})
131161
})
162+
163+
test('useMMKV hook checks for external content changes when app becomes active', () => {
164+
const appState = mockAppStateChangeListener()
165+
const key = 'app-state-key'
166+
let value = 'before'
167+
168+
const checkContentChanged = jest.spyOn(mmkv, 'checkContentChanged')
169+
jest.spyOn(mmkv, 'getString').mockImplementation((requestedKey) => {
170+
return requestedKey === key ? value : undefined
171+
})
172+
173+
const { result } = renderHook(() => useMMKVString(key, mmkv))
174+
expect(result.current[0]).toStrictEqual('before')
175+
176+
value = 'after'
177+
appState.emit('active')
178+
179+
expect(checkContentChanged).toHaveBeenCalledTimes(1)
180+
expect(result.current[0]).toStrictEqual('after')
181+
})
182+
183+
test('useMMKVKeys checks for external content changes when app becomes active', () => {
184+
const appState = mockAppStateChangeListener()
185+
let keys: string[] = []
186+
187+
const checkContentChanged = jest.spyOn(mmkv, 'checkContentChanged')
188+
jest.spyOn(mmkv, 'getAllKeys').mockImplementation(() => keys)
189+
190+
const { result } = renderHook(() => useMMKVKeys(mmkv))
191+
expect(result.current).toStrictEqual([])
192+
193+
keys = ['external-key']
194+
appState.emit('active')
195+
196+
expect(checkContentChanged).toHaveBeenCalledTimes(1)
197+
expect(result.current).toStrictEqual(['external-key'])
198+
})

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export function createMMKV(
126126
trim: () => {
127127
// no-op
128128
},
129+
checkContentChanged: () => {
130+
// no-op
131+
},
129132
dispose: () => {},
130133
equals: () => false,
131134
name: 'MMKV',

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export function createMockMMKV(
8080
trim: () => {
8181
// no-op
8282
},
83+
checkContentChanged: () => {
84+
// no-op
85+
},
8386
name: 'MMKV',
8487
dispose: () => {},
8588
equals: () => {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { AppState } from 'react-native'
2+
import type { AppStateStatus, NativeEventSubscription } from 'react-native'
3+
import type { MMKV } from '../specs/MMKV.nitro'
4+
5+
export function addContentChangedAppStateListener(
6+
mmkv: MMKV,
7+
onContentChanged: () => void
8+
): NativeEventSubscription {
9+
return AppState.addEventListener('change', (state: AppStateStatus) => {
10+
if (state === 'active') {
11+
mmkv.checkContentChanged()
12+
onContentChanged()
13+
}
14+
})
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { MMKV } from '../specs/MMKV.nitro'
2+
3+
export function addContentChangedAppStateListener(
4+
_mmkv: MMKV,
5+
_onContentChanged: () => void
6+
): { remove(): void } {
7+
return {
8+
remove: () => {
9+
// no-op
10+
},
11+
}
12+
}

0 commit comments

Comments
 (0)