patch-recorder 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +337 -0
  3. package/dist/arrays.d.ts +6 -0
  4. package/dist/arrays.d.ts.map +1 -0
  5. package/dist/arrays.js +143 -0
  6. package/dist/arrays.js.map +1 -0
  7. package/dist/index.d.ts +21 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +44 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/maps.d.ts +7 -0
  12. package/dist/maps.d.ts.map +1 -0
  13. package/dist/maps.js +96 -0
  14. package/dist/maps.js.map +1 -0
  15. package/dist/optimizer.d.ts +7 -0
  16. package/dist/optimizer.d.ts.map +1 -0
  17. package/dist/optimizer.js +123 -0
  18. package/dist/optimizer.js.map +1 -0
  19. package/dist/patches.d.ts +18 -0
  20. package/dist/patches.d.ts.map +1 -0
  21. package/dist/patches.js +46 -0
  22. package/dist/patches.js.map +1 -0
  23. package/dist/proxy.d.ts +3 -0
  24. package/dist/proxy.d.ts.map +1 -0
  25. package/dist/proxy.js +127 -0
  26. package/dist/proxy.js.map +1 -0
  27. package/dist/sets.d.ts +7 -0
  28. package/dist/sets.d.ts.map +1 -0
  29. package/dist/sets.js +78 -0
  30. package/dist/sets.js.map +1 -0
  31. package/dist/types.d.ts +55 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +6 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/utils.d.ts +33 -0
  36. package/dist/utils.d.ts.map +1 -0
  37. package/dist/utils.js +132 -0
  38. package/dist/utils.js.map +1 -0
  39. package/package.json +60 -0
  40. package/src/arrays.ts +191 -0
  41. package/src/index.ts +71 -0
  42. package/src/maps.ts +120 -0
  43. package/src/optimizer.ts +136 -0
  44. package/src/patches.ts +67 -0
  45. package/src/proxy.ts +163 -0
  46. package/src/sets.ts +100 -0
  47. package/src/types.ts +67 -0
  48. package/src/utils.ts +150 -0
