@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,25 @@
|
|
|
1
|
+
import { add } from './add.js';
|
|
2
|
+
import { bit } from './bitmask.js';
|
|
3
|
+
import { copy } from './copy.js';
|
|
4
|
+
import { increment } from './increment.js';
|
|
5
|
+
import { move } from './move.js';
|
|
6
|
+
import { remove } from './remove.js';
|
|
7
|
+
import { replace } from './replace.js';
|
|
8
|
+
import { test } from './test.js';
|
|
9
|
+
import { text } from './text.js';
|
|
10
|
+
export * from './bitmask.js';
|
|
11
|
+
export { add, bit, copy, increment, move, remove, replace, test };
|
|
12
|
+
export function getTypes(custom) {
|
|
13
|
+
return {
|
|
14
|
+
test,
|
|
15
|
+
add,
|
|
16
|
+
remove,
|
|
17
|
+
replace,
|
|
18
|
+
copy,
|
|
19
|
+
move,
|
|
20
|
+
'@inc': increment,
|
|
21
|
+
'@bit': bit,
|
|
22
|
+
'@txt': text,
|
|
23
|
+
...custom,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { getOpData } from '../utils/getOpData.js';
|
|
2
|
+
import { getTypeLike } from '../utils/getType.js';
|
|
3
|
+
import { log } from '../utils/log.js';
|
|
4
|
+
import { isAdd, mapAndFilterOps, updateRemovedOps } from '../utils/ops.js';
|
|
5
|
+
import { getArrayPrefixAndIndex, getIndexAndEnd, isArrayPath } from '../utils/paths.js';
|
|
6
|
+
import { getValue, pluckWithShallowCopy } from '../utils/pluck.js';
|
|
7
|
+
import { toArrayIndex } from '../utils/toArrayIndex.js';
|
|
8
|
+
import { updateArrayIndexes } from '../utils/updateArrayIndexes.js';
|
|
9
|
+
import { add } from './add.js';
|
|
10
|
+
export const move = {
|
|
11
|
+
like: 'move',
|
|
12
|
+
apply(state, path, from) {
|
|
13
|
+
if (path === from)
|
|
14
|
+
return;
|
|
15
|
+
let value;
|
|
16
|
+
const [keys, lastKey, target] = getOpData(state, from);
|
|
17
|
+
if (target === null) {
|
|
18
|
+
return `[op:move] path not found: ${from}`;
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(target)) {
|
|
21
|
+
const index = toArrayIndex(target, lastKey);
|
|
22
|
+
if (target.length <= index) {
|
|
23
|
+
return `[op:move] invalid array index: ${path}`;
|
|
24
|
+
}
|
|
25
|
+
value = target[index];
|
|
26
|
+
pluckWithShallowCopy(state, keys, true).splice(index, 1);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
value = target[lastKey];
|
|
30
|
+
delete pluckWithShallowCopy(state, keys, true)[lastKey];
|
|
31
|
+
}
|
|
32
|
+
return add.apply(state, path, value);
|
|
33
|
+
},
|
|
34
|
+
invert(_state, { path, from }) {
|
|
35
|
+
return { op: 'move', from: path, path: '' + from };
|
|
36
|
+
},
|
|
37
|
+
transform(state, thisOp, otherOps) {
|
|
38
|
+
log('Transforming', otherOps, 'against "move"', thisOp);
|
|
39
|
+
let removed = false;
|
|
40
|
+
const { from, path } = thisOp;
|
|
41
|
+
if (from === path)
|
|
42
|
+
return otherOps;
|
|
43
|
+
const [fromPrefix, fromIndex] = getArrayPrefixAndIndex(state, from);
|
|
44
|
+
const [pathPrefix, pathIndex] = getArrayPrefixAndIndex(state, path);
|
|
45
|
+
const isPathArray = pathPrefix !== undefined;
|
|
46
|
+
const isSameArray = isPathArray && pathPrefix === fromPrefix;
|
|
47
|
+
/*
|
|
48
|
+
A move needs to do a "remove" and an "add" at once with `from` and `path`. If it is being moved from one location in
|
|
49
|
+
an array to another in the same array, this needs to be handled special.
|
|
50
|
+
|
|
51
|
+
1. Ops that were added to where the move lands when not an array should be removed just like with an add/copy
|
|
52
|
+
2. Ops that were added to where the move came from should be translated to the new path
|
|
53
|
+
3. Ops that are in an array with the moved item after need to be adjusted up or down
|
|
54
|
+
3a. But, ops that were translated to the new path shouldn't get adjusted up or down by these adjustments
|
|
55
|
+
*/
|
|
56
|
+
// A move removes the value from one place then adds it to another, update the paths and add a marker to them so
|
|
57
|
+
// they won't be altered by `updateArrayIndexes`, then remove the markers afterwards
|
|
58
|
+
otherOps = mapAndFilterOps(otherOps, otherOp => {
|
|
59
|
+
if (removed) {
|
|
60
|
+
return otherOp;
|
|
61
|
+
}
|
|
62
|
+
const opLike = getTypeLike(state, otherOp);
|
|
63
|
+
if (opLike === 'remove' && from === otherOp.path) {
|
|
64
|
+
// Once an operation removes the moved value, the following ops should be working on the old location and not
|
|
65
|
+
// not the new one. Allow the following operations (which may include add/remove) to affect the old location
|
|
66
|
+
removed = true;
|
|
67
|
+
}
|
|
68
|
+
const original = otherOp;
|
|
69
|
+
otherOp = updateMovePath(state, otherOp, 'path', from, path, original);
|
|
70
|
+
otherOp = updateMovePath(state, otherOp, 'from', from, path, original);
|
|
71
|
+
return otherOp;
|
|
72
|
+
});
|
|
73
|
+
// Remove/adjust items that were affected by this item moving (those that actually moved because of it will not
|
|
74
|
+
// be affected because they have a temporary $ marker prefix that will keep them from doing so)
|
|
75
|
+
if (isSameArray) {
|
|
76
|
+
// need special logic when a move is within one array
|
|
77
|
+
otherOps = updateArrayIndexesForMove(state, fromPrefix, fromIndex, pathIndex, otherOps);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// if a move is not within one array, treat it as a remove then add
|
|
81
|
+
if (isArrayPath(from, state)) {
|
|
82
|
+
otherOps = updateArrayIndexes(state, from, otherOps, -1);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
otherOps = updateRemovedOps(state, from, otherOps);
|
|
86
|
+
}
|
|
87
|
+
if (isArrayPath(path, state)) {
|
|
88
|
+
otherOps = updateArrayIndexes(state, path, otherOps, 1);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
otherOps = updateRemovedOps(state, path, otherOps);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Remove the move markers added with `updateMovePath`
|
|
95
|
+
return mapAndFilterOps(otherOps, removeMoveMarkers);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Update paths for a move operation, adding a marker so the path will not be altered by array updates.
|
|
100
|
+
*/
|
|
101
|
+
function updateMovePath(state, op, pathName, from, to, original) {
|
|
102
|
+
const path = op[pathName];
|
|
103
|
+
if (!path)
|
|
104
|
+
return op; // No adjustment needed on a property that doesn't exist
|
|
105
|
+
// If a value is being added or copied to the old location it should not be adjusted
|
|
106
|
+
if (isAdd(state, op, pathName) && op.path === from) {
|
|
107
|
+
return op;
|
|
108
|
+
}
|
|
109
|
+
// If this path needs to be changed due to a move operation, change it, but prefix it with a $ temporarily so when we
|
|
110
|
+
// adjust the array indexes to account for this change, we aren't changing this path we JUST set. We will remove the
|
|
111
|
+
// $ prefix right after we adjust arrays affected by this move.
|
|
112
|
+
if (path === from || path.indexOf(from + '/') === 0) {
|
|
113
|
+
if (op === original)
|
|
114
|
+
op = Object.assign({}, op);
|
|
115
|
+
log('Moving', op, 'from', from, 'to', to);
|
|
116
|
+
// Add a marker "$" so this path will not be double-updated by array index updates
|
|
117
|
+
op[pathName] = '$' + path.replace(from, to);
|
|
118
|
+
}
|
|
119
|
+
return op;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Update array indexes to account for values being added or removed from an array. If the path is not an array index
|
|
123
|
+
* or if nothing is changed then the original array is returned.
|
|
124
|
+
*/
|
|
125
|
+
function updateArrayIndexesForMove(state, prefix, fromIndex, pathIndex, otherOps) {
|
|
126
|
+
// Check ops for any that need to be replaced
|
|
127
|
+
log(`Shifting array indexes for a move between ${prefix}/${fromIndex} and ${prefix}/${pathIndex}`);
|
|
128
|
+
return mapAndFilterOps(otherOps, otherOp => {
|
|
129
|
+
// check for items from the same array that will be affected
|
|
130
|
+
const fromUpdate = updateArrayPathForMove(state, otherOp, 'from', prefix, fromIndex, pathIndex);
|
|
131
|
+
const pathUpdate = updateArrayPathForMove(state, otherOp, 'path', prefix, fromIndex, pathIndex);
|
|
132
|
+
if (!fromUpdate || !pathUpdate)
|
|
133
|
+
return null;
|
|
134
|
+
if (fromUpdate !== otherOp || pathUpdate !== otherOp) {
|
|
135
|
+
otherOp = { ...otherOp, path: pathUpdate.path };
|
|
136
|
+
if (fromUpdate.from)
|
|
137
|
+
otherOp.from = fromUpdate.from;
|
|
138
|
+
}
|
|
139
|
+
return otherOp;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the adjusted path if it is higher, or undefined if not.
|
|
144
|
+
*/
|
|
145
|
+
function updateArrayPathForMove(state, otherOp, pathName, prefix, from, to) {
|
|
146
|
+
const path = otherOp[pathName];
|
|
147
|
+
if (!path || !path.startsWith(prefix))
|
|
148
|
+
return otherOp;
|
|
149
|
+
const min = Math.min(from, to);
|
|
150
|
+
const max = Math.max(from, to);
|
|
151
|
+
const [otherIndex, end] = getIndexAndEnd(state, path, prefix.length);
|
|
152
|
+
if (otherIndex === undefined)
|
|
153
|
+
return otherOp; // if a prop on an array is being set, for e.g.
|
|
154
|
+
const isFinalProp = end === path.length;
|
|
155
|
+
const opLike = getTypeLike(state, otherOp);
|
|
156
|
+
// If this index is not within the movement boundary, don't touch it
|
|
157
|
+
if (otherIndex < min || otherIndex > max) {
|
|
158
|
+
return otherOp;
|
|
159
|
+
}
|
|
160
|
+
// If the index touches the boundary on an unaffected side, don't touch it
|
|
161
|
+
if (isFinalProp && isAdd(state, otherOp, pathName)) {
|
|
162
|
+
/*
|
|
163
|
+
if the move is from low to high (min is a remove, max is an add) then
|
|
164
|
+
use the remove logic with an add
|
|
165
|
+
|
|
166
|
+
if the move is from high to low (min is an add, max is a remove) then
|
|
167
|
+
use the add logic at the bottom
|
|
168
|
+
*/
|
|
169
|
+
if (otherIndex === min) {
|
|
170
|
+
if (min === from) {
|
|
171
|
+
// treat like a remove
|
|
172
|
+
return otherOp;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// treat like an add
|
|
176
|
+
return otherOp;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (otherIndex === max) {
|
|
180
|
+
if (max === from) {
|
|
181
|
+
// treat like a remove
|
|
182
|
+
const fromIndex = getIndexAndEnd(state, otherOp.from, prefix.length)[0];
|
|
183
|
+
if (opLike === 'move' && pathName === 'path' && to <= fromIndex && fromIndex < from)
|
|
184
|
+
return otherOp;
|
|
185
|
+
// continue
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// treat like an add
|
|
189
|
+
return otherOp;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const modifier = from === min ? -1 : 1;
|
|
194
|
+
const newPath = prefix + (otherIndex + modifier) + path.slice(end);
|
|
195
|
+
return getValue(state, otherOp, pathName, newPath);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Remove any move markers placed during updateMovePath. This occurs in-place since these objects have already been
|
|
199
|
+
* cloned.
|
|
200
|
+
*/
|
|
201
|
+
function removeMoveMarkers(op) {
|
|
202
|
+
if (op.path[0] === '$') {
|
|
203
|
+
op.path = op.path.slice(1);
|
|
204
|
+
}
|
|
205
|
+
if (op.from && op.from[0] === '$') {
|
|
206
|
+
op.from = op.from.slice(1);
|
|
207
|
+
}
|
|
208
|
+
if (op.from === op.path)
|
|
209
|
+
return null;
|
|
210
|
+
return op;
|
|
211
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getOpData } from '../utils/getOpData.js';
|
|
2
|
+
import { log } from '../utils/log.js';
|
|
3
|
+
import { transformRemove } from '../utils/ops.js';
|
|
4
|
+
import { pluckWithShallowCopy } from '../utils/pluck.js';
|
|
5
|
+
import { toArrayIndex } from '../utils/toArrayIndex.js';
|
|
6
|
+
export const remove = {
|
|
7
|
+
like: 'remove',
|
|
8
|
+
apply(state, path) {
|
|
9
|
+
const [keys, lastKey, target] = getOpData(state, path);
|
|
10
|
+
if (target === null) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(target)) {
|
|
14
|
+
const index = toArrayIndex(target, lastKey);
|
|
15
|
+
if (target.length <= index) {
|
|
16
|
+
return '[op:remove] invalid array index: ' + path;
|
|
17
|
+
}
|
|
18
|
+
pluckWithShallowCopy(state, keys).splice(index, 1);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
delete pluckWithShallowCopy(state, keys)[lastKey];
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
invert(_state, { path }, value) {
|
|
25
|
+
return { op: 'add', path, value };
|
|
26
|
+
},
|
|
27
|
+
transform(state, thisOp, otherOps) {
|
|
28
|
+
log('Transforming', otherOps, 'against "remove"', thisOp);
|
|
29
|
+
return transformRemove(state, thisOp.path, otherOps, true);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { deepEqual } from '../utils/deepEqual.js';
|
|
2
|
+
import { getOpData } from '../utils/getOpData.js';
|
|
3
|
+
import { log } from '../utils/log.js';
|
|
4
|
+
import { updateRemovedOps } from '../utils/ops.js';
|
|
5
|
+
import { pluckWithShallowCopy } from '../utils/pluck.js';
|
|
6
|
+
import { toArrayIndex } from '../utils/toArrayIndex.js';
|
|
7
|
+
export const replace = {
|
|
8
|
+
like: 'replace',
|
|
9
|
+
apply(state, path, value) {
|
|
10
|
+
if (typeof value === 'undefined') {
|
|
11
|
+
return '[op:replace] require value, but got undefined';
|
|
12
|
+
}
|
|
13
|
+
const [keys, lastKey, target] = getOpData(state, path, true);
|
|
14
|
+
if (target === null) {
|
|
15
|
+
return `[op:replace] path not found: ${path}`;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(target)) {
|
|
18
|
+
const index = toArrayIndex(target, lastKey);
|
|
19
|
+
if (target.length <= index) {
|
|
20
|
+
return `[op:replace] invalid array index: ${path}`;
|
|
21
|
+
}
|
|
22
|
+
if (!deepEqual(target[index], value)) {
|
|
23
|
+
pluckWithShallowCopy(state, keys, true).splice(index, 1, value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
if (!deepEqual(target[lastKey], value)) {
|
|
28
|
+
pluckWithShallowCopy(state, keys, true)[lastKey] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
invert(_state, { path }, value, changedObj) {
|
|
33
|
+
if (path.endsWith('/-'))
|
|
34
|
+
path = path.replace('-', changedObj.length);
|
|
35
|
+
return value === undefined ? { op: 'remove', path } : { op: 'replace', path, value };
|
|
36
|
+
},
|
|
37
|
+
transform(state, thisOp, otherOps) {
|
|
38
|
+
log('Transforming ', otherOps, ' against "replace"', thisOp);
|
|
39
|
+
return updateRemovedOps(state, thisOp.path, otherOps);
|
|
40
|
+
},
|
|
41
|
+
compose(_state, _value1, value2) {
|
|
42
|
+
return value2;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { deepEqual } from '../utils/deepEqual.js';
|
|
2
|
+
import { getOpData } from '../utils/getOpData.js';
|
|
3
|
+
export const test = {
|
|
4
|
+
like: 'test',
|
|
5
|
+
apply(state, path, expected) {
|
|
6
|
+
const [, lastKey, target] = getOpData(state, path);
|
|
7
|
+
if (target === null) {
|
|
8
|
+
return `[op:test] path not found: ${path}`;
|
|
9
|
+
}
|
|
10
|
+
if (!deepEqual(target[lastKey], expected)) {
|
|
11
|
+
const a = JSON.stringify(target[lastKey]);
|
|
12
|
+
const b = JSON.stringify(expected);
|
|
13
|
+
return `[op:test] not matched: ${a} ${b}`;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
invert() {
|
|
17
|
+
return undefined;
|
|
18
|
+
},
|
|
19
|
+
transform(_state, _other, ops) {
|
|
20
|
+
return ops;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Delta } from '@dabble/delta';
|
|
2
|
+
import { replace } from '../ops/replace.js';
|
|
3
|
+
import { get } from '../utils/get.js';
|
|
4
|
+
import { log } from '../utils/log.js';
|
|
5
|
+
import { updateRemovedOps } from '../utils/ops.js';
|
|
6
|
+
export const text = {
|
|
7
|
+
like: 'replace',
|
|
8
|
+
apply(state, path, value) {
|
|
9
|
+
const delta = Array.isArray(value) ? new Delta(value) : value;
|
|
10
|
+
if (!delta || !Array.isArray(delta.ops)) {
|
|
11
|
+
return 'Invalid delta';
|
|
12
|
+
}
|
|
13
|
+
let existingData = get(state, path);
|
|
14
|
+
let doc;
|
|
15
|
+
if (Array.isArray(existingData)) {
|
|
16
|
+
if (existingData.length && existingData[0].insert) {
|
|
17
|
+
doc = new Delta(existingData);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else if (existingData && existingData.ops) {
|
|
21
|
+
doc = new Delta(existingData.ops);
|
|
22
|
+
}
|
|
23
|
+
if (!doc) {
|
|
24
|
+
doc = new Delta().insert('\n');
|
|
25
|
+
}
|
|
26
|
+
doc = doc.compose(delta);
|
|
27
|
+
if (hasInvalidOps(doc)) {
|
|
28
|
+
return 'Invalid text delta provided for this text document';
|
|
29
|
+
}
|
|
30
|
+
return replace.apply(state, path, doc);
|
|
31
|
+
},
|
|
32
|
+
transform(state, thisOp, otherOps) {
|
|
33
|
+
log('Transforming ', otherOps, ' against "@txt"', thisOp);
|
|
34
|
+
return updateRemovedOps(state, thisOp.path, otherOps, false, true, thisOp.op, op => {
|
|
35
|
+
if (op.path !== thisOp.path)
|
|
36
|
+
return null; // If a subpath, it is overwritten
|
|
37
|
+
if (!op.value || !Array.isArray(op.value))
|
|
38
|
+
return null; // If not a delta, it is overwritten
|
|
39
|
+
const thisDelta = new Delta(thisOp.value);
|
|
40
|
+
let otherDelta = new Delta(op.value);
|
|
41
|
+
otherDelta = thisDelta.transform(otherDelta, true);
|
|
42
|
+
return { ...op, value: otherDelta.ops };
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
invert(state, { path, value }, oldValue, changedObj) {
|
|
46
|
+
if (path.endsWith('/-'))
|
|
47
|
+
path = path.replace('-', changedObj.length);
|
|
48
|
+
const delta = new Delta(value);
|
|
49
|
+
return oldValue === undefined ? { op: 'remove', path } : { op: '@txt', path, value: delta.invert(oldValue) };
|
|
50
|
+
},
|
|
51
|
+
compose(state, delta1, delta2) {
|
|
52
|
+
return new Delta(delta1).compose(new Delta(delta2));
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
function hasInvalidOps(doc) {
|
|
56
|
+
return doc.ops.some(op => typeof op.insert !== 'string' && (typeof op.insert !== 'object' || op.insert === null));
|
|
57
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { JSONPatch } from './JSONPatch';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a proxy object that can be used in two ways:
|
|
4
|
+
*
|
|
5
|
+
* 1. **Path Generation:** When used without a `JSONPatch` instance, accessing properties
|
|
6
|
+
* on the proxy generates a JSON Pointer path string via `toString()`. This allows
|
|
7
|
+
* for type-safe path creation when using `JSONPatch` methods directly.
|
|
8
|
+
* ```ts
|
|
9
|
+
* const patch = new JSONPatch();
|
|
10
|
+
* const proxy = createPatchProxy<MyType>();
|
|
11
|
+
* patch.text(proxy.content, new Delta().insert('text')); // Path is '/content'
|
|
12
|
+
* patch.increment(proxy.counter, 5); // Path is '/counter'
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* 2. **Automatic Patch Generation:** When created with a target object and a `JSONPatch`
|
|
16
|
+
* instance, modifying the proxy (setting properties, calling array methods like
|
|
17
|
+
* `push`, `splice`, etc.) automatically generates the corresponding JSON Patch
|
|
18
|
+
* operations and adds them to the provided `patch` instance.
|
|
19
|
+
* ```ts
|
|
20
|
+
* const patch = new JSONPatch();
|
|
21
|
+
* const myObj = { name: { first: 'Alice' }, tags: ['a'] };
|
|
22
|
+
* const proxy = createPatchProxy(myObj, patch);
|
|
23
|
+
* proxy.name.first = 'Bob'; // Generates replace op
|
|
24
|
+
* proxy.tags.push('b'); // Generates add op
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The proxy behaves like the original value in most contexts due to the `valueOf` trap.
|
|
28
|
+
* For optional properties, use the non-null assertion operator (!) when setting values:
|
|
29
|
+
* ```ts
|
|
30
|
+
* interface User { middleName?: string }
|
|
31
|
+
* const proxy = createPatchProxy<User>(user, patch);
|
|
32
|
+
* proxy.middleName! = 'John'; // Assert middleName exists before setting
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @template T The type of the object to proxy.
|
|
36
|
+
* @param target The target object (required for automatic patch generation mode).
|
|
37
|
+
* @param patch The `JSONPatch` instance to add generated operations to (required for automatic patch generation mode).
|
|
38
|
+
* @returns A proxy object of type T.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createPatchProxy<T>(): T;
|
|
41
|
+
export declare function createPatchProxy<T>(target: T, patch: JSONPatch): T;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// We use a function as the target so that `push` and other array methods can be called without error.
|
|
2
|
+
const proxyFodder = {};
|
|
3
|
+
export function createPatchProxy(target, patch) {
|
|
4
|
+
// Call the internal implementation
|
|
5
|
+
return createPatchProxyInternal(target, patch);
|
|
6
|
+
}
|
|
7
|
+
// Internal implementation with the path parameter
|
|
8
|
+
function createPatchProxyInternal(target, patch, path = '') {
|
|
9
|
+
// Always use an empty function as the proxy target
|
|
10
|
+
// This allows us to proxy any type of value, including primitives and undefined,
|
|
11
|
+
// and enables calling array methods like push/splice directly on array proxies.
|
|
12
|
+
return new Proxy(proxyFodder, {
|
|
13
|
+
get(_, prop) {
|
|
14
|
+
// Return the value directly for symbol properties (not relevant for JSON paths)
|
|
15
|
+
if (typeof prop === 'symbol') {
|
|
16
|
+
return target?.[prop];
|
|
17
|
+
}
|
|
18
|
+
// Handle toString specially to make properties work as PathLike
|
|
19
|
+
if (prop === 'toString') {
|
|
20
|
+
return function () {
|
|
21
|
+
return path;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Handle valueOf to make the proxy behave like the original value in most contexts
|
|
25
|
+
if (prop === 'valueOf') {
|
|
26
|
+
return function () {
|
|
27
|
+
return target;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// --- Array Method Interception ---
|
|
31
|
+
if (Array.isArray(target)) {
|
|
32
|
+
switch (prop) {
|
|
33
|
+
case 'push':
|
|
34
|
+
return (...items) => {
|
|
35
|
+
const index = target.length;
|
|
36
|
+
for (let i = 0; i < items.length; i++) {
|
|
37
|
+
patch?.add(`${path}/${index + i}`, items[i]);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
case 'pop':
|
|
41
|
+
return () => {
|
|
42
|
+
const index = target.length - 1;
|
|
43
|
+
if (index >= 0) {
|
|
44
|
+
patch?.remove(`${path}/${index}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
case 'shift':
|
|
48
|
+
return () => {
|
|
49
|
+
if (target.length > 0) {
|
|
50
|
+
patch?.remove(`${path}/0`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
case 'unshift':
|
|
54
|
+
return (...items) => {
|
|
55
|
+
for (let i = 0; i < items.length; i++) {
|
|
56
|
+
patch?.add(`${path}/${i}`, items[i]);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
case 'splice':
|
|
60
|
+
return (start, deleteCount, ...items) => {
|
|
61
|
+
const actualStart = start < 0 ? Math.max(target.length + start, 0) : Math.min(start, target.length);
|
|
62
|
+
const actualDeleteCount = Math.min(Math.max(deleteCount === undefined ? target.length - actualStart : deleteCount, 0), target.length - actualStart);
|
|
63
|
+
// Remove deleted elements
|
|
64
|
+
for (let i = 0; i < actualDeleteCount; i++) {
|
|
65
|
+
patch?.remove(`${path}/${actualStart}`); // Path automatically adjusts for subsequent removes
|
|
66
|
+
}
|
|
67
|
+
// Add new elements
|
|
68
|
+
for (let i = 0; i < items.length; i++) {
|
|
69
|
+
patch?.add(`${path}/${actualStart + i}`, items[i]);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// --- End Array Method Interception ---
|
|
75
|
+
// Create a proxy for the property value if it's not an intercepted array method
|
|
76
|
+
// This handles objects, primitives, and undefined values uniformly
|
|
77
|
+
// Call the internal implementation recursively
|
|
78
|
+
return createPatchProxyInternal(target?.[prop], patch, `${path}/${String(prop)}`);
|
|
79
|
+
},
|
|
80
|
+
set(_, prop, value) {
|
|
81
|
+
// Ignore setting the 'length' property on arrays directly
|
|
82
|
+
if (Array.isArray(target) && prop === 'length') {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (target?.[prop] === value)
|
|
86
|
+
return true;
|
|
87
|
+
const patchPath = `${path}/${String(prop)}`;
|
|
88
|
+
if (value === undefined) {
|
|
89
|
+
patch?.remove(patchPath);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
patch?.replace(patchPath, value);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
deleteProperty(_, prop) {
|
|
97
|
+
if (target == null || prop in target) {
|
|
98
|
+
patch?.remove(`${path}/${String(prop)}`);
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
},
|
|
102
|
+
// Make the proxy appear to be the same type as the target
|
|
103
|
+
getPrototypeOf() {
|
|
104
|
+
return Object.getPrototypeOf(target);
|
|
105
|
+
},
|
|
106
|
+
// Support instanceof checks
|
|
107
|
+
isExtensible() {
|
|
108
|
+
return target != null && Object.isExtensible(target);
|
|
109
|
+
},
|
|
110
|
+
// Support Object.keys and other enumeration methods
|
|
111
|
+
ownKeys() {
|
|
112
|
+
return target != null && typeof target === 'object' ? Reflect.ownKeys(target) : [];
|
|
113
|
+
},
|
|
114
|
+
// Support property descriptor access
|
|
115
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
116
|
+
if (target == null || typeof target !== 'object')
|
|
117
|
+
return undefined;
|
|
118
|
+
return Object.getOwnPropertyDescriptor(target, prop);
|
|
119
|
+
},
|
|
120
|
+
// Support Object.hasOwnProperty
|
|
121
|
+
has(_, prop) {
|
|
122
|
+
return target != null && typeof target === 'object' && prop in target;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Based on work from
|
|
3
|
+
* https://github.com/Palindrom/JSONPatchOT
|
|
4
|
+
* (c) 2017 Tomek Wytrebowicz
|
|
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 type { JSONPatchOp, JSONPatchOpHandlerMap } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Transform an array of JSON Patch operations against another array of JSON Patch operations. Returns a new array with
|
|
16
|
+
* transformed operations. Operations that change are cloned, making the results of this function immutable.
|
|
17
|
+
* `otherOps` are transformed over `thisOps` with thisOps considered to have happened first.
|
|
18
|
+
*/
|
|
19
|
+
export declare function transformPatch(obj: any, thisOps: JSONPatchOp[], otherOps: JSONPatchOp[], custom?: JSONPatchOpHandlerMap): JSONPatchOp[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Based on work from
|
|
3
|
+
* https://github.com/Palindrom/JSONPatchOT
|
|
4
|
+
* (c) 2017 Tomek Wytrebowicz
|
|
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 { getTypes } from './ops/index.js';
|
|
14
|
+
import { runWithObject } from './state.js';
|
|
15
|
+
import { getType } from './utils/getType.js';
|
|
16
|
+
import { log } from './utils/log.js';
|
|
17
|
+
/**
|
|
18
|
+
* Transform an array of JSON Patch operations against another array of JSON Patch operations. Returns a new array with
|
|
19
|
+
* transformed operations. Operations that change are cloned, making the results of this function immutable.
|
|
20
|
+
* `otherOps` are transformed over `thisOps` with thisOps considered to have happened first.
|
|
21
|
+
*/
|
|
22
|
+
export function transformPatch(obj, thisOps, otherOps, custom) {
|
|
23
|
+
const types = getTypes(custom);
|
|
24
|
+
return runWithObject(obj, types, false, state => {
|
|
25
|
+
return thisOps.reduce((otherOps, thisOp) => {
|
|
26
|
+
// transform ops with patch operation
|
|
27
|
+
const handler = getType(state, thisOp)?.transform;
|
|
28
|
+
if (typeof handler === 'function') {
|
|
29
|
+
otherOps = handler(state, thisOp, otherOps);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
log('No function to transform against for', thisOp.op);
|
|
33
|
+
}
|
|
34
|
+
return otherOps;
|
|
35
|
+
}, otherOps);
|
|
36
|
+
});
|
|
37
|
+
}
|