patch-recorder 0.0.1 → 0.2.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.
- package/README.md +102 -89
- package/dist/arrays.d.ts +2 -2
- package/dist/arrays.d.ts.map +1 -1
- package/dist/arrays.js +69 -77
- package/dist/arrays.js.map +1 -1
- package/dist/index.d.ts +1 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -34
- package/dist/index.js.map +1 -1
- package/dist/maps.d.ts +2 -2
- package/dist/maps.d.ts.map +1 -1
- package/dist/maps.js +9 -27
- package/dist/maps.js.map +1 -1
- package/dist/optimizer.d.ts +16 -1
- package/dist/optimizer.d.ts.map +1 -1
- package/dist/optimizer.js +213 -60
- package/dist/optimizer.js.map +1 -1
- package/dist/patches.d.ts +5 -5
- package/dist/patches.d.ts.map +1 -1
- package/dist/patches.js +30 -6
- package/dist/patches.js.map +1 -1
- package/dist/proxy.d.ts +2 -2
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +25 -37
- package/dist/proxy.js.map +1 -1
- package/dist/sets.d.ts +2 -2
- package/dist/sets.d.ts.map +1 -1
- package/dist/sets.js +4 -20
- package/dist/sets.js.map +1 -1
- package/dist/types.d.ts +46 -32
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +29 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +109 -14
- package/dist/utils.js.map +1 -1
- package/package.json +2 -1
- package/src/arrays.ts +82 -102
- package/src/index.ts +9 -53
- package/src/maps.ts +16 -36
- package/src/optimizer.ts +265 -65
- package/src/patches.ts +48 -21
- package/src/proxy.ts +31 -46
- package/src/sets.ts +11 -30
- package/src/types.ts +50 -39
- package/src/utils.ts +127 -22
package/src/arrays.ts
CHANGED
|
@@ -1,64 +1,72 @@
|
|
|
1
|
-
import type {RecorderState} from './types.js';
|
|
2
|
-
import {Operation} from './types.js';
|
|
1
|
+
import type {NonPrimitive, PatchPath, RecorderState} from './types.js';
|
|
3
2
|
import {generateAddPatch, generateDeletePatch, generateReplacePatch} from './patches.js';
|
|
4
3
|
import {createProxy} from './proxy.js';
|
|
5
4
|
|
|
5
|
+
// Module-level Sets for O(1) lookup instead of O(n) array includes
|
|
6
|
+
const MUTATING_METHODS = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']);
|
|
7
|
+
|
|
8
|
+
const NON_MUTATING_METHODS = new Set([
|
|
9
|
+
'map',
|
|
10
|
+
'filter',
|
|
11
|
+
'reduce',
|
|
12
|
+
'reduceRight',
|
|
13
|
+
'forEach',
|
|
14
|
+
'find',
|
|
15
|
+
'findIndex',
|
|
16
|
+
'some',
|
|
17
|
+
'every',
|
|
18
|
+
'includes',
|
|
19
|
+
'indexOf',
|
|
20
|
+
'lastIndexOf',
|
|
21
|
+
'slice',
|
|
22
|
+
'concat',
|
|
23
|
+
'join',
|
|
24
|
+
'flat',
|
|
25
|
+
'flatMap',
|
|
26
|
+
'at',
|
|
27
|
+
]);
|
|
28
|
+
|
|
6
29
|
/**
|
|
7
30
|
* Handle array method calls and property access
|
|
8
31
|
*/
|
|
9
32
|
export function handleArrayGet(
|
|
10
|
-
|
|
33
|
+
array: unknown[],
|
|
11
34
|
prop: string,
|
|
12
|
-
path:
|
|
13
|
-
state: RecorderState<
|
|
35
|
+
path: PatchPath,
|
|
36
|
+
state: RecorderState<NonPrimitive>,
|
|
14
37
|
): any {
|
|
15
38
|
// Mutating methods
|
|
16
|
-
|
|
39
|
+
if (MUTATING_METHODS.has(prop)) {
|
|
40
|
+
return (...args: unknown[]) => {
|
|
41
|
+
// Optimized: only copy what's needed for each method
|
|
42
|
+
const oldLength = array.length;
|
|
43
|
+
let oldValue: unknown[] | null = null;
|
|
44
|
+
|
|
45
|
+
// Only create full copy for sort/reverse which need the entire old array
|
|
46
|
+
if (prop === 'sort' || prop === 'reverse') {
|
|
47
|
+
oldValue = [...array];
|
|
48
|
+
}
|
|
17
49
|
|
|
18
|
-
|
|
19
|
-
return (...args: any[]) => {
|
|
20
|
-
const oldValue = [...obj]; // Snapshot before mutation
|
|
21
|
-
const result = (Array.prototype as any)[prop].apply(obj, args);
|
|
50
|
+
const result = (Array.prototype as any)[prop].apply(array, args);
|
|
22
51
|
|
|
23
52
|
// Generate patches based on the method
|
|
24
|
-
generateArrayPatches(state,
|
|
53
|
+
generateArrayPatches(state, array, prop, args, result, path, oldValue, oldLength);
|
|
25
54
|
|
|
26
55
|
return result;
|
|
27
56
|
};
|
|
28
57
|
}
|
|
29
58
|
|
|
30
59
|
// Non-mutating methods - just return them bound to the array
|
|
31
|
-
|
|
32
|
-
|
|
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);
|
|
60
|
+
if (NON_MUTATING_METHODS.has(prop)) {
|
|
61
|
+
return (Array.prototype as any)[prop].bind(array);
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
// Property access
|
|
57
65
|
if (prop === 'length') {
|
|
58
|
-
return
|
|
66
|
+
return array.length;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
const value =
|
|
69
|
+
const value = array[prop as any];
|
|
62
70
|
|
|
63
71
|
// For numeric properties (array indices), check if the value is an object/array
|
|
64
72
|
// If so, return a proxy to enable nested mutation tracking
|
|
@@ -76,106 +84,77 @@ export function handleArrayGet(
|
|
|
76
84
|
* Generate patches for array mutations
|
|
77
85
|
*/
|
|
78
86
|
function generateArrayPatches(
|
|
79
|
-
state: RecorderState<
|
|
80
|
-
|
|
87
|
+
state: RecorderState<NonPrimitive>,
|
|
88
|
+
array: unknown[],
|
|
81
89
|
method: string,
|
|
82
|
-
args:
|
|
90
|
+
args: unknown[],
|
|
83
91
|
result: any,
|
|
84
|
-
path:
|
|
85
|
-
|
|
92
|
+
path: PatchPath,
|
|
93
|
+
oldArray: unknown[] | null,
|
|
94
|
+
oldLength: number,
|
|
86
95
|
) {
|
|
87
96
|
switch (method) {
|
|
88
97
|
case 'push': {
|
|
89
98
|
// Generate add patches for each new element
|
|
90
|
-
|
|
99
|
+
// oldLength is the starting index before push
|
|
91
100
|
args.forEach((value, i) => {
|
|
92
|
-
const index =
|
|
101
|
+
const index = oldLength + i;
|
|
93
102
|
generateAddPatch(state, [...path, index], value);
|
|
94
103
|
});
|
|
95
|
-
|
|
96
|
-
// Generate length patch if option is enabled
|
|
97
|
-
if (state.options.arrayLengthAssignment !== false) {
|
|
98
|
-
generateReplacePatch(state, [...path, 'length'], obj.length);
|
|
99
|
-
}
|
|
104
|
+
// No length patch when array grows (aligned with mutative)
|
|
100
105
|
break;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
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
109
|
if (state.options.arrayLengthAssignment !== false) {
|
|
110
|
-
|
|
110
|
+
// Generate length replace patch (mutative uses this instead of remove)
|
|
111
|
+
generateReplacePatch(state, [...path, 'length'], array.length, oldLength);
|
|
112
|
+
} else {
|
|
113
|
+
// When arrayLengthAssignment is false, generate remove patch for last element
|
|
114
|
+
generateDeletePatch(state, [...path, oldLength - 1], result);
|
|
111
115
|
}
|
|
112
116
|
break;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
case 'shift': {
|
|
116
|
-
//
|
|
120
|
+
// Remove first element (shifted elements are handled automatically by JSON Patch spec)
|
|
121
|
+
// We don't have oldValue here, but the result of shift() is the removed element
|
|
117
122
|
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
123
|
break;
|
|
130
124
|
}
|
|
131
125
|
|
|
132
126
|
case 'unshift': {
|
|
133
|
-
// Add new elements at the beginning
|
|
127
|
+
// Add new elements at the beginning (shifted elements are handled automatically by JSON Patch spec)
|
|
134
128
|
args.forEach((value, i) => {
|
|
135
129
|
generateAddPatch(state, [...path, i], value);
|
|
136
130
|
});
|
|
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
131
|
break;
|
|
150
132
|
}
|
|
151
133
|
|
|
152
134
|
case 'splice': {
|
|
153
|
-
const [start, deleteCount, ...addItems] = args;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
135
|
+
const [start, deleteCount = 0, ...addItems] = args as number[];
|
|
136
|
+
const actualStart = start < 0 ? Math.max(oldLength + start, 0) : Math.min(start, oldLength);
|
|
137
|
+
const actualDeleteCount = Math.min(deleteCount, oldLength - actualStart);
|
|
138
|
+
const minCount = Math.min(actualDeleteCount, addItems.length);
|
|
139
|
+
|
|
140
|
+
// For splice, we need the old values for delete operations
|
|
141
|
+
// Since we don't have oldValue, we need to track what was deleted
|
|
142
|
+
// The result of splice() is the array of deleted elements
|
|
143
|
+
const deletedElements = result as any[];
|
|
144
|
+
|
|
145
|
+
// First minCount elements: replace (overlap between add and delete)
|
|
146
|
+
for (let i = 0; i < minCount; i++) {
|
|
147
|
+
generateReplacePatch(state, [...path, actualStart + i], addItems[i], deletedElements[i]);
|
|
158
148
|
}
|
|
159
149
|
|
|
160
|
-
//
|
|
161
|
-
addItems.
|
|
162
|
-
generateAddPatch(state, [...path,
|
|
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
|
-
);
|
|
150
|
+
// Remaining add items: add
|
|
151
|
+
for (let i = minCount; i < addItems.length; i++) {
|
|
152
|
+
generateAddPatch(state, [...path, actualStart + i], addItems[i]);
|
|
174
153
|
}
|
|
175
154
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
155
|
+
// Remaining delete items: remove (generate in reverse order)
|
|
156
|
+
for (let i = actualDeleteCount - 1; i >= minCount; i--) {
|
|
157
|
+
generateDeletePatch(state, [...path, actualStart + i], deletedElements[i]);
|
|
179
158
|
}
|
|
180
159
|
|
|
181
160
|
break;
|
|
@@ -184,7 +163,8 @@ function generateArrayPatches(
|
|
|
184
163
|
case 'sort':
|
|
185
164
|
case 'reverse': {
|
|
186
165
|
// These reorder the entire array - generate full replace
|
|
187
|
-
|
|
166
|
+
// oldValue contains the array before the mutation
|
|
167
|
+
generateReplacePatch(state, path, array, oldArray);
|
|
188
168
|
break;
|
|
189
169
|
}
|
|
190
170
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import {createProxy} from './proxy.js';
|
|
2
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';
|
|
3
|
+
import type {NonPrimitive, Draft, RecordPatchesOptions, Patches} from './types.js';
|
|
11
4
|
|
|
12
5
|
/**
|
|
13
6
|
* Record JSON patches from mutations applied to an object, array, Map, or Set.
|
|
@@ -26,24 +19,18 @@ import type {
|
|
|
26
19
|
* console.log(state.user.name); // 'Jane' (mutated in place!)
|
|
27
20
|
* console.log(patches); // [{ op: 'replace', path: ['user', 'name'], value: 'Jane' }]
|
|
28
21
|
*/
|
|
29
|
-
export function recordPatches<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
): Patches<true> {
|
|
34
|
-
const internalPatchesOptions = {
|
|
35
|
-
pathAsArray: options.pathAsArray ?? true,
|
|
36
|
-
arrayLengthAssignment: options.arrayLengthAssignment ?? true,
|
|
37
|
-
};
|
|
38
|
-
|
|
22
|
+
export function recordPatches<
|
|
23
|
+
T extends NonPrimitive,
|
|
24
|
+
PatchesOption extends RecordPatchesOptions = {},
|
|
25
|
+
>(state: T, mutate: (state: Draft<T>) => void, options?: PatchesOption): Patches {
|
|
39
26
|
const recorderState = {
|
|
40
|
-
|
|
27
|
+
state,
|
|
41
28
|
patches: [],
|
|
42
29
|
basePath: [],
|
|
43
30
|
options: {
|
|
44
31
|
...options,
|
|
45
|
-
internalPatchesOptions,
|
|
46
32
|
},
|
|
33
|
+
proxyCache: new WeakMap(),
|
|
47
34
|
};
|
|
48
35
|
|
|
49
36
|
// Create proxy
|
|
@@ -53,42 +40,11 @@ export function recordPatches<T extends NonPrimitive>(
|
|
|
53
40
|
mutate(proxy);
|
|
54
41
|
|
|
55
42
|
// Return patches (optionally compressed)
|
|
56
|
-
if (options
|
|
43
|
+
if (options?.compressPatches !== false) {
|
|
57
44
|
return compressPatches(recorderState.patches);
|
|
58
45
|
}
|
|
59
46
|
|
|
60
|
-
return recorderState.patches as Patches
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Mutative-compatible API for easy switching between mutative and patch-recorder.
|
|
65
|
-
* Returns [state, patches] tuple like mutative does.
|
|
66
|
-
*
|
|
67
|
-
* Unlike mutative, this mutates the original object in place (state === originalState).
|
|
68
|
-
* The returned state is the same reference as the input state for API compatibility.
|
|
69
|
-
*
|
|
70
|
-
* @param state - The state to mutate and record patches from
|
|
71
|
-
* @param mutate - A function that receives a draft of the state and applies mutations
|
|
72
|
-
* @param options - Configuration options (enablePatches is forced but ignored - patches are always returned)
|
|
73
|
-
* @returns Tuple [state, patches] where state is the mutated state (same reference as input)
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* const state = { user: { name: 'John' } };
|
|
77
|
-
* const [nextState, patches] = create(state, (draft) => {
|
|
78
|
-
* draft.user.name = 'Jane';
|
|
79
|
-
* }, {enabledPatches: true});
|
|
80
|
-
* console.log(nextState === state); // true (mutated in place!)
|
|
81
|
-
* console.log(patches); // [{ op: 'replace', path: ['user', 'name'], value: 'Jane' }]
|
|
82
|
-
*/
|
|
83
|
-
export function create<T extends NonPrimitive>(
|
|
84
|
-
state: T,
|
|
85
|
-
mutate: (state: Draft<T>) => void,
|
|
86
|
-
options: RecordPatchesOptions & {enablePatches: true} = {enablePatches: true},
|
|
87
|
-
): [T, Patches<true>] {
|
|
88
|
-
// Extract enablePatches but ignore it (patches are always returned)
|
|
89
|
-
const {enablePatches, ...recordPatchesOptions} = options;
|
|
90
|
-
const patches = recordPatches(state, mutate, recordPatchesOptions);
|
|
91
|
-
return [state, patches];
|
|
47
|
+
return recorderState.patches as Patches;
|
|
92
48
|
}
|
|
93
49
|
|
|
94
50
|
// Re-export types
|
package/src/maps.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import type {RecorderState} from './types.js';
|
|
1
|
+
import type {PatchPath, RecorderState} from './types.js';
|
|
2
2
|
import {createProxy} from './proxy.js';
|
|
3
|
-
import {Operation} from './types.js';
|
|
4
3
|
import {generateAddPatch, generateDeletePatch, generateReplacePatch} from './patches.js';
|
|
5
|
-
import {cloneIfNeeded
|
|
4
|
+
import {cloneIfNeeded} from './utils.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Handle property access on Map objects
|
|
9
8
|
* Wraps mutating methods (set, delete, clear) to generate patches
|
|
10
9
|
*/
|
|
11
|
-
export function handleMapGet
|
|
12
|
-
obj: Map<
|
|
10
|
+
export function handleMapGet(
|
|
11
|
+
obj: Map<any, any>,
|
|
13
12
|
prop: string | symbol,
|
|
14
|
-
path:
|
|
13
|
+
path: PatchPath,
|
|
15
14
|
state: RecorderState<any>,
|
|
16
15
|
): any {
|
|
17
|
-
//
|
|
16
|
+
// Handle symbol properties - return the property value directly
|
|
17
|
+
// Symbol methods like Symbol.iterator should work normally
|
|
18
18
|
if (typeof prop === 'symbol') {
|
|
19
19
|
return (obj as any)[prop];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// Mutating methods
|
|
23
23
|
if (prop === 'set') {
|
|
24
|
-
return (key:
|
|
25
|
-
// Check if key
|
|
26
|
-
const existed =
|
|
24
|
+
return (key: any, value: any) => {
|
|
25
|
+
// Check if key exists BEFORE mutation (current state, not original)
|
|
26
|
+
const existed = obj.has(key);
|
|
27
27
|
const oldValue = obj.get(key);
|
|
28
28
|
const result = obj.set(key, value);
|
|
29
29
|
|
|
@@ -31,8 +31,8 @@ export function handleMapGet<K = any, V = any>(
|
|
|
31
31
|
const itemPath = [...path, key as any];
|
|
32
32
|
|
|
33
33
|
if (existed) {
|
|
34
|
-
// Key exists - replace
|
|
35
|
-
generateReplacePatch(state, itemPath, cloneIfNeeded(value));
|
|
34
|
+
// Key exists - replace (pass oldValue for getItemId)
|
|
35
|
+
generateReplacePatch(state, itemPath, cloneIfNeeded(value), oldValue);
|
|
36
36
|
} else {
|
|
37
37
|
// Key doesn't exist - add
|
|
38
38
|
generateAddPatch(state, itemPath, cloneIfNeeded(value));
|
|
@@ -43,7 +43,7 @@ export function handleMapGet<K = any, V = any>(
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (prop === 'delete') {
|
|
46
|
-
return (key:
|
|
46
|
+
return (key: any) => {
|
|
47
47
|
const oldValue = obj.get(key);
|
|
48
48
|
const result = obj.delete(key);
|
|
49
49
|
|
|
@@ -71,14 +71,12 @@ export function handleMapGet<K = any, V = any>(
|
|
|
71
71
|
|
|
72
72
|
// Non-mutating methods
|
|
73
73
|
if (prop === 'get') {
|
|
74
|
-
return (key:
|
|
74
|
+
return (key: any) => {
|
|
75
75
|
const value = obj.get(key);
|
|
76
76
|
|
|
77
|
-
// If the value is
|
|
77
|
+
// If the value is an object, return a proxy for nested mutation tracking
|
|
78
78
|
if (value != null && typeof value === 'object') {
|
|
79
|
-
|
|
80
|
-
return createProxy(value, [...path, key as any], state);
|
|
81
|
-
}
|
|
79
|
+
return createProxy(value, [...path, key as any], state);
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
return value;
|
|
@@ -100,21 +98,3 @@ export function handleMapGet<K = any, V = any>(
|
|
|
100
98
|
return (obj as any)[prop];
|
|
101
99
|
}
|
|
102
100
|
|
|
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
|
-
}
|