@@ -0,0 +1,33 @@
1
+ import type { PatchesOptions } from './types.js';
2
+ /**
3
+ * Type guard to check if a value is a plain object (not null, not array, not Map/Set)
4
+ */
5
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
6
+ /**
7
+ * Type guard to check if a value is an array
8
+ */
9
+ export declare function isArray(value: unknown): value is unknown[];
10
+ /**
11
+ * Type guard to check if a value is a Map
12
+ */
13
+ export declare function isMap(value: unknown): value is Map<unknown, unknown>;
14
+ /**
15
+ * Type guard to check if a value is a Set
16
+ */
17
+ export declare function isSet(value: unknown): value is Set<unknown>;
18
+ /**
19
+ * Format a path array to either array format or JSON Pointer string format
20
+ */
21
+ export declare function formatPath(path: (string | number)[], options: {
22
+ internalPatchesOptions: PatchesOptions;
23
+ }): string | (string | number)[];
24
+ /**
25
+ * Check if two values are deeply equal
26
+ */
27
+ export declare function isEqual(a: unknown, b: unknown): boolean;
28
+ /**
29
+ * Deep clone a value if it's an object/array, otherwise return as-is
30
+ * This is needed for patch values to avoid reference issues
31
+ */
32
+ export declare function cloneIfNeeded<T>(value: T): T;
33
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,YAAY,CAAC;AAE/C;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzE;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,EAAE,CAE1D;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAEpE;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAE3D;AAED;;GAEG;AACH,wBAAgB,UAAU,CACzB,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EACzB,OAAO,EAAE;IAAC,sBAAsB,EAAE,cAAc,CAAA;CAAC,GAC/C,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAkB9B;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CA2CvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAmC5C"}
package/dist/utils.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Type guard to check if a value is a plain object (not null, not array, not Map/Set)
3
+ */
4
+ export function isObject(value) {
5
+ return (typeof value === 'object' &&
6
+ value !== null &&
7
+ !Array.isArray(value) &&
8
+ !(value instanceof Map) &&
9
+ !(value instanceof Set));
10
+ }
11
+ /**
12
+ * Type guard to check if a value is an array
13
+ */
14
+ export function isArray(value) {
15
+ return Array.isArray(value);
16
+ }
17
+ /**
18
+ * Type guard to check if a value is a Map
19
+ */
20
+ export function isMap(value) {
21
+ return value instanceof Map;
22
+ }
23
+ /**
24
+ * Type guard to check if a value is a Set
25
+ */
26
+ export function isSet(value) {
27
+ return value instanceof Set;
28
+ }
29
+ /**
30
+ * Format a path array to either array format or JSON Pointer string format
31
+ */
32
+ export function formatPath(path, options) {
33
+ if (options.internalPatchesOptions &&
34
+ typeof options.internalPatchesOptions === 'object' &&
35
+ options.internalPatchesOptions.pathAsArray === false) {
36
+ // Convert to JSON Pointer string format
37
+ return path
38
+ .map((part) => {
39
+ if (typeof part === 'number') {
40
+ return String(part);
41
+ }
42
+ return '/' + String(part).replace(/~/g, '~0').replace(/\//g, '~1');
43
+ })
44
+ .join('');
45
+ }
46
+ return path;
47
+ }
48
+ /**
49
+ * Check if two values are deeply equal
50
+ */
51
+ export function isEqual(a, b) {
52
+ if (a === b)
53
+ return true;
54
+ if (a == null || b == null)
55
+ return a === b;
56
+ if (typeof a !== typeof b)
57
+ return false;
58
+ if (typeof a === 'object') {
59
+ if (Array.isArray(a) && Array.isArray(b)) {
60
+ if (a.length !== b.length)
61
+ return false;
62
+ for (let i = 0; i < a.length; i++) {
63
+ if (!isEqual(a[i], b[i]))
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ if (a instanceof Map && b instanceof Map) {
69
+ if (a.size !== b.size)
70
+ return false;
71
+ for (const [key, value] of a) {
72
+ if (!b.has(key) || !isEqual(value, b.get(key)))
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+ if (a instanceof Set && b instanceof Set) {
78
+ if (a.size !== b.size)
79
+ return false;
80
+ for (const value of a) {
81
+ if (!b.has(value))
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+ if (Object.keys(a).length !== Object.keys(b).length)
87
+ return false;
88
+ for (const key in a) {
89
+ if (!Object.prototype.hasOwnProperty.call(b, key) ||
90
+ !isEqual(a[key], b[key])) {
91
+ return false;
92
+ }
93
+ }
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+ /**
99
+ * Deep clone a value if it's an object/array, otherwise return as-is
100
+ * This is needed for patch values to avoid reference issues
101
+ */
102
+ export function cloneIfNeeded(value) {
103
+ if (value === null || typeof value !== 'object') {
104
+ return value;
105
+ }
106
+ if (Array.isArray(value)) {
107
+ return value.map((item) => cloneIfNeeded(item));
108
+ }
109
+ if (value instanceof Map) {
110
+ const clonedMap = new Map();
111
+ for (const [key, val] of value) {
112
+ clonedMap.set(cloneIfNeeded(key), cloneIfNeeded(val));
113
+ }
114
+ return clonedMap;
115
+ }
116
+ if (value instanceof Set) {
117
+ const clonedSet = new Set();
118
+ for (const item of value) {
119
+ clonedSet.add(cloneIfNeeded(item));
120
+ }
121
+ return clonedSet;
122
+ }
123
+ // Plain object
124
+ const cloned = {};
125
+ for (const key in value) {
126
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
127
+ cloned[key] = cloneIfNeeded(value[key]);
128
+ }
129
+ }
130
+ return cloned;
131
+ }
132
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "patch-recorder",
3
+ "version": "0.0.0",
4
+ "description": "Record JSON patches (RFC 6902) from mutations applied to objects, arrays, Maps, and Sets via a proxy interface. Mutates the original object in place while recording changes, preserving object references and avoiding memory and computational overhead from copying.",
5
+ "keywords": [
6
+ "patch",
7
+ "mutable",
8
+ "record",
9
+ "typescript",
10
+ "type-safe",
11
+ "lightweight",
12
+ "minimal"
13
+ ],
14
+ "author": "Ronan Sandford",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/wighawag/patch-recorder.git"
19
+ },
20
+ "homepage": "https://github.com/wighawag/patch-recorder#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/wighawag/patch-recorder/issues"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src"
40
+ ],
41
+ "sideEffects": false,
42
+ "devDependencies": {
43
+ "@types/node": "^25.0.10",
44
+ "as-soon": "^0.1.5",
45
+ "mutative": "^1.3.0",
46
+ "prettier": "^3.8.0",
47
+ "tsx": "^4.21.0",
48
+ "typescript": "^5.3.3",
49
+ "vitest": "^2.1.8"
50
+ },
51
+ "scripts": {
52
+ "format": "prettier --write .",
53
+ "format:check": "prettier --check .",
54
+ "build": "tsc",
55
+ "dev": "as-soon -w src pnpm build",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "benchmark": "tsx benchmark/index.ts"
59
+ }
60
+ }
package/src/arrays.ts ADDED
@@ -0,0 +1,191 @@
1
+ import type {RecorderState} from './types.js';
2
+ import {Operation} from './types.js';
3
+ import {generateAddPatch, generateDeletePatch, generateReplacePatch} from './patches.js';
4
+ import {createProxy} from './proxy.js';
5
+
6
+ /**
7
+ * Handle array method calls and property access
8
+ */
9
+ export function handleArrayGet(
10
+ obj: any[],
11
+ prop: string,
12
+ path: (string | number)[],
13
+ state: RecorderState<any>,
14
+ ): any {
15
+ // Mutating methods
16
+ const mutatingMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
17
+
18
+ if (mutatingMethods.includes(prop)) {
19
+ return (...args: any[]) => {
20
+ const oldValue = [...obj]; // Snapshot before mutation
21
+ const result = (Array.prototype as any)[prop].apply(obj, args);
22
+
23
+ // Generate patches based on the method
24
+ generateArrayPatches(state, obj, prop, args, result, path, oldValue);
25
+
26
+ return result;
27
+ };
28
+ }
29
+
30
+ // Non-mutating methods - just return them bound to the array
31
+ const nonMutatingMethods = [
32
+ 'map',
33
+ 'filter',
34
+ 'reduce',
35
+ 'reduceRight',
36
+ 'forEach',
37
+ 'find',
38
+ 'findIndex',
39
+ 'some',
40
+ 'every',
41
+ 'includes',
42
+ 'indexOf',
43
+ 'lastIndexOf',
44
+ 'slice',
45
+ 'concat',
46
+ 'join',
47
+ 'flat',
48
+ 'flatMap',
49
+ 'at',
50
+ ];
51
+
52
+ if (nonMutatingMethods.includes(prop)) {
53
+ return (Array.prototype as any)[prop].bind(obj);
54
+ }
55
+
56
+ // Property access
57
+ if (prop === 'length') {
58
+ return obj.length;
59
+ }
60
+
61
+ const value = obj[prop as any];
62
+
63
+ // For numeric properties (array indices), check if the value is an object/array
64
+ // If so, return a proxy to enable nested mutation tracking
65
+ if (!isNaN(Number(prop)) && typeof value === 'object' && value !== null) {
66
+ const index = Number(prop);
67
+ return createProxy(value, [...path, index], state);
68
+ }
69
+
70
+ // For numeric properties (array indices), return the value
71
+ // The main proxy handler's get will handle creating proxies for nested objects
72
+ return value;
73
+ }
74
+
75
+ /**
76
+ * Generate patches for array mutations
77
+ */
78
+ function generateArrayPatches(
79
+ state: RecorderState<any>,
80
+ obj: any[],
81
+ method: string,
82
+ args: any[],
83
+ result: any,
84
+ path: (string | number)[],
85
+ oldValue: any[],
86
+ ) {
87
+ switch (method) {
88
+ case 'push': {
89
+ // Generate add patches for each new element
90
+ const startIndex = oldValue.length;
91
+ args.forEach((value, i) => {
92
+ const index = startIndex + i;
93
+ generateAddPatch(state, [...path, index], value);
94
+ });
95
+
96
+ // Generate length patch if option is enabled
97
+ if (state.options.arrayLengthAssignment !== false) {
98
+ generateReplacePatch(state, [...path, 'length'], obj.length);
99
+ }
100
+ break;
101
+ }
102
+
103
+ case 'pop': {
104
+ // Generate remove patch for the removed element
105
+ const removedIndex = oldValue.length - 1;
106
+ generateDeletePatch(state, [...path, removedIndex], result);
107
+
108
+ // Generate length patch if option is enabled
109
+ if (state.options.arrayLengthAssignment !== false) {
110
+ generateReplacePatch(state, [...path, 'length'], obj.length);
111
+ }
112
+ break;
113
+ }
114
+
115
+ case 'shift': {
116
+ // Generate remove patch for the removed element
117
+ generateDeletePatch(state, [...path, 0], result);
118
+
119
+ // Shift is complex - we need to update all remaining elements
120
+ // Update all shifted elements (after the shift, each element moves to index - 1)
121
+ for (let i = 0; i < obj.length; i++) {
122
+ generateReplacePatch(state, [...path, i], oldValue[i + 1]);
123
+ }
124
+
125
+ // Generate length patch if option is enabled
126
+ if (state.options.arrayLengthAssignment !== false) {
127
+ generateReplacePatch(state, [...path, 'length'], obj.length);
128
+ }
129
+ break;
130
+ }
131
+
132
+ case 'unshift': {
133
+ // Add new elements at the beginning
134
+ args.forEach((value, i) => {
135
+ generateAddPatch(state, [...path, i], value);
136
+ });
137
+
138
+ // Update all existing elements
139
+
140
+ for (let i = 0; i < oldValue.length; i++) {
141
+ generateReplacePatch(state, [...path, i + args.length], oldValue[i]);
142
+ }
143
+
144
+ // Generate length patch if option is enabled
145
+ if (state.options.arrayLengthAssignment !== false) {
146
+ generateReplacePatch(state, [...path, 'length'], obj.length);
147
+ }
148
+
149
+ break;
150
+ }
151
+
152
+ case 'splice': {
153
+ const [start, deleteCount, ...addItems] = args;
154
+
155
+ // Generate remove patches for deleted items
156
+ for (let i = 0; i < deleteCount; i++) {
157
+ generateDeletePatch(state, [...path, start], oldValue[start]);
158
+ }
159
+
160
+ // Generate add patches for new items
161
+ addItems.forEach((item, i) => {
162
+ generateAddPatch(state, [...path, start + i], item);
163
+ });
164
+
165
+ // If there are both deletions and additions, update the shifted elements
166
+
167
+ const itemsToShift = oldValue.length - start - deleteCount;
168
+ for (let i = 0; i < itemsToShift; i++) {
169
+ generateReplacePatch(
170
+ state,
171
+ [...path, start + addItems.length + i],
172
+ oldValue[start + deleteCount + i],
173
+ );
174
+ }
175
+
176
+ // Generate length patch if option is enabled
177
+ if (state.options.arrayLengthAssignment !== false) {
178
+ generateReplacePatch(state, [...path, 'length'], obj.length);
179
+ }
180
+
181
+ break;
182
+ }
183
+
184
+ case 'sort':
185
+ case 'reverse': {
186
+ // These reorder the entire array - generate full replace
187
+ generateReplacePatch(state, path, [...obj]);
188
+ break;
189
+ }
190
+ }
191
+ }
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ import {createProxy} from './proxy.js';
2
+ import {compressPatches} from './optimizer.js';
3
+ import type {
4
+ NonPrimitive,
5
+ Draft,
6
+ RecordPatchesOptions,
7
+ Patches,
8
+ Patch,
9
+ Operation,
10
+ } from './types.js';
11
+
12
+ /**
13
+ * Record JSON patches from mutations applied to an object, array, Map, or Set.
14
+ * Unlike mutative or immer, this mutates the original object in place while recording changes.
15
+ *
16
+ * @param state - The state to record patches from
17
+ * @param mutate - A function that receives a draft of the state and applies mutations
18
+ * @param options - Configuration options
19
+ * @returns Array of JSON patches (RFC 6902 compliant)
20
+ *
21
+ * @example
22
+ * const state = { user: { name: 'John' } };
23
+ * const patches = recordPatches(state, (draft) => {
24
+ * draft.user.name = 'Jane';
25
+ * });
26
+ * console.log(state.user.name); // 'Jane' (mutated in place!)
27
+ * console.log(patches); // [{ op: 'replace', path: ['user', 'name'], value: 'Jane' }]
28
+ */
29
+ export function recordPatches<T extends NonPrimitive>(
30
+ state: T,
31
+ mutate: (state: Draft<T>) => void,
32
+ options: RecordPatchesOptions = {},
33
+ ): Patches<true> {
34
+ const internalPatchesOptions = {
35
+ pathAsArray: options.pathAsArray ?? true,
36
+ arrayLengthAssignment: options.arrayLengthAssignment ?? true,
37
+ };
38
+
39
+ const recorderState = {
40
+ original: state,
41
+ patches: [],
42
+ basePath: [],
43
+ options: {
44
+ ...options,
45
+ internalPatchesOptions,
46
+ },
47
+ };
48
+
49
+ // Create proxy
50
+ const proxy = createProxy(state, [], recorderState) as Draft<T>;
51
+
52
+ // Apply mutations
53
+ mutate(proxy);
54
+
55
+ // Return patches (optionally optimized)
56
+ if (options.optimize) {
57
+ return compressPatches(recorderState.patches);
58
+ }
59
+
60
+ return recorderState.patches as Patches<true>;
61
+ }
62
+
63
+ // Re-export types
64
+ export type {
65
+ NonPrimitive,
66
+ Draft,
67
+ RecordPatchesOptions,
68
+ Patches,
69
+ Patch,
70
+ Operation,
71
+ } from './types.js';
package/src/maps.ts ADDED
@@ -0,0 +1,120 @@
1
+ import type {RecorderState} from './types.js';
2
+ import {createProxy} from './proxy.js';
3
+ import {Operation} from './types.js';
4
+ import {generateAddPatch, generateDeletePatch, generateReplacePatch} from './patches.js';
5
+ import {cloneIfNeeded, isMap, isArray} from './utils.js';
6
+
7
+ /**
8
+ * Handle property access on Map objects
9
+ * Wraps mutating methods (set, delete, clear) to generate patches
10
+ */
11
+ export function handleMapGet<K = any, V = any>(
12
+ obj: Map<K, V>,
13
+ prop: string | symbol,
14
+ path: (string | number)[],
15
+ state: RecorderState<any>,
16
+ ): any {
17
+ // Skip symbol properties
18
+ if (typeof prop === 'symbol') {
19
+ return (obj as any)[prop];
20
+ }
21
+
22
+ // Mutating methods
23
+ if (prop === 'set') {
24
+ return (key: K, value: V) => {
25
+ // Check if key existed BEFORE mutation
26
+ const existed = keyExistsInOriginal(state.original, path, key);
27
+ const oldValue = obj.get(key);
28
+ const result = obj.set(key, value);
29
+
30
+ // Generate patch
31
+ const itemPath = [...path, key as any];
32
+
33
+ if (existed) {
34
+ // Key exists - replace
35
+ generateReplacePatch(state, itemPath, cloneIfNeeded(value));
36
+ } else {
37
+ // Key doesn't exist - add
38
+ generateAddPatch(state, itemPath, cloneIfNeeded(value));
39
+ }
40
+
41
+ return result;
42
+ };
43
+ }
44
+
45
+ if (prop === 'delete') {
46
+ return (key: K) => {
47
+ const oldValue = obj.get(key);
48
+ const result = obj.delete(key);
49
+
50
+ if (result) {
51
+ const itemPath = [...path, key as any];
52
+ generateDeletePatch(state, itemPath, cloneIfNeeded(oldValue));
53
+ }
54
+
55
+ return result;
56
+ };
57
+ }
58
+
59
+ if (prop === 'clear') {
60
+ return () => {
61
+ const entries = Array.from(obj.entries());
62
+ obj.clear();
63
+
64
+ // Generate remove patches for all items
65
+ entries.forEach(([key, value]) => {
66
+ const itemPath = [...path, key as any];
67
+ generateDeletePatch(state, itemPath, cloneIfNeeded(value));
68
+ });
69
+ };
70
+ }
71
+
72
+ // Non-mutating methods
73
+ if (prop === 'get') {
74
+ return (key: K) => {
75
+ const value = obj.get(key);
76
+
77
+ // If the value is a Map, Array, or object, return a proxy
78
+ if (value != null && typeof value === 'object') {
79
+ if (isMap(value) || isArray(value)) {
80
+ return createProxy(value, [...path, key as any], state);
81
+ }
82
+ }
83
+
84
+ return value;
85
+ };
86
+ }
87
+
88
+ const nonMutatingMethods = ['has', 'keys', 'values', 'entries', 'forEach'];
89
+
90
+ if (nonMutatingMethods.includes(prop)) {
91
+ return (obj as any)[prop].bind(obj);
92
+ }
93
+
94
+ // Size property
95
+ if (prop === 'size') {
96
+ return obj.size;
97
+ }
98
+
99
+ // Return any other property
100
+ return (obj as any)[prop];
101
+ }
102
+
103
+ /**
104
+ * Navigate to the original Map at the given path and check if a key exists
105
+ * This is needed to check if a key existed before mutations
106
+ */
107
+ function keyExistsInOriginal(original: any, path: (string | number)[], key: any): boolean {
108
+ let current = original;
109
+ for (const part of path) {
110
+ if (current == null) return false;
111
+ current = current[part];
112
+ }
113
+
114
+ // If we reached a Map, check if the key exists
115
+ if (current instanceof Map) {
116
+ return current.has(key);
117
+ }
118
+
119
+ return false;
120
+ }