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 e4189a68..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 { @@ -125,7 +126,12 @@ export type NativeMMKV = Pick< | 'getBuffer' | 'set' | 'recrypt' ->; +> & { + applyBatchedWrites: ( + map: MemoryCacheMap, + deleteFlag: typeof DELETE_SYMBOL + ) => Promise; +}; const onValueChangedListeners = new Map void)[]>(); @@ -233,6 +239,37 @@ 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: MemoryCache): MMKVInterface { + return { + addOnValueChangedListener: this.addOnValueChangedListener, + 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: memoryCache.set, + }; + } + + batch(callback: (storage: MMKVInterface) => void): Promise { + const memoryCache = new MemoryCache(this); + const copy = this.createInMemoryCopy(memoryCache); + callback(copy); + return this.nativeInstance.applyBatchedWrites( + memoryCache.map, + DELETE_SYMBOL + ); + } + addOnValueChangedListener(onValueChanged: (key: string) => void): Listener { this.onValueChangedListeners.push(onValueChanged); 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); + } + } +}