@dabble/patches 0.1.1
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 +632 -0
- package/dist/client/PatchDoc.d.ts +85 -0
- package/dist/client/PatchDoc.js +299 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +1 -0
- package/dist/event-signal.d.ts +31 -0
- package/dist/event-signal.js +40 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/json-patch/JSONPatch.d.ts +126 -0
- package/dist/json-patch/JSONPatch.js +221 -0
- package/dist/json-patch/applyPatch.d.ts +11 -0
- package/dist/json-patch/applyPatch.js +37 -0
- package/dist/json-patch/composePatch.d.ts +2 -0
- package/dist/json-patch/composePatch.js +38 -0
- package/dist/json-patch/createJSONPatch.d.ts +35 -0
- package/dist/json-patch/createJSONPatch.js +41 -0
- package/dist/json-patch/index.d.ts +9 -0
- package/dist/json-patch/index.js +8 -0
- package/dist/json-patch/invertPatch.d.ts +2 -0
- package/dist/json-patch/invertPatch.js +31 -0
- package/dist/json-patch/ops/add.d.ts +2 -0
- package/dist/json-patch/ops/add.js +52 -0
- package/dist/json-patch/ops/bitmask.d.ts +14 -0
- package/dist/json-patch/ops/bitmask.js +48 -0
- package/dist/json-patch/ops/copy.d.ts +2 -0
- package/dist/json-patch/ops/copy.js +34 -0
- package/dist/json-patch/ops/increment.d.ts +5 -0
- package/dist/json-patch/ops/increment.js +21 -0
- package/dist/json-patch/ops/index.d.ts +22 -0
- package/dist/json-patch/ops/index.js +25 -0
- package/dist/json-patch/ops/move.d.ts +2 -0
- package/dist/json-patch/ops/move.js +211 -0
- package/dist/json-patch/ops/remove.d.ts +2 -0
- package/dist/json-patch/ops/remove.js +31 -0
- package/dist/json-patch/ops/replace.d.ts +2 -0
- package/dist/json-patch/ops/replace.js +44 -0
- package/dist/json-patch/ops/test.d.ts +2 -0
- package/dist/json-patch/ops/test.js +22 -0
- package/dist/json-patch/ops/text.d.ts +2 -0
- package/dist/json-patch/ops/text.js +57 -0
- package/dist/json-patch/patchProxy.d.ts +41 -0
- package/dist/json-patch/patchProxy.js +125 -0
- package/dist/json-patch/state.d.ts +2 -0
- package/dist/json-patch/state.js +8 -0
- package/dist/json-patch/transformPatch.d.ts +19 -0
- package/dist/json-patch/transformPatch.js +37 -0
- package/dist/json-patch/types.d.ts +52 -0
- package/dist/json-patch/types.js +1 -0
- package/dist/json-patch/utils/deepEqual.d.ts +1 -0
- package/dist/json-patch/utils/deepEqual.js +33 -0
- package/dist/json-patch/utils/exit.d.ts +2 -0
- package/dist/json-patch/utils/exit.js +4 -0
- package/dist/json-patch/utils/get.d.ts +2 -0
- package/dist/json-patch/utils/get.js +6 -0
- package/dist/json-patch/utils/getOpData.d.ts +2 -0
- package/dist/json-patch/utils/getOpData.js +10 -0
- package/dist/json-patch/utils/getType.d.ts +3 -0
- package/dist/json-patch/utils/getType.js +6 -0
- package/dist/json-patch/utils/index.d.ts +14 -0
- package/dist/json-patch/utils/index.js +14 -0
- package/dist/json-patch/utils/log.d.ts +2 -0
- package/dist/json-patch/utils/log.js +7 -0
- package/dist/json-patch/utils/ops.d.ts +14 -0
- package/dist/json-patch/utils/ops.js +103 -0
- package/dist/json-patch/utils/paths.d.ts +9 -0
- package/dist/json-patch/utils/paths.js +53 -0
- package/dist/json-patch/utils/pluck.d.ts +5 -0
- package/dist/json-patch/utils/pluck.js +30 -0
- package/dist/json-patch/utils/shallowCopy.d.ts +1 -0
- package/dist/json-patch/utils/shallowCopy.js +20 -0
- package/dist/json-patch/utils/softWrites.d.ts +7 -0
- package/dist/json-patch/utils/softWrites.js +18 -0
- package/dist/json-patch/utils/toArrayIndex.d.ts +1 -0
- package/dist/json-patch/utils/toArrayIndex.js +12 -0
- package/dist/json-patch/utils/toKeys.d.ts +1 -0
- package/dist/json-patch/utils/toKeys.js +15 -0
- package/dist/json-patch/utils/updateArrayIndexes.d.ts +5 -0
- package/dist/json-patch/utils/updateArrayIndexes.js +38 -0
- package/dist/json-patch/utils/updateArrayPath.d.ts +5 -0
- package/dist/json-patch/utils/updateArrayPath.js +45 -0
- package/dist/net/AbstractTransport.d.ts +47 -0
- package/dist/net/AbstractTransport.js +37 -0
- package/dist/net/PatchesOfflineFirst.d.ts +3 -0
- package/dist/net/PatchesOfflineFirst.js +3 -0
- package/dist/net/PatchesRealtime.d.ts +90 -0
- package/dist/net/PatchesRealtime.js +257 -0
- package/dist/net/index.d.ts +9 -0
- package/dist/net/index.js +8 -0
- package/dist/net/protocol/JSONRPCClient.d.ts +55 -0
- package/dist/net/protocol/JSONRPCClient.js +106 -0
- package/dist/net/protocol/types.d.ts +142 -0
- package/dist/net/protocol/types.js +1 -0
- package/dist/net/webrtc/WebRTCAwareness.d.ts +81 -0
- package/dist/net/webrtc/WebRTCAwareness.js +119 -0
- package/dist/net/webrtc/WebRTCTransport.d.ts +80 -0
- package/dist/net/webrtc/WebRTCTransport.js +157 -0
- package/dist/net/websocket/PatchesWebSocket.d.ts +107 -0
- package/dist/net/websocket/PatchesWebSocket.js +144 -0
- package/dist/net/websocket/SignalingService.d.ts +91 -0
- package/dist/net/websocket/SignalingService.js +140 -0
- package/dist/net/websocket/WebSocketTransport.d.ts +47 -0
- package/dist/net/websocket/WebSocketTransport.js +138 -0
- package/dist/persist/IndexedDBStore.d.ts +72 -0
- package/dist/persist/IndexedDBStore.js +283 -0
- package/dist/persist/index.d.ts +2 -0
- package/dist/persist/index.js +1 -0
- package/dist/server/BranchManager.d.ts +40 -0
- package/dist/server/BranchManager.js +138 -0
- package/dist/server/HistoryManager.d.ts +63 -0
- package/dist/server/HistoryManager.js +92 -0
- package/dist/server/PatchServer.d.ts +129 -0
- package/dist/server/PatchServer.js +358 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +3 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.js +83 -0
- package/package.json +78 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Based on work from
|
|
3
|
+
* https://github.com/mohayonao/json-touch-patch
|
|
4
|
+
* (c) 2018 mohayonao
|
|
5
|
+
*
|
|
6
|
+
* MIT license
|
|
7
|
+
* (c) 2022 Jacob Wright
|
|
8
|
+
*
|
|
9
|
+
*
|
|
10
|
+
* WARNING: using /array/- syntax to indicate the end of the array makes it impossible to transform arrays correctly in
|
|
11
|
+
* all situaions. Please avoid using this syntax when using Operational Transformations.
|
|
12
|
+
*/
|
|
13
|
+
import { Delta } from '@dabble/delta';
|
|
14
|
+
import { applyPatch } from './applyPatch.js';
|
|
15
|
+
import { composePatch } from './composePatch.js';
|
|
16
|
+
import { invertPatch } from './invertPatch.js';
|
|
17
|
+
import { bitmask } from './ops/bitmask.js';
|
|
18
|
+
import { transformPatch } from './transformPatch.js';
|
|
19
|
+
/**
|
|
20
|
+
* A JSONPatch helps with creating and applying one or more "JSON patches". It can track one or more changes
|
|
21
|
+
* together which may form a single operation or transaction.
|
|
22
|
+
*/
|
|
23
|
+
export class JSONPatch {
|
|
24
|
+
/**
|
|
25
|
+
* Create a new JSONPatch, optionally with an existing array of operations.
|
|
26
|
+
*/
|
|
27
|
+
constructor(ops = [], custom = {}) {
|
|
28
|
+
this.ops = ops;
|
|
29
|
+
this.custom = custom;
|
|
30
|
+
}
|
|
31
|
+
op(op, path, value, from, soft) {
|
|
32
|
+
path = checkPath(path);
|
|
33
|
+
if (from !== undefined) {
|
|
34
|
+
from = checkPath(from);
|
|
35
|
+
}
|
|
36
|
+
const patchOp = (from ? { op, from, path } : { op, path });
|
|
37
|
+
if (value !== undefined)
|
|
38
|
+
patchOp.value = value;
|
|
39
|
+
if (soft)
|
|
40
|
+
patchOp.soft = soft;
|
|
41
|
+
this.ops.push(patchOp);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Tests a value exists. If it doesn't, the patch is not applied.
|
|
46
|
+
*/
|
|
47
|
+
test(path, value) {
|
|
48
|
+
return this.op('test', path, value);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Adds the value to an object or array, inserted before the given index.
|
|
52
|
+
*/
|
|
53
|
+
add(path, value, options) {
|
|
54
|
+
if (value && value.toJSON)
|
|
55
|
+
value = value.toJSON();
|
|
56
|
+
return this.op('add', path, value, undefined, options?.soft);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Deletes the value at the given path or removes it from an array.
|
|
60
|
+
*/
|
|
61
|
+
remove(path) {
|
|
62
|
+
return this.op('remove', path);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Replaces a value (same as remove+add).
|
|
66
|
+
*/
|
|
67
|
+
replace(path, value, options) {
|
|
68
|
+
if (value && value.toJSON)
|
|
69
|
+
value = value.toJSON();
|
|
70
|
+
return this.op('replace', path, value, undefined, options?.soft);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Copies the value at `from` to `path`.
|
|
74
|
+
*/
|
|
75
|
+
copy(from, to, options) {
|
|
76
|
+
return this.op('copy', to, undefined, from, options?.soft);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Moves the value at `from` to `path`.
|
|
80
|
+
*/
|
|
81
|
+
move(from, to) {
|
|
82
|
+
if (from === to)
|
|
83
|
+
return this;
|
|
84
|
+
return this.op('move', to, undefined, from);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Increments a numeric value by 1 or the given amount.
|
|
88
|
+
*/
|
|
89
|
+
increment(path, value = 1) {
|
|
90
|
+
return this.op('@inc', path, value);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Decrements a numeric value by 1 or the given amount.
|
|
94
|
+
*/
|
|
95
|
+
decrement(path, value = 1) {
|
|
96
|
+
return this.op('@inc', path, -value);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Flips a bit at the given index in a bitmask to the given value.
|
|
100
|
+
*/
|
|
101
|
+
bit(path, index, on) {
|
|
102
|
+
return this.op('@bit', path, bitmask(index, on));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Applies a delta to a text document.
|
|
106
|
+
*/
|
|
107
|
+
text(path, value) {
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
value = new Delta(value);
|
|
110
|
+
}
|
|
111
|
+
else if (!(value instanceof Delta) && Array.isArray(value?.ops)) {
|
|
112
|
+
value = new Delta(value.ops);
|
|
113
|
+
}
|
|
114
|
+
else if (!(value instanceof Delta)) {
|
|
115
|
+
throw new Error('Invalid Delta');
|
|
116
|
+
}
|
|
117
|
+
return this.op('@txt', path, value);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Creates a patch from an object partial, updating each field. Set a field to undefined to delete it.
|
|
121
|
+
*/
|
|
122
|
+
addUpdates(updates, path = '/') {
|
|
123
|
+
if (path[path.length - 1] !== '/')
|
|
124
|
+
path += '/';
|
|
125
|
+
Object.keys(updates).forEach(key => {
|
|
126
|
+
const value = updates[key];
|
|
127
|
+
if (value == undefined) {
|
|
128
|
+
this.remove(path + key);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.replace(path + key, value);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* This will ensure an "add empty object" operation is created for each property along the path that does not exist.
|
|
138
|
+
*/
|
|
139
|
+
addObjectsInPath(obj, path) {
|
|
140
|
+
path = checkPath(path);
|
|
141
|
+
const parts = path.split('/');
|
|
142
|
+
for (var i = 1; i < parts.length - 1; i++) {
|
|
143
|
+
const prop = parts[i];
|
|
144
|
+
if (!obj || !obj[prop]) {
|
|
145
|
+
this.add(parts.slice(0, i + 1).join('/'), {});
|
|
146
|
+
}
|
|
147
|
+
obj = obj && obj[prop];
|
|
148
|
+
}
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Apply this patch to an object, returning a new object with the applied changes (or the same object if nothing
|
|
153
|
+
* changed in the patch). Optionally apply the page at the given path prefix.
|
|
154
|
+
*/
|
|
155
|
+
apply(obj, options) {
|
|
156
|
+
return applyPatch(obj, this.ops, options, this.custom);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Transform the given patch against this one. This patch is considered to have happened first. Optionally provide
|
|
160
|
+
* the object these operations are being applied to if available to know for sure if a numerical path is an array
|
|
161
|
+
* index or object key. Otherwise, all numerical paths are treated as array indexes.
|
|
162
|
+
*/
|
|
163
|
+
transform(patch, obj) {
|
|
164
|
+
const JSONPatch = this.constructor;
|
|
165
|
+
return new JSONPatch(transformPatch(obj, this.ops, Array.isArray(patch) ? patch : patch.ops, this.custom), this.custom);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Create a patch which can reverse what this patch does. Because JSON Patches do not store previous values, you
|
|
169
|
+
* must provide the previous object to create a reverse patch.
|
|
170
|
+
*/
|
|
171
|
+
invert(obj) {
|
|
172
|
+
const JSONPatch = this.constructor;
|
|
173
|
+
return new JSONPatch(invertPatch(obj, this.ops, this.custom), this.custom);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Compose/collapse patches into fewer operations.
|
|
177
|
+
*/
|
|
178
|
+
compose(patch) {
|
|
179
|
+
const JSONPatch = this.constructor;
|
|
180
|
+
let ops = this.ops;
|
|
181
|
+
if (patch)
|
|
182
|
+
ops = ops.concat(Array.isArray(patch) ? patch : patch.ops);
|
|
183
|
+
return new JSONPatch(composePatch(ops), this.custom);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Add two patches together.
|
|
187
|
+
*/
|
|
188
|
+
concat(patch) {
|
|
189
|
+
const JSONPatch = this.constructor;
|
|
190
|
+
return new JSONPatch(this.ops.concat(Array.isArray(patch) ? patch : patch.ops), this.custom);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Returns an array of patch operations.
|
|
194
|
+
*/
|
|
195
|
+
toJSON() {
|
|
196
|
+
return this.ops.slice();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Create a new JSONPatch with the provided JSON patch operations.
|
|
200
|
+
*/
|
|
201
|
+
static fromJSON(ops, types) {
|
|
202
|
+
return new this(ops, types);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function checkPath(path) {
|
|
206
|
+
path = path.toString();
|
|
207
|
+
if (path.length && path[0] !== '/')
|
|
208
|
+
path = `/${path}`;
|
|
209
|
+
return path;
|
|
210
|
+
}
|
|
211
|
+
export function createJSONPath() {
|
|
212
|
+
const handler = {
|
|
213
|
+
get(target, prop) {
|
|
214
|
+
if (prop === 'toString') {
|
|
215
|
+
return () => target.path ?? '';
|
|
216
|
+
}
|
|
217
|
+
return new Proxy({ path: `${target.path}/${prop}` }, handler);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
return new Proxy({ path: '' }, handler);
|
|
221
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ApplyJSONPatchOptions, JSONPatchOp, JSONPatchOpHandlerMap } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Applies a sequence of JSON patch operations to an object.
|
|
4
|
+
*
|
|
5
|
+
* @param object - The object to apply the patches to
|
|
6
|
+
* @param patches - The JSON patch operations to apply
|
|
7
|
+
* @param opts - Options for applying the patch
|
|
8
|
+
* @param custom - Custom patch operation handlers
|
|
9
|
+
* @returns The object after applying the patches
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyPatch(object: any, patches: JSONPatchOp[], opts?: ApplyJSONPatchOptions, custom?: JSONPatchOpHandlerMap): any;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getTypes } from './ops/index.js';
|
|
2
|
+
import { runWithObject } from './state.js';
|
|
3
|
+
import { exit } from './utils/exit.js';
|
|
4
|
+
import { getType } from './utils/getType.js';
|
|
5
|
+
/**
|
|
6
|
+
* Applies a sequence of JSON patch operations to an object.
|
|
7
|
+
*
|
|
8
|
+
* @param object - The object to apply the patches to
|
|
9
|
+
* @param patches - The JSON patch operations to apply
|
|
10
|
+
* @param opts - Options for applying the patch
|
|
11
|
+
* @param custom - Custom patch operation handlers
|
|
12
|
+
* @returns The object after applying the patches
|
|
13
|
+
*/
|
|
14
|
+
export function applyPatch(object, patches, opts = {}, custom) {
|
|
15
|
+
if (patches.length === 0) {
|
|
16
|
+
return object;
|
|
17
|
+
}
|
|
18
|
+
if (opts.atPath) {
|
|
19
|
+
patches = patches.map(op => ({ ...op, path: opts.atPath + op.path }));
|
|
20
|
+
}
|
|
21
|
+
const types = getTypes(custom);
|
|
22
|
+
return runWithObject(object, types, patches.length > 1, state => {
|
|
23
|
+
for (let i = 0, imax = patches.length; i < imax; i++) {
|
|
24
|
+
const patch = patches[i];
|
|
25
|
+
const handler = getType(state, patch)?.apply;
|
|
26
|
+
const error = handler ? handler(state, '' + patch.path, patch.from || patch.value) : `[op:${patch.op}] unknown`;
|
|
27
|
+
if (error) {
|
|
28
|
+
if ((!opts.silent && !opts.strict) || opts.silent === false)
|
|
29
|
+
console.error(error, patch);
|
|
30
|
+
if (opts.strict)
|
|
31
|
+
throw new TypeError(error);
|
|
32
|
+
if (opts.rigid)
|
|
33
|
+
return exit(state, object, patch, opts);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getTypes } from './ops/index.js';
|
|
2
|
+
import { runWithObject } from './state.js';
|
|
3
|
+
import { getType } from './utils/getType.js';
|
|
4
|
+
import { mapAndFilterOps } from './utils/ops.js';
|
|
5
|
+
import { getValue } from './utils/pluck.js';
|
|
6
|
+
export function composePatch(patches, custom = {}) {
|
|
7
|
+
const types = getTypes(custom);
|
|
8
|
+
const opsByPath = new Map();
|
|
9
|
+
// Only composing ops next to each other on the same path. It becomes too complex to do more because of moves and arrays
|
|
10
|
+
return runWithObject(null, types, patches.length > 1, state => {
|
|
11
|
+
return mapAndFilterOps(patches, op => {
|
|
12
|
+
const type = getType(state, op);
|
|
13
|
+
const handler = type?.compose;
|
|
14
|
+
if (handler) {
|
|
15
|
+
const lastOp = opsByPath.get(op.path);
|
|
16
|
+
if (lastOp && match(lastOp, op)) {
|
|
17
|
+
lastOp.value = handler(state, lastOp.value, op.value);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const prefix = `${op.path}/`;
|
|
22
|
+
for (const path of opsByPath.keys()) {
|
|
23
|
+
if (path.startsWith(prefix))
|
|
24
|
+
opsByPath.delete(path);
|
|
25
|
+
}
|
|
26
|
+
opsByPath.set(op.path, (op = getValue(state, op)));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
opsByPath.clear();
|
|
31
|
+
}
|
|
32
|
+
return op;
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function match(op1, op2) {
|
|
37
|
+
return op1 && op2 && op1.op === op2.op && op1.path === op2.path;
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JSONPatch } from './JSONPatch';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a `JSONPatch` instance by tracking changes made to a proxy object within an updater function.
|
|
4
|
+
*
|
|
5
|
+
* This provides a convenient way to generate patches based on direct object manipulation.
|
|
6
|
+
* The `updater` function receives a proxy of the `target` object and the `JSONPatch` instance.
|
|
7
|
+
* Modifications made to the proxy object (setting properties, calling array methods)
|
|
8
|
+
* are automatically converted into JSON Patch operations and added to the patch instance.
|
|
9
|
+
* You can also directly call methods on the `patch` instance within the updater.
|
|
10
|
+
*
|
|
11
|
+
* @template T The type of the target object.
|
|
12
|
+
* @param target The initial state of the object.
|
|
13
|
+
* @param updater A function that receives a proxy of the target and a `JSONPatch` instance.
|
|
14
|
+
* Modify the proxy or call patch methods within this function to generate operations.
|
|
15
|
+
* @returns A `JSONPatch` instance containing the operations generated within the updater.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const myObj = { name: { first: 'Alice' }, age: 30, tags: ['a'] };
|
|
20
|
+
*
|
|
21
|
+
* const patch = createJSONPatch(myObj, (proxy, p) => {
|
|
22
|
+
* proxy.name.first = 'Bob'; // Generates a 'replace' op via the proxy
|
|
23
|
+
* proxy.tags.push('b'); // Generates an 'add' op via the proxy
|
|
24
|
+
* p.increment(proxy.age, 1); // Directly adds an 'increment' op
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* console.log(patch.ops);
|
|
28
|
+
* // [
|
|
29
|
+
* // { op: 'replace', path: '/name/first', value: 'Bob' },
|
|
30
|
+
* // { op: 'add', path: '/tags/1', value: 'b' },
|
|
31
|
+
* // { op: 'increment', path: '/age', value: 1 }
|
|
32
|
+
* // ]
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function createJSONPatch<T>(target: T, updater: (proxy: T, patch: JSONPatch) => void): JSONPatch;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { JSONPatch } from './JSONPatch';
|
|
2
|
+
import { createPatchProxy } from './patchProxy';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a `JSONPatch` instance by tracking changes made to a proxy object within an updater function.
|
|
5
|
+
*
|
|
6
|
+
* This provides a convenient way to generate patches based on direct object manipulation.
|
|
7
|
+
* The `updater` function receives a proxy of the `target` object and the `JSONPatch` instance.
|
|
8
|
+
* Modifications made to the proxy object (setting properties, calling array methods)
|
|
9
|
+
* are automatically converted into JSON Patch operations and added to the patch instance.
|
|
10
|
+
* You can also directly call methods on the `patch` instance within the updater.
|
|
11
|
+
*
|
|
12
|
+
* @template T The type of the target object.
|
|
13
|
+
* @param target The initial state of the object.
|
|
14
|
+
* @param updater A function that receives a proxy of the target and a `JSONPatch` instance.
|
|
15
|
+
* Modify the proxy or call patch methods within this function to generate operations.
|
|
16
|
+
* @returns A `JSONPatch` instance containing the operations generated within the updater.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const myObj = { name: { first: 'Alice' }, age: 30, tags: ['a'] };
|
|
21
|
+
*
|
|
22
|
+
* const patch = createJSONPatch(myObj, (proxy, p) => {
|
|
23
|
+
* proxy.name.first = 'Bob'; // Generates a 'replace' op via the proxy
|
|
24
|
+
* proxy.tags.push('b'); // Generates an 'add' op via the proxy
|
|
25
|
+
* p.increment(proxy.age, 1); // Directly adds an 'increment' op
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* console.log(patch.ops);
|
|
29
|
+
* // [
|
|
30
|
+
* // { op: 'replace', path: '/name/first', value: 'Bob' },
|
|
31
|
+
* // { op: 'add', path: '/tags/1', value: 'b' },
|
|
32
|
+
* // { op: 'increment', path: '/age', value: 1 }
|
|
33
|
+
* // ]
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createJSONPatch(target, updater) {
|
|
37
|
+
const patch = new JSONPatch();
|
|
38
|
+
// Use the specific overload of createPatchProxy that takes target and patch
|
|
39
|
+
updater(createPatchProxy(target, patch), patch);
|
|
40
|
+
return patch;
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { applyPatch } from './applyPatch.js';
|
|
2
|
+
export { composePatch } from './composePatch.js';
|
|
3
|
+
export { invertPatch } from './invertPatch.js';
|
|
4
|
+
export { applyBitmask, bitmask, combineBitmasks } from './ops/bitmask.js';
|
|
5
|
+
export * as defaultOps from './ops/index.js';
|
|
6
|
+
export { transformPatch } from './transformPatch.js';
|
|
7
|
+
export * from './ops/index.js';
|
|
8
|
+
export * from './ops/index.js';
|
|
9
|
+
export type { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, JSONPatchOp } from './types.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { applyPatch } from './applyPatch.js';
|
|
2
|
+
export { composePatch } from './composePatch.js';
|
|
3
|
+
export { invertPatch } from './invertPatch.js';
|
|
4
|
+
export { applyBitmask, bitmask, combineBitmasks } from './ops/bitmask.js';
|
|
5
|
+
export * as defaultOps from './ops/index.js';
|
|
6
|
+
export { transformPatch } from './transformPatch.js';
|
|
7
|
+
export * from './ops/index.js'; // Exports all ops: add, remove, etc.
|
|
8
|
+
export * from './ops/index.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getTypes } from './ops/index.js';
|
|
2
|
+
import { runWithObject } from './state.js';
|
|
3
|
+
import { getType } from './utils/getType.js';
|
|
4
|
+
export function invertPatch(object, ops, custom = {}) {
|
|
5
|
+
const types = getTypes(custom);
|
|
6
|
+
return runWithObject({}, types, false, state => {
|
|
7
|
+
return ops
|
|
8
|
+
.map((op) => {
|
|
9
|
+
const pathParts = op.path.split('/').slice(1);
|
|
10
|
+
let changedObj = object;
|
|
11
|
+
const prop = pathParts.pop();
|
|
12
|
+
let value, isIndex;
|
|
13
|
+
try {
|
|
14
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
15
|
+
changedObj = changedObj[pathParts[i]];
|
|
16
|
+
}
|
|
17
|
+
value = changedObj[prop];
|
|
18
|
+
isIndex = prop >= 0;
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
throw new Error(`Patch mismatch. This patch was not applied to the provided object and cannot be inverted. ${err.message || err}`);
|
|
22
|
+
}
|
|
23
|
+
const handler = getType(state, op)?.invert;
|
|
24
|
+
if (!handler)
|
|
25
|
+
throw new Error('Unknown patch operation, cannot invert');
|
|
26
|
+
return handler(state, op, value, changedObj, isIndex);
|
|
27
|
+
})
|
|
28
|
+
.filter(op => !!op)
|
|
29
|
+
.reverse();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { deepEqual } from '../utils/deepEqual.js';
|
|
2
|
+
import { getOpData } from '../utils/getOpData.js';
|
|
3
|
+
import { isArrayPath, isEmptyObject, log, updateArrayIndexes, updateRemovedOps, updateSoftWrites, } from '../utils/index.js';
|
|
4
|
+
import { pluckWithShallowCopy } from '../utils/pluck.js';
|
|
5
|
+
import { toArrayIndex } from '../utils/toArrayIndex.js';
|
|
6
|
+
export const add = {
|
|
7
|
+
like: 'add',
|
|
8
|
+
apply(state, path, value) {
|
|
9
|
+
if (typeof value === 'undefined') {
|
|
10
|
+
return '[op:add] require value, but got undefined';
|
|
11
|
+
}
|
|
12
|
+
const [keys, lastKey, target] = getOpData(state, path, true);
|
|
13
|
+
if (target === null) {
|
|
14
|
+
return `[op:add] path not found: ${path}`;
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(target)) {
|
|
17
|
+
const index = toArrayIndex(target, lastKey);
|
|
18
|
+
if (target.length < index) {
|
|
19
|
+
return `[op:add] invalid array index: ${path}`;
|
|
20
|
+
}
|
|
21
|
+
pluckWithShallowCopy(state, keys, true).splice(index, 0, value);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (!deepEqual(target[lastKey], value)) {
|
|
25
|
+
pluckWithShallowCopy(state, keys, true)[lastKey] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
invert(_state, { path }, value, changedObj, isIndex) {
|
|
30
|
+
if (path.endsWith('/-'))
|
|
31
|
+
return { op: 'remove', path: path.replace('-', changedObj.length) };
|
|
32
|
+
else if (isIndex)
|
|
33
|
+
return { op: 'remove', path };
|
|
34
|
+
return value === undefined ? { op: 'remove', path } : { op: 'replace', path, value };
|
|
35
|
+
},
|
|
36
|
+
transform(state, thisOp, otherOps) {
|
|
37
|
+
log('Transforming', otherOps, 'against "add"', thisOp);
|
|
38
|
+
if (isArrayPath(thisOp.path, state)) {
|
|
39
|
+
// Adjust any operations on the same array by 1 to account for this new entry
|
|
40
|
+
return updateArrayIndexes(state, thisOp.path, otherOps, 1);
|
|
41
|
+
}
|
|
42
|
+
else if (isEmptyObject(thisOp.value)) {
|
|
43
|
+
// Treat empty objects special. If two empty objects are added to the same location, don't overwrite the existing
|
|
44
|
+
// one, allowing for the merging of maps together which did not exist before
|
|
45
|
+
return updateSoftWrites(thisOp.path, otherOps);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Remove anything that was done at this path since it is being overwritten by the add
|
|
49
|
+
return updateRemovedOps(state, thisOp.path, otherOps);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JSONPatchOpHandler } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create and maintain up to 15 boolean values in a single positive number (a bitmask). This can reduce the size of your
|
|
4
|
+
* JSON document. Limiting the bits to 15 allows us to keep the number smaller (max of 9 characters), positive, and
|
|
5
|
+
* allows us to combine multiple operations into a single number.
|
|
6
|
+
*/
|
|
7
|
+
export declare const bit: JSONPatchOpHandler;
|
|
8
|
+
/**
|
|
9
|
+
* A helper function to create a mask number for bitmask operations. The bottom 15 bits are used to turn bits on, and
|
|
10
|
+
* the top 15 bits are used to turn bits off.
|
|
11
|
+
*/
|
|
12
|
+
export declare function bitmask(index: number, value: boolean): number;
|
|
13
|
+
export declare function applyBitmask(num: number, mask: number): number;
|
|
14
|
+
export declare function combineBitmasks(a: number, b: number): number;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { get } from '../utils/get.js';
|
|
2
|
+
import { updateRemovedOps } from '../utils/ops.js';
|
|
3
|
+
import { replace } from './replace.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create and maintain up to 15 boolean values in a single positive number (a bitmask). This can reduce the size of your
|
|
6
|
+
* JSON document. Limiting the bits to 15 allows us to keep the number smaller (max of 9 characters), positive, and
|
|
7
|
+
* allows us to combine multiple operations into a single number.
|
|
8
|
+
*/
|
|
9
|
+
export const bit = {
|
|
10
|
+
like: 'replace',
|
|
11
|
+
apply(state, path, value) {
|
|
12
|
+
return replace.apply(state, path, applyBitmask(get(state, path) || 0, value || 0));
|
|
13
|
+
},
|
|
14
|
+
transform(state, thisOp, otherOps) {
|
|
15
|
+
return updateRemovedOps(state, thisOp.path, otherOps, false, true);
|
|
16
|
+
},
|
|
17
|
+
invert(state, op, value, changedObj, isIndex) {
|
|
18
|
+
return replace.invert(state, op, value, changedObj, isIndex);
|
|
19
|
+
},
|
|
20
|
+
compose(_state, value1, value2) {
|
|
21
|
+
return combineBitmasks(value1, value2);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A helper function to create a mask number for bitmask operations. The bottom 15 bits are used to turn bits on, and
|
|
26
|
+
* the top 15 bits are used to turn bits off.
|
|
27
|
+
*/
|
|
28
|
+
export function bitmask(index, value) {
|
|
29
|
+
if (index < 0 || index > 14)
|
|
30
|
+
throw new Error('Index must be between 0 and 14');
|
|
31
|
+
return value ? 1 << index : 1 << (index + 15);
|
|
32
|
+
}
|
|
33
|
+
export function applyBitmask(num, mask) {
|
|
34
|
+
const offMask = (mask >> 15) & 0x7fff;
|
|
35
|
+
const onMask = mask & 0x7fff;
|
|
36
|
+
num &= ~offMask;
|
|
37
|
+
num |= onMask;
|
|
38
|
+
return num;
|
|
39
|
+
}
|
|
40
|
+
export function combineBitmasks(a, b) {
|
|
41
|
+
const aOff = (a >> 15) & 0x7fff;
|
|
42
|
+
const aOn = a & 0x7fff;
|
|
43
|
+
const bOff = (b >> 15) & 0x7fff;
|
|
44
|
+
const bOn = b & 0x7fff;
|
|
45
|
+
const combinedOn = (aOn & ~bOff) | bOn;
|
|
46
|
+
const combinedOff = (aOff & ~bOn) | bOff;
|
|
47
|
+
return (combinedOff << 15) | combinedOn;
|
|
48
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getOpData } from '../utils/getOpData.js';
|
|
2
|
+
import { log } from '../utils/log.js';
|
|
3
|
+
import { updateRemovedOps } from '../utils/ops.js';
|
|
4
|
+
import { isArrayPath } from '../utils/paths.js';
|
|
5
|
+
import { updateArrayIndexes } from '../utils/updateArrayIndexes.js';
|
|
6
|
+
import { add } from './add.js';
|
|
7
|
+
export const copy = {
|
|
8
|
+
like: 'copy',
|
|
9
|
+
apply(state, path, from) {
|
|
10
|
+
const [, lastKey, target] = getOpData(state, from);
|
|
11
|
+
if (target === null) {
|
|
12
|
+
return `[op:copy] path not found: ${from}`;
|
|
13
|
+
}
|
|
14
|
+
return add.apply(state, path, target[lastKey]);
|
|
15
|
+
},
|
|
16
|
+
invert(_state, { path }, value, changedObj, isIndex) {
|
|
17
|
+
if (path.endsWith('/-'))
|
|
18
|
+
return { op: 'remove', path: path.replace('-', changedObj.length) };
|
|
19
|
+
else if (isIndex)
|
|
20
|
+
return { op: 'remove', path };
|
|
21
|
+
return value === undefined ? { op: 'remove', path } : { op: 'replace', path, value };
|
|
22
|
+
},
|
|
23
|
+
transform(state, thisOp, otherOps) {
|
|
24
|
+
log('Transforming', otherOps, 'against "add"', thisOp);
|
|
25
|
+
if (isArrayPath(thisOp.path, state)) {
|
|
26
|
+
// Adjust any operations on the same array by 1 to account for this new entry
|
|
27
|
+
return updateArrayIndexes(state, thisOp.path, otherOps, 1);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Remove anything that was done at this path since it is being overwritten
|
|
31
|
+
return updateRemovedOps(state, thisOp.path, otherOps);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { get } from '../utils/get.js';
|
|
2
|
+
import { updateRemovedOps } from '../utils/ops.js';
|
|
3
|
+
import { replace } from './replace.js';
|
|
4
|
+
/**
|
|
5
|
+
* Increment or decrement a number (using `+` or `-`).
|
|
6
|
+
*/
|
|
7
|
+
export const increment = {
|
|
8
|
+
like: 'replace',
|
|
9
|
+
apply(state, path, value) {
|
|
10
|
+
return replace.apply(state, path, (get(state, path) || 0) + value);
|
|
11
|
+
},
|
|
12
|
+
transform(state, thisOp, otherOps) {
|
|
13
|
+
return updateRemovedOps(state, thisOp.path, otherOps, false, true);
|
|
14
|
+
},
|
|
15
|
+
invert(state, op, value, changedObj, isIndex) {
|
|
16
|
+
return replace.invert(state, op, value, changedObj, isIndex);
|
|
17
|
+
},
|
|
18
|
+
compose(_state, value1, value2) {
|
|
19
|
+
return value1 + value2;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { JSONPatchOpHandlerMap } from '../types.js';
|
|
2
|
+
import { add } from './add.js';
|
|
3
|
+
import { bit } from './bitmask.js';
|
|
4
|
+
import { copy } from './copy.js';
|
|
5
|
+
import { increment } from './increment.js';
|
|
6
|
+
import { move } from './move.js';
|
|
7
|
+
import { remove } from './remove.js';
|
|
8
|
+
import { replace } from './replace.js';
|
|
9
|
+
import { test } from './test.js';
|
|
10
|
+
export * from './bitmask.js';
|
|
11
|
+
export { add, bit, copy, increment, move, remove, replace, test };
|
|
12
|
+
export declare function getTypes(custom?: JSONPatchOpHandlerMap): {
|
|
13
|
+
test: import("../types.js").JSONPatchOpHandler;
|
|
14
|
+
add: import("../types.js").JSONPatchOpHandler;
|
|
15
|
+
remove: import("../types.js").JSONPatchOpHandler;
|
|
16
|
+
replace: import("../types.js").JSONPatchOpHandler;
|
|
17
|
+
copy: import("../types.js").JSONPatchOpHandler;
|
|
18
|
+
move: import("../types.js").JSONPatchOpHandler;
|
|
19
|
+
'@inc': import("../types.js").JSONPatchOpHandler;
|
|
20
|
+
'@bit': import("../types.js").JSONPatchOpHandler;
|
|
21
|
+
'@txt': import("../types.js").JSONPatchOpHandler;
|
|
22
|
+
};
|