From f167c1b1812ebd8be41cceb802ff848e4a5a6a96 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 7 Apr 2023 18:39:01 +0200 Subject: [PATCH 1/2] Update MMKV.ts --- src/MMKV.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/MMKV.ts b/src/MMKV.ts index e4189a68..2231e313 100644 --- a/src/MMKV.ts +++ b/src/MMKV.ts @@ -125,7 +125,15 @@ export type NativeMMKV = Pick< | 'getBuffer' | 'set' | 'recrypt' ->; +> & { + applyBatchedWrites: ( + map: Map + ) => Promise; +}; + +function combineArrays(...arrays: T[][]): T[] { + return [...new Set(arrays.flat())]; +} const onValueChangedListeners = new Map void)[]>(); @@ -233,6 +241,62 @@ export class MMKV implements MMKVInterface { }; } + /** + * Creates a proxy of the MMKV instance that writes all new operations to an + * in-memory `Map` instead of writing it to the file. + * The Map can later be used to apply all writes in a single batch. + */ + private createInMemoryCopy( + memoryCache: Map + ): MMKVInterface { + return { + addOnValueChangedListener: this.addOnValueChangedListener, + clearAll: () => { + memoryCache.clear(); + for (const key of this.getAllKeys()) { + memoryCache.set(key, null); + } + }, + contains: (key) => { + if (memoryCache.has(key)) return memoryCache.get(key) != null; + else return this.contains(key); + }, + delete: (key) => memoryCache.set(key, null), + getAllKeys: () => { + return [...new Set([...this.getAllKeys(), ...memoryCache.keys()])]; + }, + getBoolean: (key) => { + if (!memoryCache.has(key)) { + memoryCache.set(key, this.getBoolean(key)); + } + if (memoryCache.has(key)) { + return Boolean(memoryCache.get(key)); + } + }, + getBuffer: (key) => + memoryCache.has(key) + ? (memoryCache.get(key) as Uint8Array) + : this.getBuffer(key), + getNumber: (key) => + memoryCache.has(key) + ? Number(memoryCache.get(key)) + : this.getNumber(key), + getString: (key) => + memoryCache.has(key) + ? String(memoryCache.get(key)) + : this.getString(key), + recrypt: this.recrypt, + set: (key, value) => memoryCache.set(key, value), + }; + } + + batch(callback: (storage: MMKVInterface) => void): Promise { + const map = new Map(); + const copy = this.createInMemoryCopy(map); + callback(copy); + return this.nativeInstance.applyBatchedWrites(map); + } + addOnValueChangedListener(onValueChanged: (key: string) => void): Listener { this.onValueChangedListeners.push(onValueChanged); From f1941a27b2094e7577c131139c7738bb7a0062f4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 7 Apr 2023 18:55:50 +0200 Subject: [PATCH 2/2] Create MemoryCache and pass to native --- ios/MmkvHostObject.mm | 32 +++++++++++++ src/MMKV.ts | 65 ++++++++------------------ src/MemoryCache.ts | 103 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 46 deletions(-) create mode 100644 src/MemoryCache.ts diff --git a/ios/MmkvHostObject.mm b/ios/MmkvHostObject.mm index 60cebf93..d88aed1a 100644 --- a/ios/MmkvHostObject.mm +++ b/ios/MmkvHostObject.mm @@ -295,6 +295,38 @@ return jsi::Value::undefined(); }); } + + if (propName == "applyBatchedWrites") { + // MMKV.applyBatchedWrites(Map, DELETE_SYMBOL) + return jsi::Function::createFromHostFunction(runtime, + jsi::PropNameID::forAscii(runtime, funcName), + 1, // encryptionKey + [this](jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + auto run = [this](jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + auto resolve = arguments[0].asObject(runtime).asFunction(runtime); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // TODO: Read from Map and apply everything here? + + // TODO: Resolve on CallInvoker + }); + + return jsi::Value::undefined(); + }; + + auto newPromise = runtime.global().getPropertyAsFunction(runtime, "Promise"); + auto runFunction = jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forAscii(runtime, "Promise::run"), 1, std::move(run)); + + return newPromise.callAsConstructor(runtime, runFunction, 1); + }); + + } return jsi::Value::undefined(); } diff --git a/src/MMKV.ts b/src/MMKV.ts index 2231e313..3b774c57 100644 --- a/src/MMKV.ts +++ b/src/MMKV.ts @@ -1,5 +1,6 @@ import { createMMKV } from './createMMKV'; import { createMockMMKV } from './createMMKV.mock'; +import { DELETE_SYMBOL, MemoryCache, MemoryCacheMap } from './MemoryCache'; import { isJest } from './PlatformChecker'; interface Listener { @@ -127,14 +128,11 @@ export type NativeMMKV = Pick< | 'recrypt' > & { applyBatchedWrites: ( - map: Map + map: MemoryCacheMap, + deleteFlag: typeof DELETE_SYMBOL ) => Promise; }; -function combineArrays(...arrays: T[][]): T[] { - return [...new Set(arrays.flat())]; -} - const onValueChangedListeners = new Map void)[]>(); /** @@ -246,55 +244,30 @@ export class MMKV implements MMKVInterface { * in-memory `Map` instead of writing it to the file. * The Map can later be used to apply all writes in a single batch. */ - private createInMemoryCopy( - memoryCache: Map - ): MMKVInterface { + private createInMemoryCopy(memoryCache: MemoryCache): MMKVInterface { return { addOnValueChangedListener: this.addOnValueChangedListener, - clearAll: () => { - memoryCache.clear(); - for (const key of this.getAllKeys()) { - memoryCache.set(key, null); - } - }, - contains: (key) => { - if (memoryCache.has(key)) return memoryCache.get(key) != null; - else return this.contains(key); - }, - delete: (key) => memoryCache.set(key, null), - getAllKeys: () => { - return [...new Set([...this.getAllKeys(), ...memoryCache.keys()])]; - }, - getBoolean: (key) => { - if (!memoryCache.has(key)) { - memoryCache.set(key, this.getBoolean(key)); - } - if (memoryCache.has(key)) { - return Boolean(memoryCache.get(key)); - } - }, - getBuffer: (key) => - memoryCache.has(key) - ? (memoryCache.get(key) as Uint8Array) - : this.getBuffer(key), - getNumber: (key) => - memoryCache.has(key) - ? Number(memoryCache.get(key)) - : this.getNumber(key), - getString: (key) => - memoryCache.has(key) - ? String(memoryCache.get(key)) - : this.getString(key), + clearAll: memoryCache.clear, + contains: memoryCache.has, + delete: memoryCache.delete, + getAllKeys: memoryCache.getAllKeys, + getBoolean: memoryCache.getBoolean, + getBuffer: memoryCache.getBuffer, + getNumber: memoryCache.getNumber, + getString: memoryCache.getString, recrypt: this.recrypt, - set: (key, value) => memoryCache.set(key, value), + set: memoryCache.set, }; } batch(callback: (storage: MMKVInterface) => void): Promise { - const map = new Map(); - const copy = this.createInMemoryCopy(map); + const memoryCache = new MemoryCache(this); + const copy = this.createInMemoryCopy(memoryCache); callback(copy); - return this.nativeInstance.applyBatchedWrites(map); + return this.nativeInstance.applyBatchedWrites( + memoryCache.map, + DELETE_SYMBOL + ); } addOnValueChangedListener(onValueChanged: (key: string) => void): Listener { diff --git a/src/MemoryCache.ts b/src/MemoryCache.ts new file mode 100644 index 00000000..9559071b --- /dev/null +++ b/src/MemoryCache.ts @@ -0,0 +1,103 @@ +import type { MMKV } from './MMKV'; + +export const DELETE_SYMBOL = Symbol(); +export const NOT_YET_LOADED_SYMBOL = Symbol(); + +type ValueType = string | boolean | number | Uint8Array; +type NOT_YET_LOADED = typeof NOT_YET_LOADED_SYMBOL; +type WILL_DELETE = typeof DELETE_SYMBOL; + +export type MemoryCacheMap = Map< + string, + ValueType | NOT_YET_LOADED | WILL_DELETE +>; + +export class MemoryCache { + map: MemoryCacheMap; + private mmkv: MMKV; + + constructor(mmkv: MMKV) { + this.map = new Map(); + this.mmkv = mmkv; + + // init all keys, values are not yet loaded though. + for (const key of mmkv.getAllKeys()) { + this.map.set(key, NOT_YET_LOADED_SYMBOL); + } + } + + has(key: string): boolean { + const value = this.map.get(key); + if (value == null) return false; + if (value === DELETE_SYMBOL) return false; + return true; + } + + getAllKeys(): string[] { + const keys: string[] = []; + this.map.forEach((value, key) => { + if (value !== DELETE_SYMBOL) keys.push(key); + }); + return keys; + } + + private get( + key: string, + method: T + ): ValueType | undefined { + const value = this.map.get(key); + + if (value === NOT_YET_LOADED_SYMBOL) { + const nativeValue = this.mmkv[method](key); + if (nativeValue != null) { + this.map.set(key, nativeValue); + return nativeValue; + } else { + return undefined; + } + } + if (value === DELETE_SYMBOL) { + return undefined; + } + + return value; + } + + getBoolean(key: string): boolean | undefined { + const value = this.get(key, 'getBoolean'); + if (typeof value === 'boolean') return value; + else return undefined; + } + + getString(key: string): string | undefined { + const value = this.get(key, 'getString'); + if (typeof value === 'string') return value; + else return undefined; + } + + getNumber(key: string): number | undefined { + const value = this.get(key, 'getNumber'); + if (typeof value === 'number') return value; + else return undefined; + } + + getBuffer(key: string): Uint8Array | undefined { + const value = this.get(key, 'getBuffer'); + if (value instanceof Uint8Array) return value; + else return undefined; + } + + set(key: string, value: ValueType): void { + this.map.set(key, value); + } + + delete(key: string): void { + this.map.set(key, DELETE_SYMBOL); + } + + clear(): void { + for (const key of this.map.keys()) { + this.delete(key); + } + } +}