From c3775dacf2b83c31e631188e9a786d0d03ab3d70 Mon Sep 17 00:00:00 2001 From: unadlib Date: Thu, 22 May 2025 23:14:52 +0800 Subject: [PATCH] refactor(apply): reuse mutative apply() with mutable mode --- package.json | 4 +- src/apply.ts | 96 ------------------------------------------------ src/index.ts | 16 +++++--- src/interface.ts | 20 ---------- src/utils.ts | 36 ------------------ yarn.lock | 8 ++-- 6 files changed, 16 insertions(+), 164 deletions(-) delete mode 100644 src/apply.ts delete mode 100644 src/interface.ts delete mode 100644 src/utils.ts diff --git a/package.json b/package.json index e5595e1..3b22cd3 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "mutative": "^1.1.0", + "mutative": "^1.2.0", "prettier": "^3.2.5", "rimraf": "^5.0.7", "rollup": "^4.22.5", @@ -82,6 +82,6 @@ } }, "peerDependencies": { - "mutative": "^1.0" + "mutative": "^1.2.0" } } diff --git a/src/apply.ts b/src/apply.ts deleted file mode 100644 index 4985a1c..0000000 --- a/src/apply.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { DraftType, Operation, Patches } from './interface'; -import { deepClone, get, getType, unescapePath } from './utils'; - -/** - * Apply patches to the state. - */ -export function apply(state: any, patches: Patches) { - let i; - for (i = patches.length - 1; i >= 0; i -= 1) { - const { value, op, path } = patches[i]; - if ( - (!path.length && op === Operation.Replace) || - (path === '' && op === Operation.Add) - ) { - state = value; - break; - } - } - if (i > -1) { - patches = patches.slice(i + 1); - } - patches.forEach((patch) => { - const { path: _path, op } = patch; - const path = unescapePath(_path); - let base: any = state; - for (let index = 0; index < path.length - 1; index += 1) { - const parentType = getType(base); - let key = path[index]; - if (typeof key !== 'string' && typeof key !== 'number') { - key = String(key); - } - if ( - ((parentType === DraftType.Object || parentType === DraftType.Array) && - (key === '__proto__' || key === 'constructor')) || - (typeof base === 'function' && key === 'prototype') - ) { - throw new Error( - `Patching reserved attributes like __proto__ and constructor is not allowed.` - ); - } - // use `index` in Set draft - base = get( - getType(base) === DraftType.Set ? Array.from(base) : base, - key - ); - if (typeof base !== 'object') { - throw new Error(`Cannot apply patch at '${path.join('/')}'.`); - } - } - - const type = getType(base); - // ensure the original patch is not modified. - const value = deepClone(patch.value); - const key = path[path.length - 1]; - switch (op) { - case Operation.Replace: - switch (type) { - case DraftType.Map: - return base.set(key, value); - case DraftType.Set: - throw new Error(`Cannot apply replace patch to set.`); - default: - return (base[key] = value); - } - case Operation.Add: - switch (type) { - case DraftType.Array: - // If the "-" character is used to - // index the end of the array (see [RFC6901](https://datatracker.ietf.org/doc/html/rfc6902)), - // this has the effect of appending the value to the array. - return key === '-' - ? base.push(value) - : base.splice(key as number, 0, value); - case DraftType.Map: - return base.set(key, value); - case DraftType.Set: - return base.add(value); - default: - return (base[key] = value); - } - case Operation.Remove: - switch (type) { - case DraftType.Array: - return base.splice(key as number, 1); - case DraftType.Map: - return base.delete(key); - case DraftType.Set: - return base.delete(patch.value); - default: - return delete base[key]; - } - default: - throw new Error(`Unsupported patch operation: ${op}.`); - } - }); -} diff --git a/src/index.ts b/src/index.ts index 0bbbbbd..098aa6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,19 @@ -import { create } from 'mutative'; -import { apply } from './apply'; - -export { apply } from './apply'; +import { create, apply as baseApply, Patches } from 'mutative'; /** * Transactional updates to the base state with the recipe. */ -export const mutate = (baseState: T, recipe: (state: T) => void) => { +export const mutate = ( + baseState: T, + recipe: (state: T) => void +) => { const [, patches, inversePatches] = create(baseState, recipe, { enablePatches: true, }); - apply(baseState, patches); + baseApply(baseState, patches, { mutable: true }); return { inversePatches, patches }; }; + +export const apply = (baseState: T, patches: Patches) => { + baseApply(baseState, patches, { mutable: true }); +}; diff --git a/src/interface.ts b/src/interface.ts deleted file mode 100644 index 02693e8..0000000 --- a/src/interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const Operation = { - Remove: 'remove', - Replace: 'replace', - Add: 'add', -} as const; - -export const enum DraftType { - Object, - Array, - Map, - Set, -} - -interface Patch { - op: (typeof Operation)[keyof typeof Operation]; - value?: any; - path: string | (string | number)[]; -} - -export type Patches = Patch[]; diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 209b73e..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { DraftType } from './interface'; - -export function unescapePath(path: string | (string | number)[]) { - if (Array.isArray(path)) return path; - return path - .split('/') - .map((_item) => _item.replace(/~1/g, '/').replace(/~0/g, '~')) - .slice(1); -} - -export function getType(target: any) { - if (Array.isArray(target)) return DraftType.Array; - if (target instanceof Map) return DraftType.Map; - if (target instanceof Set) return DraftType.Set; - return DraftType.Object; -} - -export function get(target: any, key: PropertyKey) { - return getType(target) === DraftType.Map ? target.get(key) : target[key]; -} - -function deepClone(target: T): T; -function deepClone(target: any) { - if (typeof target !== 'object' || target === null) return target; - if (Array.isArray(target)) return target.map(deepClone); - if (target instanceof Map) - return new Map( - Array.from(target.entries()).map(([k, v]) => [k, deepClone(v)]) - ); - if (target instanceof Set) return new Set(Array.from(target).map(deepClone)); - const copy = Object.create(Object.getPrototypeOf(target)); - for (const key in target) copy[key] = deepClone(target[key]); - return copy; -} - -export { deepClone }; diff --git a/yarn.lock b/yarn.lock index 8bb0c9e..05f69ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3946,10 +3946,10 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -mutative@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mutative/-/mutative-1.1.0.tgz#ff3e12a9903702e0487428510f8502dd4667e908" - integrity sha512-2PJADREjOusk3iJkD3rXV2YjAxTuaLxdfqtqTEt6vcY07LtEBR1seHuBHXWEIuscqRDGvbauYPs+A4Rj/KTczQ== +mutative@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mutative/-/mutative-1.2.0.tgz#db75b60b436ad0a940e2e47bffb6661dd77c2754" + integrity sha512-1muFw45Lwjso6TSBGiXfbjKS01fVSD/qaqBfTo/gXgp79e8KM4Sa1XP/S4iN2/DvSdIZgjFJI+JIhC7eKf3GTg== mute-stream@0.0.8: version "0.0.8"