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.
- package/LICENSE +21 -0
- package/README.md +337 -0
- package/dist/arrays.d.ts +6 -0
- package/dist/arrays.d.ts.map +1 -0
- package/dist/arrays.js +143 -0
- package/dist/arrays.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/maps.d.ts +7 -0
- package/dist/maps.d.ts.map +1 -0
- package/dist/maps.js +96 -0
- package/dist/maps.js.map +1 -0
- package/dist/optimizer.d.ts +7 -0
- package/dist/optimizer.d.ts.map +1 -0
- package/dist/optimizer.js +123 -0
- package/dist/optimizer.js.map +1 -0
- package/dist/patches.d.ts +18 -0
- package/dist/patches.d.ts.map +1 -0
- package/dist/patches.js +46 -0
- package/dist/patches.js.map +1 -0
- package/dist/proxy.d.ts +3 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +127 -0
- package/dist/proxy.js.map +1 -0
- package/dist/sets.d.ts +7 -0
- package/dist/sets.d.ts.map +1 -0
- package/dist/sets.js +78 -0
- package/dist/sets.js.map +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +132 -0
- package/dist/utils.js.map +1 -0
- package/package.json +60 -0
- package/src/arrays.ts +191 -0
- package/src/index.ts +71 -0
- package/src/maps.ts +120 -0
- package/src/optimizer.ts +136 -0
- package/src/patches.ts +67 -0
- package/src/proxy.ts +163 -0
- package/src/sets.ts +100 -0
- package/src/types.ts +67 -0
- package/src/utils.ts +150 -0
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|